JavaScript의 에러 처리, 왜 이렇게 어려울까? ("On JavaScript Errors" 요약)

okorion·2025년 7월 16일
post-thumbnail

원문 - On JavaScript Errors

2025년 5월, Hayden Bleasel의 트윗 하나가 JavaScript 커뮤니티에 뜨거운 논쟁을 일으켰다. 그 내용은 단순했다:

“나는 모든 JS 프로젝트에 이 두 파일을 무조건 넣는다. 더 나은 방법이 있을까?”

이 단순한 질문은 많은 개발자들이 느껴온 JS 에러 처리의 불편함과 모순을 다시금 떠올리게 만들었다. 이 글은 그 논의의 핵심과 해결책, 그리고 다양한 접근 방식들을 정리한 것이다.


🚨 에러 처리는 왜 중요한가?

에러 처리는 단순히 예외를 잡는 것이 아니다. 앱이 예기치 않게 깨지는 걸 방지하고:

  • 개발자에겐 디버깅 정보를 제공하고
  • 사용자에겐 친절한 메시지를 보여주는 것

을 포함한다. JS에서는 try...catch, throw, Error 객체를 기본 도구로 제공한다.

try {
  const data = await fetchData();
  renderData(data);
} catch (err) {
  console.error("에러 발생:", err);
  showErrorToUser("데이터를 불러오지 못했습니다.");
}

🧼 버블링: 에러는 어디까지 떠오르는가?

JS의 에러는 호출 스택을 따라 위로 "버블링"된다. 처리되지 않으면 전역에 도달해 앱이 죽거나 콘솔 에러를 발생시킨다.

function C() {
  throw new Error("에러!");
}
function B() { C(); }
function A() { B(); }
A(); // 최상단에서 에러 발생

이 구조는 에러를 고수준에서 처리할 수 있는 유연성을 제공하지만, 중간에 놓치면 치명적이다.


🧩 JavaScript의 에러, 정말 까다로운 이유

  1. 무엇이든 throw 가능throw "문자열", throw { custom: true }, throw undefined 다 허용됨
  2. catch(error)는 항상 unknown
  3. TypeScript도 예외 타입을 추론하지 않음
  4. 라이브러리마다 다른 에러 처리 방식
    • ex: Supabase는 에러를 throw하지 않고 객체에 .error 필드를 넣음
export const parseError = (error: unknown) => {
  if (typeof error === 'string') return error;
  if (error instanceof Error) return error.message;
  return '알 수 없는 에러 발생';
};

✅ 최소한의 패턴: parseError + handleError

// parseError.ts
export const parseError = (error: unknown) => {
  if (typeof error === 'string') return error;
  if (error instanceof Error) return error.message;
  return 'An error occurred';
};

// handleError.ts
import { toast } from 'sonner';
import { parseError } from './parseError';

export const handleError = (title: string, error: unknown) => {
  const description = parseError(error);
  toast.error(title, { description });
};

위 코드 조합은 단순하지만 강력하다:

  • 어떤 에러든 문자열 메시지로 변환
  • UI 알림으로 표시하거나 로깅 등 중앙 처리 가능

🏕️ 커뮤니티의 다양한 접근

1. "그냥 try/catch 쓰자" 캠프

  • 복잡한 추상화보다 JS 내장 도구에 집중
  • Error 확장 객체(AxiosError 등) 활용
  • React의 ErrorBoundary 또는 Express의 에러 미들웨어 활용

장점: 간단, 의존성 없음, 모두가 이해함
단점: 실수에 취약함 (ex. try 누락, 로그 누락 등)


2. neverthrow: Rust 스타일 Result<T, E>

const result = getUserById(id);
if (result.isErr()) handle(result.error);
else render(result.value);

장점:

  • 실패를 값으로 명시
  • 타입 기반 오류 처리 가능
  • 체이닝 API (map, mapErr, match 등)

단점:

  • 패러다임 변화 필요
  • 기존 try/catch 대비 다소 장황
  • 외부 의존성 도입 필요

3. Effect: 완전한 함수형 에러 시스템

Effect는 Effect<E, A>라는 타입으로 오류 가능성을 타입에 반영함. 이는 Haskell이나 Scala의 영향을 받은 타입 기반 오류 추적 시스템이다.

장점:

  • 오류 추론과 강제
  • 동시성 제어, 의존성 주입, 리소스 관리 등 포괄적
  • 대규모 코드베이스에 적합

단점:

  • 러닝커브 매우 높음
  • "타입스크립트 안의 새로운 언어"라는 평가도 있음
  • 전사적 도입 없이는 효과 반감

🤹 왜 이렇게 분분할까?

JavaScript는 본질적으로 유연성 중심 언어:

  • 오류를 자유롭게 던지고 잡을 수 있음
  • 타입스크립트도 예외 타입 추적을 포기함 (throws 제안 무산)

결국 에러 처리는 자율성 + 규율의 균형 문제다. 다양한 해결책이 존재하는 이유도 그 때문이다.


🧾 실전에서의 적용 팁

  1. 기본기부터 철저히
    • 모든 비동기 호출에 .catch 또는 try/catch 작성
    • 적어도 콘솔 로그라도 남기자
  2. 일관된 팀 컨벤션 수립
    • ex: "모든 catch에는 handleError 호출"
    • "절대 string만 throw하지 않기"
  3. 도구를 적절히 활용하자
    • 간단한 유틸 함수 (parseError, handleError)
    • 큰 프로젝트는 neverthrow, Effect 고려

🎯 결론: 완벽한 정답은 없지만, 무대응은 최악이다

에러 처리 방식은 정답이 없다. 프로젝트 규모, 팀 문화, 코드 스타일에 따라 적절한 선택지가 다를 수 있다. 다만 가장 나쁜 것은 아무 것도 하지 않는 것이다.

에러는 언제든지 발생한다. 중요한 건 그걸 어떻게 다룰지에 대한 전략이 있는가다.

throw new Error('읽어주셔서 감사합니다!');
profile
okorion's Tech Study Blog.

0개의 댓글