main

프론트에서 에러핸들링하기

Articles
10

최근에 원티드 로그인 구현 세션을 듣고 있는데, 로그인 구현을 하다보니 fetchaxios를 사용하면서 프론트에서는 어떻게 에러핸들링을 하는게 좋은 방법일까? 생각이 들어 포스팅을 하게 되었습니다.
오늘 포스팅은 다양한 에러 핸들링과 서버와 통신할 때에도 어떻게 에러핸들링을 해야하는지에 대해서 이야기하고자 합니다.


자바스크립트를 공부하다보면, 예외처리를 해주어야한다는 이야기를 많이 들었었는데요,
이때말하는 예외처리란 실행중인 프로그램에서 예기치 못한 상황이 발생해서 더이상 진행할 수 없는 상황을 말합니다.
즉, 에러 상황이라고 볼 수 있습니다. 이런 상황들에 대한 예비 장치 코드를 마련해놔야 정말 잘하는 개발자라는 이야기를 많이 들었던 것 같습니다.😅

에러의 종류

에러는 컴파일 에러와 런타임 에러로 나눌 수 있습니다. 컴파일 에러는 코드를 컴파일 하는 동안 발생하는 오류입니다. 코드가 실행되기 이전에 발견합니다. 반대로 런타임 에러는 프로그램에서 이미 실행중인 상태에서 문제가 발생하는 것을 말합니다. 코드가 실행중에 에러가 발생하게 됩니다.

주로 자바스크립트에서 에러를 이야기한다면 런타임 에러를 말하게 되는데요, 자바스크립트는 인터프리터 언어이기 때문입니다. 타입스크립트의 tsc를 사용하게 되면 javascript로 컴파일도 해주지만, 컴파일 에러도 잡아주게 됩니다.
개인적인 의견이지만, 타입스크립트를 사용해서 컴파일 에러도 잡고 에러핸들링도 해주면 정말 좋지 않을까? 란 생각입니다.


자바스크립트의 에러

예외로 인해 발생한 Error 객체를 핸들링하는 것을 에러 핸들링이라고 합니다. 에러 객체는 여러 가지가 있는데요,
자바스크립트에서 에러는 TypeError, SyntaxError, EvalError, InternalError 등 크게 몇 가지의 타입으로 나뉩니다.

TypeError (타입에러)

타입에러는 코드에서 기대한 값이 변수에 들어있지 않을 때, 즉 기대했던 타입의 값이 아닐 때 발생하는 오류 입니다.

null.appendChild();
// Uncaught TypeError: Cannot read properties of null (reading 'appendChild')
[1,2,3].go();
// Uncaught TypeError: [1,2,3].go is not a function

SyntaxError (문법 오류)

오타가 있거나, 쉼표, 따옴표, 괄호 같은 기호 사용에 실수가 있을 때 나오는 에러 메세지입니다.

conts name = "front-end";
// Uncaught SyntaxError: Unexpected identifier 'name'

ReferenceError (참조 오류)

ReferenceError은 현재 범위에서 존재하지 않거나 초기화되지 않은 변수를 참조했을 때 발생한다.

"" + helloworld;
// Uncaught ReferenceError: helloworld is not defined

에러핸들링

그렇다면 에러 핸들링은 왜 해야할까요?
에러 핸들링을 하지 않는다면 프로그램이 갑자기 멈추게 되거나 원치 않는 상황이 발생할 수 있습니다. 그렇다보면 사용자에게 좋지 않은 경험을 제공할 수 있습니다. 에러 핸들링을 하지 않고 에러가 발생하게 되었다면, 에러가 발생한 부분의 아래 코드는 실행되지 않습니다. 따라서 에러 핸들링은 필수적이라고 할 수 있습니다.

function addClass(els, className){
  els.forEach(el => el.classList.add(className));
}
 
//const els = document.querySelectorAll("li");
addClass(document.body, "opacity100")

위 코드는 querySelector로 DOM을 선택하고 투명도를 클래스를 부여하여 설정하는 코드입니다.
하지만, addClass의 매개변수인 els가 배열이 아니라면 forEach는 실행되지 않고 타입에러가 발생하게 됩니다.
개발자 입장에서는 친절하지 않은 에러 메시지를 받게 되므로 이는 좋은 코드라고 할 수 없습니다.

function addClass(els, className){
  if(!Array.isArray(els)) throw new Error("addClass함수는 배열을 받습니다.");
  els.forEach(el => el.classList.add(className));
}
 
//const els = document.querySelectorAll("li");
addClass(document.body, "opacity100")

그럼 위와 같이 throw Error를 사용해 에러를 던지게 되면 어떤 에러인지 설명을 볼 수 있고 확인할 수 있게 됩니다.
따라서 에러의 원인을 알기 위해서는 에러를 잘 던져주는 것이 중요합니다.


또 다른 코드 예시를 살펴볼게요.

function addClass(els, className){
  if(!Array.isArray(els)) throw new Error("addClass함수는 배열을 받습니다.");
  els.forEach(el => el.classList.add(className));
}
 
function bar(el){
  addClass(el, "daeun");
}
 
function foo(){
  const el =  document.querySelector("li");
  bar(el);
}
 
foo();

위 코드는 foo -> bar -> addClass 순으로 함수가 실행되게 됩니다. 이렇게 순차적으로 함수가 실행되지만 addClass에서 에러를 마주하게 되는데요. 이 경우, addClass에서 실행이 멈추게 되고 그 아래 코드는 실행되지 않고 중단되게 됩니다. 즉, 코드 진행이 되지 않게됩니다.
에러의 원인은 아는데, 프로그램의 흐름이 끊기는 건 우리가 원하는 방향이 아닙니다. 따라서 끊기지 않기위해서 필요한 것이 에러핸들링이라고 볼 수 있습니다.

function addClass(els, className){
  if(!Array.isArray(els)) throw new Error("addClass함수는 배열을 받습니다."); // 에러가 던져집니다.
  els.forEach(el => el.classList.add(className));
}
 
function bar(el){
  addClass(el, "daeun");
}
 
function foo(){
  try{
    const el =  document.querySelector("li");
    bar(el);
  }
  catch(e) {
    console.error("에러가 잡혔습니다.", e);  // 에러는 여기로 들어옵니다.
  }
}
 
foo();
console.log("이후에도 코드가 실행됩니다.")
 
 
// 에러가 잡혔습니다. Error: addClass함수는 배열을 받습니다.
//    at addClass (<anonymous>:2:33)
//    at bar (<anonymous>:7:3)
//    at foo (<anonymous>:13:5)
//    at <anonymous>:20:1
// 이후에도 코드가 실행됩니다.

위와 같이 코드를 수정하게 되면 catch문 안에서 에러를 잡게 됩니다. 또한 try, catch문으로 에러를 핸들링 하게 되면 이후에 흐름이 끊기지 않고 콘솔도 찍히게 됩니다.


다음은 비동기 관련 코드를 작성할 때 코드 예시로 들어보겠습니다.

async function bar(url){
  const res = await fetch(url);
  if(!res.ok) throw new Error(`ok가 아닙니다. 상태코드는 ${res.status}`); // 에러 던지고
  const data = await res.json();
  return data;
}
 
async function foo(){
  try{
    const result = await bar("https://httpstat.us/500");
    return result;
  } catch(e){ // 에러 받고
    console.log("여기서 에러가 잡혔습니다. 통신에 문제가 있습니다.", e);
  }
}
 
foo();
 
// 여기서 에러가 잡혔습니다. 통신에 문제가 있습니다. TypeError: Failed to fetch
//    at bar (<anonymous>:2:21)
//    at foo (<anonymous>:10:26)
//    at <anonymous>:17:1

fetch문을 쓸 때는 함수 내에서 데이터를 불러오고, 에러가 발생했을 시 에러를 던지고, 데이터를 불러오는 함수 내에서 try catch문으로 에러를 잡는 것이 좋습니다.

즉, 쉽게 말하면 에러를 던지는 로직에러를 잡는 로직을 나뉘어 작성하는 것이 좋습니다.

Reference Doc

아무도 안 알려주지만 제일 중요한 그것 : 에러 핸들링


김다은 이모지
Daeun Kim
Junior Frontend Engineer