제어 흐름과 오류 처리

JavaScript는 애플리케이션에 다양한 상호작용을 추가하기 위한 일련의 명령문, 특히 제어 흐름 명령문을 지원합니다. 이번 장에서는 이 명령문들을 간략하게 살펴보겠습니다.

여기서 다룬 명령문들의 자세한 설명은 JavaScript 참고서에서 읽을 수 있습니다. 세미콜론(;)은 두 명령문을 분리하기 위해 사용했습니다.

모든 JavaScript 표현식은 명령문이기도 합니다. 표현식에 대한 자세한 정보는 표현식과 연산자에서 확인하세요.

블록문

가장 기본적인 명령문은, 명령문들을 그룹으로 묶을 수 있는 블록문입니다. 블록문의 블록은 한 쌍의 중괄호로 감싸는 것으로 나타냅니다.

{
  statement_1;
  statement_2;
  ⋮
  statement_n;
}

예제

블록문은 제어 흐름 명령문과 많이 사용합니다. (if, for, while)

while (x < 10) {
  x++;
}

여기서 { x++; }가 블록문입니다.

참고: ECMA2015 (제6판) 이전의 JavaScript에는 블록 스코프가 없었습니다! 구형 JavaScript 코드에서는 블록 내에 정의한 변수의 스코프를 그 블록이 아니라, 변수 선언을 포함한 함수 또는 스크립트로 설정합니다. 때문에 변수 할당의 영향은 블록을 넘어서도 확인할 수 있습니다. 즉, 블록문이 스코프를 정의하지 않습니다.

JavaScript의 "독립 블록"은 C나 Java에서 기대할 수 있는 것과는 완전히 다른 결과를 낳습니다. 예를 들어,

var x = 1;
{
  var x = 2;
}
console.log(x); // 2 출력

위 코드가 2를 출력하는 이유는, 블록 내의 var x가 블록 밖의 var x와 같은 스코프를 갖기 때문입니다. C와 Java였다면, 위와 같은 코드에선 1을 출력했을 것입니다.

ECMAScript 2015에 도입된 letconst 변수 선언은 블록 스코프를 가집니다. 참고서의 letconst 문서에서 더 많은 정보를 확인하세요.

조건문

조건문은 지정한 조건이 참인 경우에 실행하는 명령 집합입니다. JavaScript는 if...elseswitch 두 종류의 조건문을 지원합니다.

if...else

명령문을 논리 조건이 참일 때 실행하려면 if 문을 사용하세요. 선택적으로, else 절을 추가해서 조건이 거짓인 경우에 실행할 명령문을 지정할 수 있습니다.

if 문의 형태는 다음과 같습니다.

if (condition) {
  statement_1;
} else {
  statement_2;
}

위 코드에서, condition에는 truefalse로 평가할 수 있는 아무 표현식이나 대입할 수 있습니다. (Boolean 문서에서 truefalse 평가에 대해 더 알아보세요.)

conditiontrue로 평가되면 statement_1을 실행합니다. 그렇지 않으면 statement_2를 실행합니다. statement_1statement_2에는 다른 if 문을 포함해 아무 명령문이나 사용할 수 있습니다.

else if를 사용해서 다수의 조건을 순차적으로 검사할 수도 있습니다.

if (condition_1) {
  statement_1;
} else if (condition_2) {
  statement_2;
} else if (condition_n) {
  statement_n;
} else {
  statement_last;
}

이 경우, 처음으로 true로 평가되는 조건의 명령문들만 실행됩니다.

모범 사례

일반적으로는 if에 항상, 특히 if 문을 중첩할 때는 블록문을 함께 사용하는 것이 좋습니다.

if (condition) {
  statement_1_runs_if_condition_is_true;
  statement_2_runs_if_condition_is_true;
} else {
  statement_3_runs_if_condition_is_false;
  statement_4_runs_if_condition_is_false;
}

또한 if...else의 조건에 "x = y"와 같은 할당은 지양하세요.

if ((x = y)) {
  /* 명령문 */
}

그러나 가끔은 조건에서 할당하는 것이 더 편한 경우도 있습니다. 이런 상황에 참고할 수 있도록, while 문서의 "할당을 조건으로 사용하기"에서 일반적인 모범 구문의 안내를 제공하고 있습니다.

거짓 값

다음 값은 false로 평가됩니다. (거짓 값이라고 부릅니다)

  • false
  • undefined
  • null
  • 0
  • NaN
  • 빈 문자열 ("")

객체를 포함해 다른 모든 값은 조건문에 전달했을 때 true로 평가됩니다.

참고: truefalse 원시 값을 Boolean 객체의 참과 거짓 값과 혼동하지 마세요!

예를 들어,

const b = new Boolean(false);
if (b)         // 참으로 평가
if (b == true) // 거짓으로 평가

예제

다음 예제에서, 함수 checkDataText 객체에 포함된 문자의 수가 3이면 true를 반환합니다. 그렇지 않으면 경고를 표시한 후 false를 반환합니다.

function checkData() {
  if (document.form1.threeChar.value.length == 3) {
    return true;
  } else {
    alert(
      '정확히 세 글자를 입력하세요. ' +
      `${document.form1.threeChar.value}은(는) 유효하지 않습니다.`);
    return false;
  }
}

switch

switch 문은 프로그램이 표현식을 평가한 후, 그 값과 case 레이블의 값을 비교해 일치하는 case의 명령문을 실행합니다.

switch 문의 모습은 다음과 같습니다.

switch (expression) {
  case label_1:
    statements_1;
    break;
  case label_2:
    statements_2;
    break;default:
    statements_default;
}

JavaScript는 위의 switch 문을 다음의 과정으로 평가합니다.

  • 우선 표현식(expression)의 결과와 일치하는 레이블을 가진 case 절을 찾아, 관련된 명령문을 실행합니다.
  • 일치하는 레이블을 찾지 못했으면 default 절을 탐색합니다.
    • default 절을 찾았으면 관련된 명령문을 실행합니다.
    • default 절을 찾지 못했으면 switch 문 바깥의 다음 명령문을 실행합니다.
    • (default를 마지막에 배치하는 것은 관습적인 것으로, 사실 위치는 상관 없습니다.)

break 문

각각의 case에는 선택적으로 break 문을 추가할 수 있습니다. breakcase의 명령문을 실행한 후에 프로그램이 switch의 밖으로 나가도록 합니다. break를 생략하면 프로그램은 switch 문을 탈출하지 않고, 다음 case의 명령문을 실행합니다.

예제

이 예제에서는 fruitType'바나나'라면 '바나나' 레이블을 가진 case의 명령문을 실행합니다. break를 마주치면 프로그램이 switch 밖으로 나가서 바로 다음 명령문을 실행합니다. '바나나'break가 없었다면 case '체리' 아래의 명령문도 실행했을 것입니다.

switch (fruittype) {
  case '오렌지':
    console.log('오렌지는 파운드 당 $0.59입니다.');
    break;
  case '사과':
    console.log('사과는 파운드 당 $0.32입니다.');
    break;
  case '바나나':
    console.log('바나나는 파운드 당 $0.48입니다.');
    break;
  case '체리':
    console.log('체리는 파운드 당 $3.00입니다.');
    break;
  case '망고':
    console.log('망고는 파운드 당 $0.56입니다.');
    break;
  case '파파야':
    console.log('망고와 파파야는 파운드 당 $2.79입니다.');
    break;
  default:
    console.log(`죄송합니다. ${fruitType}은 품절입니다.`);
}
console.log('더 필요한게 있으신가요?');

예외 처리 명령문

throw 문을 사용하면 예외를 던질 수 있고, 던진 예외는 try...catch 문으로 처리할 수 있습니다.

예외 유형

JavaScript에서는 모든 것을 throw로 던질 수 있습니다. 그래서 숫자나 문자열을 오류로 던지는 경우도 많지만, 예외를 나타내기 위해 사전에 정의된 예외 유형을 쓰는 것이 보통 더 효과적입니다.

throw 문

예외를 던질 땐 throw 문을 사용하세요. throw에 던질 값을 지정하면 됩니다.

throw expression;

특정 타입의 표현식이 아니라 무엇이든 던질 수 있습니다. 아래 코드에서 다양한 타입을 예외로 던지는 모습을 볼 수 있습니다.

throw 'Error2'; // String
throw 42; // Number
throw true; // Boolean
throw {
  toString: function () {
    return '저는 객체예요';
  },
};

try...catch

try...catch 문은 실행을 시도할 블록을 표시하고, 그 안에서 예외가 발생할 경우 처리를 맡을 하나 이상의 반응 명령문을 지정합니다. 예외가 발생하면, try...catch 문이 잡아냅니다.

try...catch 문은 하나 이상의 명령문을 포함하는 try 블록, 그리고 try에서 예외가 발생할 경우 그 예외를 처리할 명령문을 담은 하나의 catch 블록으로 구성합니다.

다르게 설명해보면, try...catch는 성공하길 바라는 코드(try 블록)가 만약 실패하면 catch로 제어권을 넘겨야 할 때 사용합니다. try 블록의 명령문 중 하나에서 예외를 던지면, 실행 제어권은 그 즉시 catch 블록으로 넘어갑니다. try 블록 내에서 예외가 발생하지 않았으면 catch 블록은 실행되지 않습니다.

다음은 try...catch의 사용 모습을 보이는 예제입니다. getMonthName()은 매개변수의 값을 사용해 months 배열에서 영어 월 이름을 가져옵니다. 그런데 유효한 월(1-12) 범위의 숫자 값을 받은 것이 아니라면 'InvalidMonthNo'를 값으로 한 예외를 던집니다. 그러면 catch 블록의 명령문이 monthName 변수를 'unknown'으로 설정합니다.

function getMonthName(mo) {
  mo = mo - 1; // 배열 인덱스에 맞춰 월 조절 (1 = Jan, 12 = Dec)
  let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  if (months[mo]) {
    return months[mo];
  } else {
    throw 'InvalidMonthNo'; // 여기서 throw 키워드 사용
  }
}

try {
  // 시도할 명령문
  monthName = getMonthName(myMonth); // 예외가 발생할 수 있는 함수
} catch (e) {
  monthName = 'unknown';
  logMyErrors(e); // 오류 처리기에 예외 객체 전달
}

catch 블록

try 블록에서 발생할 수 있는 모든 예외는 catch 블록에서 처리할 수 있습니다.

catch (catchID) {
  statements;
}

catch 블록은 throw 명령문이 던진 예외의 값을 담을 식별자(위 코드 블록에서는 catchID)를 지정합니다. 이 식별자를 통해, 발생한 예외의 정보를 알아낼 수 있습니다.

JavaScript는 catch 블록에 진입해야 예외의 식별자를 생성하고, catch 블록의 밖으로 나가면 식별자를 더 이상 유지하지 않습니다. 즉, catch 블록의 실행이 끝나면 예외 식별자에 접근할 수 없습니다.

아래는 예외를 던지고 잡는 예제 코드입니다. 예외를 던지면 그 순간 제어권이 catch 블록으로 넘어갑니다.

try {
  throw 'myException'; // 예외 생성
} catch (e) {
  // 모든 예외를 처리하기 위한 명령문
  logMyErrors(e); // 오류 처리기에 예외 객체 전달
}

참고: catch 블록 안에서 오류를 기록할 땐 console.log()보다 console.error()를 사용하는 것을 추천합니다. console.error()는 출력 형식도 오류에 적합하고, 현재 페이지에서 발생한 오류의 목록도 업데이트합니다.

finally 블록

finally 블록은 trycatch 블록 실행이 끝난 후 이어서, 그리고 try...catch...finally 문 이후의 명령문들보다는 먼저 실행할 명령문을 담습니다.

finally 블록은 try 블록 안에서 예외가 발생했는지 여부에 관계 없이, catch 블록이 따로 존재하지 않더라도 항상 실행됩니다.

finally 블록을 활용하면 예외가 발생했을 때 프로그램이 우아하게 실패하도록 방어할 수 있습니다. 예를 들어, 예외의 발생 여부를 따지지 않고 스크립트가 점유한 리소스를 해제해야 할 때 사용하세요.

다음의 코드 예제는 파일을 열어서 그 안의 데이터를 사용하는 함수(writeMyFile())를 호출하는 예시입니다. (서버사이드 JavaScript에서는 파일에 접근할 수 있습니다) 파일이 열려있는 동안 예외가 나타나면, 스크립트가 실패하기 전에 finally 블록이 파일을 닫아줍니다. finally를 사용함으로써 오류가 발생하더라도 파일이 열린 채 방치되는 일을 방지할 수 있는 것입니다.

openMyFile();
try {
  writeMyFile(theData); // 오류가 발생할 수 있는 코드
} catch (e) {
  handleError(e); // 오류가 발생하면 처리함
} finally {
  closeMyFile(); // 항상 리소스 해제
}

만약 finally 블록이 값을 반환한다면, 그 값이 전체 try...catch...finally 문의 반환 값이 됩니다. trycatch 블록에서 반환하는 값은 무시합니다.

function f() {
  try {
    console.log(0);
    throw 'bogus';
  } catch (e) {
    console.log(1);
    return true; // finally 블록의 실행이 끝날 때까지 중단됨
    console.log(2); // 접근 불가
  } finally {
    console.log(3);
    return false; // 앞선 return보다 우선함
    console.log(4); // 접근 불가
  }
  // return false가 실행됨
  console.log(5); // 접근 불가
}
console.log(f()); // 0, 1, 3, false

finally의 반환 값이 우선하는 것은 catch 블록에서 던진 예외에도 적용됩니다.

function f() {
  try {
    throw '예외';
  } catch (e) {
    console.log('내부 "예외" 포획');
    throw e; // finally 블록의 실행이 끝날 때까지 중단
  } finally {
    return false; // 앞선 throw보다 우선함
  }
  // return false가 실행됨
}

try {
  console.log(f());
} catch (e) {
  // 도달 불가능한 catch 블록!
  // f()가 실행되면 `finally`에서 false를 반환함
  // 반환이 catch의 throw보다 우선했으므로 예외가 없음
  console.log('caught outer "bogus"');
}

// 출력 결과
// 내부 "예외" 포획
// false

try...catch 문 중첩하기

try...catch 문을 하나 이상 중첩할 수 있습니다.

안쪽 try 블록이 catch 블록을 가지지 않을 경우,

  1. try 블록에는 반드시 finally 블록이 있어야 합니다.
  2. 바깥 try...catch 문의 catch 블록이 안쪽 예외를 처리하게 됩니다.

더 자세한 정보는 try...catch 참고서의 중첩 try 블록을 참고하세요.

Error 객체 활용하기

Error 객체의 namemessage 속성으로부터 오류의 유형에 따라 좀 더 정제된 메시지를 가져올 수 있습니다.

nameError의 일반적인 클래스(DOMException, Error, 등등)를 제공합니다. message는 오류 메시지인데, 대개 오류 객체를 문자열로 변환한 결과보다 더 간결한 형태입니다.

직접 예외를 던지려는 경우에는, 특히 catch 블록이 시스템 오류와 직접 던지는 예외를 구분하지 않는다면, Error 생성자를 사용해서 두 속성을 제공할 수 있습니다.

function doSomethingErrorProne () {
  if (ourCodeMakesAMistake()) {
    throw (new Error('메시지'));
  } else {
    doSomethingToGetAJavascriptError();
  }
}try {
  doSomethingErrorProne();
}
catch (e) {
  console.log(e.name); // 'Error' 기록
  console.log(e.message); // '메시지' 또는 JavaScript 오류 메시지 기록
}