Skip to content

Latest commit

 

History

History
560 lines (480 loc) · 18.6 KB

7주차.md

File metadata and controls

560 lines (480 loc) · 18.6 KB

1. 'try..catch'와 에러 핸들링
2. 커스텀 에러와 확장


'try..catch'와 에러 핸들링

  • 'try..catch' 에러 문법
     // 에러가 없는 코드
     try {
       alert('try 블록 시작');  // (1) <--
       // ...에러가 없습니다.
       alert('try 블록 끝');   // (2) <--
     } catch(err) {
       alert('에러가 없으므로, catch는 무시됩니다.'); // (3)
     }
     
     // 에러가 있는 코드
     try {
       alert('try 블록 시작');  // (1) <--
       lalala; // 에러, 변수가 정의되지 않음!
       alert('try 블록 끝(절대 도달하지 않음)');  // (2)
     } catch(err) {
       alert(`에러가 발생했습니다!`); // (3) <--
     }
    
    • 에러가 없으면 (1) -> (2)
    • 에러가 있다면 (1) -> (3)

[!WARNING] try..ctch는 오직 런타임 에러만 동작합니다. 문법적으로 잘못된 경우에는 try..catch가 작동하지 않음

try {
  {{{{{{{{{{{
} catch(e) {
  alert('유효하지 않은 코드이기 때문에, 자바스크립트 엔진은 이 코드를 이해할 수 없습니다.');
}

자바스크립트 엔진은 코드를 읽고 난 후 코드를 실행함. 코드를 읽는 중에 발생하는 에러는 'parse-time'에러라고 부르는데, 엔진은 이 코드를 이해할 수 없기 때문에 parse-time 에러는 코드 안에서 복구가 불가능함 => try..catch는 유효한 코드에서 발생하는 에러만 처리할 수 있음. 이런 에러를 '런타임 에러' 혹은 '예외'라고 부름

[!WARNING] try..catch는 동기적으로 동작합니다. setTimeout처럼 '스케줄 된(scheduled)' 코드에서 발생한 예외는 try..catch에서 잡아낼 수 없음

try {
  setTimeout(function() {
  	noSuchVariable; // 스크립트는 여기서 죽습니다.
  }, 1000);
} catch (e) {
  alert('작동 멈춤');
}

setTimeout에 넘겨진 익명 함수는 엔진이 try..catch를 떠난 다음에서야 실행되기 때문 스케줄 된 함수 내부의 예외를 잡으려면, try..catch를 함수 내부에 구현해야 함

setTimeout(function() {
  try {
  	noSuchVariable; // 이제 try..catch에서 에러를 핸들링함
  } catch {
  	alert('에러를 잡았습니다');
  }
}, 1000);

  • 에러 객체의 프로퍼티
    • name - 에러 이름 ex) 정의되지 않은 변수 때문에 발생한 에러라면 ReferenceError가 이름이 됨
    • message - 에러 상세 내용을 담고 있는 문자 메세지
    • stack - 현재 호출 스택. 에러를 유발한 중첩 호출들의 순서 정보를 가진 문자열로 디버깅 목적으로 사용
     try {
       lalala; // 에러, 변수가 정의되지 않음!
     } catch(err) {
       alert(err.name); // ReferenceError
       alert(err.message); // lalala is not defined
       alert(err.stack); // ReferenceError: lalala is not defined at ... (호출 스택)
     
       // 에러 전체를 보여줄 수도 있습니다.
       // 이때, 에러 객체는 "name: message" 형태의 문자열로 변환됩니다.
       alert(err); // ReferenceError: lalala is not defined
     }
    

  • 에러 정보가 필요 없다면 생략할 수도 있음
     try {
       // ...
     } catch { // <-- (err) 없이 쓸 수 있음
       // ...
     }
    

  • try..catch를 통해 스크립트가 죽은 원인을 파악할 수 있음
    • catch 블록 안에서 새로운 네트워크 요청 보내기, 사용자에게 대안 제안하기, 로깅 장치에서 에러 정보 보내기 등의 작업을 할 수 있음
     let json = "{ bad json }";
     
     try {
       let user = JSON.parse(json); // <-- 여기서 에러가 발생하므로
       alert(user.name); // 이 코드는 동작하지 않음
     } catch (e) {
       // 에러가 발생하면 제어 흐름이 catch 문으로 �넘어옴
       alert("데이터에 에러가 있어 재요청을 시도합니다.");
       alert(e.name);
       alert(e.message);
     }
    

  • json이 문법적으로 잘못되지 않고, 필수 프로퍼티를 가지고 있지 않아서 오류가 발생했을 경우 throw 연산자를 사용해 에러처리를 진행해야 함
    • throw <error object>
     let json = '{ "age": 30 }'; // 불완전한 데이터
     
     try {
       let user = JSON.parse(json); // <-- 에러 없음
       alert(user.name); // 이름이 없습니다!
     } catch (e) {
       alert("실행되지 않습니다.");
     }
    

  • 자바스크립트는 Error, SyntaxError, ReferenceError, TypeError 등의 표준 에러 객체 관련 생성자를 지원하기 때문에 이를 활용해서 에러 객체를 만들 수 있음
     let error = new Error(message);
     // or
     let error = new SyntaxError(message);
     let error = new ReferenceError(message);
     // ...
    
     // `name` 프로퍼티는 생성자 이름과 동일한 값을 가짐
     let error = new Error("이상한 일이 발생했습니다. o_O");
     
     alert(error.name); // Error
     alert(error.message); // 이상한 일이 발생했습니다. o_O
    

  • throw 문을 활용해 에러 객체로 던지기
     let json = '{ "age": 30 }'; // 불완전한 데이터
     
     try {
       let user = JSON.parse(json); // <-- 에러 없음
       if (!user.name) {
         throw new SyntaxError("불완전한 데이터: 이름 없음"); // (*)
       }
       alert( user.name );
     } catch(e) {
       alert("JSON Error: " + e.message); // JSON Error: 불완전한 데이터: 이름 없음
     }
    
    • (*) - throw 연산자는 message를 이용해 SyntaxError를 생성

  • 또 다른 예기치 않은 에러try {...} 블록 안에 발생할 경우 에러가 발생할 수도 있음
  • 정의되지 않은 변수 사용 등의 프로그래밍 에러가 발생할 가능성은 항상 존재
     let json = '{ "age": 30 }'; // 불완전한 데이터
     
     try {
       user = JSON.parse(json); // <-- user 앞에 let을 붙이는 걸 잊었네요.
       // ...
     } catch(err) {
       alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
       // (실제론 JSON Error가 아닙니다.)
     }
    
    • 위의 코드에서는 예상치 못한 에러를 잡아내 주긴 했지만, 에러 종류와 관계 없이 'JSON Error' 메세지를 보여줌
    • 이렇게 에러 종류와 관계 없이 동일한 방식으로 에러를 처리하는 것은 디버깅을 어렵게 만듦 => '다시 던지기(rethrowing)' 기술 사용

  • catch는 알고 있는 에러만 처리하고 나머지는 '다시 던져야'함
    1. catch가 모든 에러를 받음
    2. catch(err) {...} 블록 안에서 에러 객체 err를 분석
    3. 에러 처리 방법을 알지 못하면 throw err를 함
  • 이때의 에러 타입을 instanceof 명령어로 체크
     try {
       user = { /*...*/ };
     } catch(err) {
       if (err instanceof ReferenceError) {
         alert('ReferenceError'); //  정의되지 않은 변수에 접근하여 'ReferenceError' 발생
       }
     }
    

  • err.name 프로퍼티로 에러 클래스 이름을 알 수 있음
  • 기본형 에러는 모두 err.name 프로퍼티를 가짐 or err.constructor.name을 사용할 수 있음
     let json = '{ "age": 30 }'; // 불완전한 데이터
     try {
       let user = JSON.parse(json);
       if (!user.name) {
         throw new SyntaxError("불완전한 데이터: 이름 없음");
       }
       blabla(); // 예상치 못한 에러
       alert(user.name);
     } catch(e) {
       if (e instanceof SyntaxError) {
         alert("JSON Error: " + e.message);
       } else {
         throw e; // 에러 다시 던지기 (*)
       }
     }
    
    • (*) - 다시 던져진 에러는 try..catch '밖으로 던져짐'
      • 바깥에 try..catch가 있다면 여기서 에러를 잡음. 아니면 스크립트가 죽음
    • catch 블록에선 어떻게 다룰지 알고 있는 에러만 처리하고, 알 수 없는 에러는 '건너뛸 수' 있음

  • 던져진 에러를 다시 try..catch 문을 통해 에러 검출
     function readData() {
       let json = '{ "age": 30 }';
     
       try {
         // ...
         blabla(); // 에러!
       } catch (e) {
         // ...
         if (!(e instanceof SyntaxError)) {
           throw e; // 알 수 없는 에러 다시 던지기
         }
       }
     }
     
     try {
       readData();
     } catch (e) {
       alert("External catch got: " + e); // 에러를 잡음
     }
    
    • readDataSyntaxError만 처리할 수 있지만, 함수 바깥의 try..catch에서는 예상치 못한 에러도 처리할 수 있게 되었음

  • finally를 통해 try..catch를 확장할 수 있음
     try {
       alert( 'try 블록 시작' );
       if (confirm('에러를 만드시겠습니까?')) 이상한_코드();
     } catch (e) {
       alert( 'catch' );
     } finally {
       alert( 'finally' );
     }
    
    1. '에러를 만드시겠습니까?'에 'OK'로 답한 경우: try -> catch -> finally
    2. 'No'로 답한 경우: try -> finally
  • finally 절은 무언가를 실행하고, 실행 결과에 상관없이 실행을 완료하고 싶을 때 사용
    • ex) 피보나치 함수 fn(n) 연산 시간을 측정하고 싶을때, 도중에 에러가 발생하면 측정 불가능하니 finally 절에 넣음

[!NOTE] try..catch..finally 안의 변수는 지역변수입니다. try 블록 내부에서 선언한 변수는 블록 안에서만 유효한 지역변수가 됨

[!NOTE] finallyreturn finally절은 try..catch 절을 빠져나가는 어떤 경우에도 실행 return을 사용해 명시적으로 빠져나가려는 경우도 마찬가지 아래의 코드의 try 블록 안엔 return이 존재하고, 이 경우엔 값이 바깥 코드로 반환되기 전에 finally가 실행됨

function func() {
  try {
  	return 1;
  } catch(e) {
  	/ * ... * /
  } finally {
  	alert('finally');
  }
}
alert(func()); // finally 안의 alert가 실행되고 난 후, 실행됨

[!NOTE] try..finally catch 절이 없는 try..finally 구문도 상황에 따라 유용하게 쓸 수 있음 try..finally 안에선 에러를 처리하고 싶지 않지만, 시작한 프로세스가 마무리되었는지 확실히 하고 싶은 경우에 사용

function func() {
  try {
  	// ...
  } finally {
  	// 스크립트가 죽도라도 완료됨
  }
}

커스텀 에러와 에러 확장

  • 자체 에러 클래스를 위한 커스텀 에러를 만들 수 있임
  • 직관적임 ex) 네트워크 에러 - HttpError / 데이터베이스 에러 - DbError / 검색 에러 - NotFoundError
  • throw 인수엔 아무런 제약이 없기 때문에 보통 Error를 상속받아서 에러 객체를 생성함 > 상속 받은 Error를 이용해 커스텀 에러를 만듦

  • 사용자 데이터가 저장된 JSON을 읽는 함수 readUser(json)이 존재
    • 유효한 json은 아래와 같은 형태
       let json = `{ "name": "John", "age": 30 }`;
      
  • 잘못된 json 형식이 들어오면 SyntaxError가 발생
  • JSON 형식은 맞지만, 프로퍼티가 누락되는 등의 상황이 발생할 경우
    • 데이터를 '검증'하는 에러를 추가 > ValidationError

  • 일반적으로 Error 클래스를 상복받아 ValidationError 클래스로 확장함
    • 슈도 코드란? 실제 동작 방식을 이해하기 쉽게 설명하기 위해 작성된 가상의 코드 / 구체적인 구현 세부 사항보다는 기본적인 개념을 전달하는 데 초첨이 맞춰있음
     // 자바스크립트 자체 내장 에러 클래스 Error의 '슈도 코드'
     class Error {
       constructor(message) {
         this.message = message;
         this.name = "Error"; // (name은 내장 에러 클래스마다 다릅니다.)
         this.stack = <call stack>;  // stack은 표준은 아니지만, 대다수 환경이 지원합니다.
       }
     }
     
     class ValidationError extends Error {
       constructor(message) {
         super(message); // (1)
         this.name = "ValidationError"; // (2)
       }
     }
     
     function test() {
       throw new ValidationError("에러 발생!");
     }
     
     try {
       test();
     } catch(err) {
       alert(err.message); // 에러 발생!
       alert(err.name); // ValidationError
       alert(err.stack); // 각 행 번호가 있는 중첩된 호출들의 목록
     }
    
    • 자바스크립트에서는 자식 생성자 안에서 super를 반드시 호출해야 함. message 프로퍼티는 부모 생성자에서 설정됨
    • 부모 생성자에선 message 뿐만이 아니라 name 프로퍼티도 설정하기 때문에 (2) 에서 원하는 값으로 재설정해 주었음

  • readUser(json) 내부에서 사용하는 ValidationError
     class ValidationError extends Error {
       constructor(message) {
         super(message);
         this.name = "ValidationError";
       }
     }
     
     // 사용법
     function readUser(json) {
       let user = JSON.parse(json);
     
       if (!user.age) {
         throw new ValidationError("No field: age");
       }
       if (!user.name) {
         throw new ValidationError("No field: name");
       }
     
       return user;
     }
     
     // try..catch와 readUser를 함께 사용한 예시
     
     try {
       let user = readUser('{ "age": 25 }');
     } catch (err) {
       if (err instanceof ValidationError) {
         alert("Invalid data: " + err.message); // Invalid data: No field: name
       } else if (err instanceof SyntaxError) { // (*)
         alert("JSON Syntax Error: " + err.message);
       } else {
         throw err; // 알려지지 않은 에러는 재던지기 합니다. (**)
       }
     }
    
    • try..catch 블록에서 커스텀 에러 ValidationErrorJSON.parse에서 발생하는 SyntaxError 둘 다 처리할 수 있게 되었음
    • instanceof를 통해 에러 유형 확인 가능((*))
    • err.name으로도 에러 유형 확인 가능
       // ...
       // (err instanceof SyntaxError) 대신 사용 가능
       } else if (err.name == "SyntaxError") { // (*)
       // ...
      

  • 위의 코드에서 에러 유형은 instanceof를 사용하는 것을 권장((*))
  • 나중에 ValidationError를 확장해서 PropertyRequiredError 같은 새로운 확장 에러를 만들게 될 때, instanceof는 새로운 상속 클래스에서도 동작함
  • catch 블록에선 유효성 검사와 문법 오류만 처리하고, 다른 종류의 에러는 밖으로 던져야 함((**))

  • ValidationError는 프로퍼티가 누락되거나 age에 문자열 값이 들어가는 것처럼 형식이 잘못된 경우를 처리할 수 없음

  • 필수 프로퍼티가 없는 경우에 대응할 수 있도록 구체적인 에러를 만듦 > PropertyRequiredError

     class ValidationError extends Error {
       constructor(message) {
         super(message);
         this.name = "ValidationError";
       }
     }
     
     // 프로퍼티의 누락 여부를 확인할 수 있는 에러
     class PropertyRequiredError extends ValidationError {
       constructor(property) {
         super("No property: " + property);
         this.name = "PropertyRequiredError";
         this.property = property;
       }
     }
     
     // 사용법
     function readUser(json) {
       let user = JSON.parse(json);
     
       if (!user.age) {
         throw new PropertyRequiredError("age");
       }
       if (!user.name) {
         throw new PropertyRequiredError("name");
       }
     
       return user;
     }
     
     // try..catch와 readUser를 함께 사용하면 다음과 같습니다.
     try {
       let user = readUser('{ "age": 25 }');
     } catch (err) {
       if (err instanceof ValidationError) {
         alert("Invalid data: " + err.message); // Invalid data: No property: name
         alert(err.name); // PropertyRequiredError
         alert(err.property); // name
       } else if (err instanceof SyntaxError) {
         alert("JSON Syntax Error: " + err.message);
       } else {
         throw err; // 알려지지 않은 에러는 재던지기 합니다.
       }
     }
    
    • this.property에 누락된 속성의 이름을 저장하는 프로퍼티를 전달하면 됨
  • PropertyRequiredErrorValidationError처럼 this.name을 수동으로 할당해 주었음 > 매번 커스텀 에러 클래스의 생성자에 할당해 주는 것은 번거로움 > '기본 에러' 클래스를 만들고 커스텀 에러들이 이 클래스를 상속받게 하면 해결 가능

  • 기본 에러의 생성자에 this.name = this.constructor.name 추가

     class MyError extends Error {
       constructor(message) {
         super(message);
         this.name = this.constructor.name;
       }
     }
     
     class ValidationError extends MyError { }
     
     class PropertyRequiredError extends ValidationError {
       constructor(property) {
         super("No property: " + property);
         this.property = property;
       }
     }
     
     // 제대로 된 이름이 출력됩니다.
     alert(new PropertyRequiredError("field").name); // PropertyRequiredError
    

  • 현재는 ValidationError, SyntaxError로만 에러 처리를 진행하고 있지만 readUser가 커질수록 에러를 확장할 필요가 있음
  • 하지만 에러 종류에 따라 에러 처리 분기문을 매번 추가해야하는 불편함이 존재 > '예외 감싸기'를 통해 예방 가능
    1. '데이터 읽기'와 같은 포괄적인 에러를 대변하는 새로운 클래스 ReadError를 만듦
    2. 함수 readUser 발생한 ValidationError, SyntaxError 등의 에러는 readUser 내부에서 잡고, 이때 ReadUser를 생성함
    3. ReadUser 객체의 cause 프로퍼티엔 실제 에러에 대한 참조가 저장됨
  • 예외 감싸기 기술을 통해 스키마를 변경하면 readUser를 호출하는 코드에선 ReadError만 확인하면 됨
    • 에러 종류 전체를 확인할 필요 X / 추가 정보가 필요한 경우엔 cause 프로퍼티를 확인
     class ReadError extends Error {
       constructor(message, cause) {
         super(message);
         this.cause = cause;
         this.name = 'ReadError';
       }
     }
     
     class ValidationError extends Error { /*...*/ }
     class PropertyRequiredError extends ValidationError { /* ... */ }
     
     function validateUser(user) {
       if (!user.age) {
         throw new PropertyRequiredError("age");
       }
     
       if (!user.name) {
         throw new PropertyRequiredError("name");
       }
     }
     
     function readUser(json) {
       let user;
     
       try {
         user = JSON.parse(json);
       } catch (err) {
         if (err instanceof SyntaxError) {
           throw new ReadError("Syntax Error", err);
         } else {
           throw err;
         }
       }
     
       try {
         validateUser(user);
       } catch (err) {
         if (err instanceof ValidationError) {
           throw new ReadError("Validation Error", err);
         } else {
           throw err;
         }
       }
     }
     
     try {
       readUser('{잘못된 형식의 json}');
     } catch (e) {
       if (e instanceof ReadError) {
         alert(e);
         // Original error: SyntaxError: Unexpected token b in JSON at position 1
         alert("Original error: " + e.cause);
       } else {
         throw e;
       }
     }
    
    • Syntax 에러나 Validation 에러가 발생한 경우 해당 에러 차제를 다시 던지기하지 않고 ReadError를 던지게 됨
    • readUser를 호출하는 바깥 코드에선 instanceof ReadError 하나만 확인하면 됨
    • e.cause를 통해 구체적인 에러 정보를 확인할 수 있지만 필수 프로퍼티는 아님