[번역] 자바스크립트 에러 핸들링 방법

cookie004·2022년 6월 13일
53

좋은 아티클 번역

목록 보기
2/2

자바스크립트 에러 핸들링 방법

원문: https://blog.openreplay.com/an-introduction-to-javascript-error-handling

개발 경험이 쌓일수록 당신의 앱은 더욱 견고해집니다. 코딩 기술의 개선이 많은 부분을 차지하지만, 작은 엣지 케이스를 다루는 방법도 배우게 될 것입니다. 일반적으로 첫 번째 사용자가 당신의 새 시스템에 접속할 때 문제가 발생할 수 있습니다.

어떤 에러들은 피할 수 있습니다.

  • 자바스크립트 린터(linter) 또는 좋은 에디터는 오타나 누락된 대괄호 같은 구문 에러를 잡아줄 수 있습니다.
  • 유효성 검사(validation)를 사용하여 유저 데이터나 다른 시스템의 데이터 에러를 잡을 수 있습니다.
    절대로 대혼란을 일으키는 유저의 독창성을 추측하려 하지 마세요. 사용자의 나이를 물어볼 때 그들이 정수인 숫자로 답할 거라 예상하겠지만, 그들은 빈칸으로 두거나, 음수를 입력하거나, 분수를 사용합니다. 심지어 완전 그들의 모국어대로 "스물한 살"처럼 입력할 수도 있습니다.
    서버에서의 유효성 검사는 필수라는 걸 기억하세요. 브라우저 기반 클라이언트에서의 유효성 검사는 UI를 향상시킬 수 있지만 사용자는 자바스크립트가 비활성화되거나 로딩되지 않거나 실행되지 않는 애플리케이션을 사용할 수도 있습니다. 또한 API를 호출하는 브라우저가 아닐 수도 있습니다!

다른 런타임 에러들은 피하는 것이 불가능합니다.

  • 네트워크 연결에 실패할 수 있습니다.
  • 서버가 바쁘거나 애플리케이션의 응답이 너무 느릴 수 있습니다.
  • 스크립트 또는 다른 애셋을 로드할 때, 시간 초과 될 수 있습니다.
  • 애플리케이션이 실패할 수 있습니다.
  • 디스크나 데이터베이스가 실패할 수 있습니다.
  • 서버 OS가 실패할 수 있습니다.
  • 호스트의 인프라가 실패할 수 있습니다.

이 문제들은 일시적일 수 있습니다. 이러한 문제들을 완벽하게 피할 순 없지만, 문제를 예측하고 적절한 조치를 취하여 애플리케이션의 복원력을 높일 수 있습니다.

에러를 보여주는 것은 최후의 수단입니다

우리는 모두 애플리케이션에서 에러를 경험했습니다. 그 경험의 일부는 유용합니다.

"해당 파일이 이미 존재합니다. 덮어쓰시겠습니까?"

아래와 같은 에러는 그렇지 않습니다.

"에러 5969"

사용자에게 에러를 표시하는 것은, 다른 모든 옵션을 모두 적용해본 후에 사용할 마지막 수단이어야 합니다.

이미지 로드 실패같이 덜 중요한 에러는 무시할 수 있습니다. 무시하기 어렵다면 에러를 개선하거나 복구하는 동작도 가능합니다. 예를 들어 네트워크 장애로 인해 서버에 데이터를 저장할 수 없다면 indexedDB 또는 로컬스토리지에 임시로 저장하고 몇 분 후에 다시 시도할 수 있습니다. 반복된 저장에 실패하고 유저가 데이터를 잃을 위험이 있는 경우에만 경고를 표시해야 합니다. 그런 경우에도, 유저가 적절한 조치를 취할 수 있는지 확인해야 합니다. 네트워크는 다시 연결할 수 있어도 서버가 다운된 경우라면 도움이 되지 않습니다.

자바스크립트의 에러 핸들링

자바스크립트에서 에러 핸들링은 간단하지만 종종 미스테리에 싸여 있고 비동기 코드를 고려할 때 복잡해질 수 있습니다.

”error"는 예외를 발생시키기 위해 던질(throw) 수 있는 객체입니다 — 에러가 제대로 감지가 되지 않고 적절하게 처리되지 않는다면 프로그램이 중단될 수 있습니다. 생성자에게 선택적으로 메시지 전달하여 에러 객체를 만들 수 있습니다:

const e = new Error('An error has occurred');

new 없이 함수처럼 Error를 사용할 수도 있습니다 — 여전히 위와 동일하게 Error 객체를 반환합니다.

const e = Error('An error has occurred');

파일 이름과 줄 번호를 두 번째와 세 번째 매개변수로 전달할 수 있습니다.

const e = new Error("An error has occurred", "script.js", 99);

위 구문은 현재 파일에서 Error 객체를 만든 행이 기본값이기 때문에 대부분의 경우엔 필요하지 않습니다.

생성된 Error 객체에는 읽고 쓸 수 있는 다음과 같은 속성이 있습니다.

  • .name: 에러 유형의 이름 (발생한 "Error" )
  • .message: 에러 메시지

다음 읽기/쓰기 속성은 파이어폭스에서도 지원됩니다.

  • .fileName: 에러가 발생한 파일
  • .lineNumber: 에러가 발생한 줄 번호
  • .columnNumber: 에러가 발생한 줄의 열 번호
  • .stack: 스택 추적 — 에러에 도달하기 위해 호출된 함수 목록

에러 유형

자바스크립트는 일반적인 에러 뿐 아니라 다음과 같은 특정한 에러 유형들도 지원합니다.

자바스크립트 인터프리터는 필요에 따라 적절한 에러를 발생시킵니다. 대부분의 경우에 사용자 코드에서 Error 또는 TypeError를 사용할 수 있습니다.

예외 발생시키기

Error 객체를 생성하는 것으로는 아무런 작업도 수행되지 않습니다. 예외를 발생시키려면 Errorthrow해야 합니다.

throw new Error('An error has occurred');

아래 sum() 함수는 두 인수 중 하나가 숫자가 아닌 경우 TypeError를 발생시킵니다. return은 실행되지 않습니다.

function sum(a, b) {
  if (isNaN(a) || isNaN(b)) {
    throw new TypeError('Value is not a number.');
  }
  return a + b;
}

Error 객체를 던지는 것이 실용적이지만 다음과 같이 값이나 객체를 사용할 수도 있습니다.

throw 'Error string';
throw 42;
throw true;
throw { message: 'An error', name: 'CustomError' };

예외를 던지면 catch문에서 잡히지 않는 한, 호출 스택이 버블업 됩니다.
잡히지 않은 예외는 결국 스택의 맨 위에 도달해 프로그램이 중지되고, 개발자 도구의 콘솔에 오류가 표시됩니다.

Uncaught TypeError: Value is not a number.
  sum https://mysite.com/js/index.js:3

예외 잡기

try ... catch 블록에서 블록에서 예외를 잡을 수 있습니다.

try {
  console.log( sum(1, 'a') );
}
catch (err) {
  console.error( err.message );
}

위 코드는 try {} 블록 코드 부분을 실행하지만, 예외가 발생하면 catch {} 블록이 throw에서 반환된 객체를 받습니다.

catch 블록은 에러를 분석하고 그에 따라 대응할 수 있습니다.

try {
  console.log( sum(1, 'a') );
}
catch (err) {
  if (err instanceof TypeError) {
    console.error( 'wrong type' );
  }
  else {
    console.error( err.message );
  }
}

try 또는 catch 코드가 실행되는지와 관계없이 반드시 실행되야 하는 코드가 있는 경우, 선택적 finally {} 블록을 정의할 수 있습니다. 이것은 정리(cleaning up)할 때 유용할 수 있습니다. 예를 들어 Node.js나 Deno에서 데이터베이스 연결을 닫는 경우가 있습니다:

try {
  console.log( sum(1, 'a') );
}
catch (err) {
  console.error( err.message );
}
finally {
  // 아래 구문은 항상 실행됩니다.
  console.log( 'program has ended' );
}

try 블록에는 catch 블록, finally 블록 또는 둘 다 필요합니다.

finally 블록에 반환값이 포함된 경우 해당 값은 trycatch 블록의 반환 문에 관계없이 전체 try ... catch ... finally에 대한 반환 값이 됩니다.

오픈 소스 세션 재생

OpenReplay는 사용자가 웹 앱에서 수행하는 작업을 볼 수 있는 오픈 소스 세션 재생 모음으로, 문제를 더 빨리 해결할 수 있도록 도와줍니다. OpenReplay는 데이터를 완전히 제어하기 위해 자체 호스팅됩니다.

디버깅 경험을 즐기세요 - 무료로 OpenReplay를 사용하세요.

중첩된 try ... catch 블록 및 에러 다시 던지기

예외는 스택에 버블업되고 가장 가까운 catch 블록에 의해 단 한 번만 포착됩니다.

try {
  try {
    console.log( sum(1, 'a') );
  }
  catch (err) {
    console.error('This error will trigger', err.message);
  }
}
catch (err) {
  console.error('This error will not trigger', err.message);
}

모든 catch 블록은 외부 catch에 의해 잡히는 새로운 throw할 수 있습니다. 새로운 에러를 생성할 때 생성자의 cause 속성으로 첫번째 Error 객체를 전달함으로서, 첫번째 Error를 새로운 Error로 전달 할 수 있습니다. 이를 통해 에러를 연속적으로 발생시키고 검사할 수 있습니다.

이 예에서는 첫 번째 에러가 두 번째 에러를 발생시키기 때문에 두 개의 catch 블록이 모두 실행됩니다.

try {
  try {
    console.log( sum(1, 'a') );
  }
  catch (err) {
    console.error('First error caught', err.message);
    throw new Error('Second error', { cause: err });
  }
}
catch (err) {
  console.error('Second error caught', err.message);
  console.error('Error cause:', err.cause.message);
}

비동기 함수에서 예외 발생

try ... catch 블록이 실행된 후에 예외를 발생시키기 때문에, 비동기 함수에서 발생시킨(throw) 예외는 잡을 수가 없습니다. 아래 로직은 실패할 것입니다.

function wait(delay = 1000) {
  setTimeout(() => {
    throw new Error('I am never caught!');
  }, delay);
}
try {
  wait();
} catch (err) {
  // 아래 구문은 절대 실행되지 않습니다.
  console.error('caught!', err.message);
}

1초가 지나면 콘솔에 다음과 같이 표시됩니다:

Uncaught Error: I am never caught!
  wait http://localhost:8888/:14

콜백을 사용하는 경우, Node.js와 같은 프레임워크 및 런타임에서 가정하는 규칙은 에러를 해당 함수의 첫 번째 매개변수로 반환하는 것입니다. 필요한 경우 수동으로 이 작업을 수행할 수 있지만 예외가 발생하지는 않습니다.

function wait(delay = 1000, callback) {
  setTimeout(() => {
    callback('I am caught!');
  }, delay);
}
wait(1000, (err) => {
  if (err) {
    throw new Error(err);
  }
});

최신 ES6에서는 비동기 함수를 정의할 때 Promise를 반환하는 것이 더 나은 경우가 많습니다. 에러가 발생하면 Promise의 reject 함수는 새로운 Error 객체를 반환할 수 있습니다 (어떤 값이나 객체도 가능하지만):

function wait(delay = 1000) {
  return new Promise((resolve, reject) => {
    if (isNaN(delay) || delay < 0) {
      reject(new TypeError('Invalid delay'));
    }
    else {
      setTimeout(() => {
        resolve(`waited ${ delay } ms`);
      }, delay);
    }
  })
}

Promise.catch() 함수는 잘못된 delay 매개변수를 전달할 때 실행되므로 반환된 Error 객체에 대응할 수 있습니다.

// 아래 구문은 실패하므로 catch 블록이 실행됩니다.
wait('x')
  .then( res => console.log( res ))
  .catch( err => console.error( err.message ))
  .finally(() => console.log('done'));

Promise를 반환하는 모든 함수는 await 문을 사용하여 async 함수에서 호출할 수 있습니다. 위의 .then/.catch Promise 예제와 동일하게 실행되지만 조금 더 쉽게 읽으려면 try ... catch 블록에 이를 포함할 수 있습니다.

// 즉시 호출되는 (비동기) 함수 표현식
(async () => {
  try {
    console.log( await wait('x') );
  }
  catch (err) {
    console.error( err.message );
  }
  finally {
    console.log('done');
  }
})();

에러는 피할 수 없습니다

자바스크립트에서는 에러를 만들고 예외를 발생시키는 것이 쉽습니다. 하지만 에러에 적절하게 대응하고 복구가 빠른 애플리케이션을 구축하는 것은 다소 어렵습니다!
드릴 수 있는 최선의 조언은 예상할 수 없는 일을 예상하고 가능한 한 빨리 에러를 처리하라는 것입니다.

profile
Software Developer

3개의 댓글

comment-user-thumbnail
2022년 6월 14일

좋은 글 감사합니다

답글 달기
comment-user-thumbnail
2022년 6월 14일

async function asyncErrorHandle (promise) {
try { return [await promise, null ] }
catch(e) { return [null, e || 'error'] }
}

const [ result, error ] = await asyncErrorHandle(asyncFn());
if(error) // do your magic

답글 달기
comment-user-thumbnail
2022년 6월 20일

I see the best blissful on your blog and http://monikaroy.in I very much love figuring out them.

답글 달기