Skip to main content

Command Palette

Search for a command to run...

타입스크립트에서 에러 핸들링을 개선하기

Published
타입스크립트에서 에러 핸들링을 개선하기

원문: Alain Perkaz, "Improving error handling in TypeScript"

완벽한 격리 상태에서 동작하는 프로그램은 거의 없습니다. 개발자가 언제나 완벽한 코드를 작성해도 데이터베이스, REST API, 심지어 다수의 별을 받은 최신 유행 npm 패키지와 같은 외부 컴포넌트와 코드가 상호 작용할 때면 에러를 마주할 가능성이 높습니다!

서버가 뻗는 동안 방관하는 대신 책임감 있는 개발자는 방어적으로 생각하며 잘못된 형식의 요청이 들어올 때를 대비합니다. 이 글에서는 타입스크립트의 가장 일반적인 에러 핸들링 접근법 몇 가지를 다룹니다. 각 메서드의 사용법을 배우고 어떻게 개선될 수 있는지를 살펴봅니다. 마지막으로 에러를 관리하는 보다 깔끔한 방법을 제시합니다.

시작해 보겠습니다!

비고: 타입스크립트에 익숙하지 않아도 괜찮습니다. 에러를 일으키는 조건과 해결책은 자바스크립트에도 적용되니까요.

인기 있는 타입스크립트 에러 핸들링 접근법

자세히 살펴보기 전에 아래 소개하는 접근법이 결코 완전하지 않다는 것을 명심하세요. 여기 제시된 에러와 해결책은 저의 주관적인 경험에 근거하기 때문에 여러분 개개인의 결과는 다를 수 있습니다. 🏎️

null 반환하기

null 반환은 코드에서 무언가 문제가 발생했다는 것을 나타내는 흔한 방법입니다. null 반환은 함수가 실패할 수 있는 방법이 오직 하나뿐일 때 가장 적합하지만 일부 개발자는 함수에 여러 에러가 있는 경우에도 이 방법을 사용합니다.

null을 반환하면 코드 어디에서나 null 확인이 강제되어 에러의 원인에 관한 구체적인 정보가 손실됩니다. null 반환은 에러를 임의적으로 표현하므로 0, -1, 또는 false를 반환하려 해도 결국 동일한 결과를 얻게 됩니다.

아래 코드 블록에서 특정 도시의 온도 및 습도에 관한 데이터를 검색하는 함수 하나를 작성해 보겠습니다. 함수 getWeather는 두 함수 externalTemperatureAPIexternalHumidityAPI를 통하여 두 개의 외부 API와 상호 작용하고 결과를 종합합니다.

const getWeather = async (city: string): Promise<{ temp: number; humidity: number } | null> => {
  const temp = await externalTemperatureAPI(city);
  if (!temp) {
    console.log(`Error fetching temperature for ${city}`);
    return null;
  }
  const humidity = await externalHumidityAPI(city);
  if (!humidity) {
    console.log(`Error fetching humidity for ${city}`);
    return null;
  }
  return { temp, humidity };
};

const weather = await getWeather('Berlin');
if (weather === null) console.log('getWeather() failed');

Berlin을 입력하면 Error fetching temperature for ${city}Error fetching humidity for ${city} 에러 메시지를 받게 됩니다.

두 외부 API 함수 모두 실패할 수 있기 때문에 getWeather는 두 함수 모두에서 null을 강제로 확인합니다. null 확인은 에러를 아예 처리하지 않는 것보다는 낫지만 이는 호출자가 어쩔 수 없이 몇몇 추측을 하도록 만듭니다. 새로운 에러를 지원하도록 함수를 확장한다면 호출자는 함수 내부를 확인하지 않고는 해당 에러를 알 수 없습니다.

온도 API가 내부 서버 에러를 나타내는 HTTP Code 500을 반환하면 externalTemperatureAPI가 처음 null을 던진다고 가정해 봅시다. 함수를 확장하여 API 응답의 구조와 타입 유효성을 확인한다면 (즉, 해당 응답이 type number인지를 확인한다면) 호출자는 함수가 null을 반환한 이유가 HTTP Code 500 때문인지 아니면 예상치 못한 API 응답 구조 때문인지 알 수 없습니다.

try...catch를 사용한 커스텀 에러 던지기

커스텀 에러를 생성하여 던지는 것은 null 반환보다 더 나은 선택지입니다. 왜냐하면 함수가 서로 다른 에러를 던지고 함수의 호출자가 구분된 에러를 별도로 처리할 수 있도록 에러 세분화를 달성할 수 있기 때문입니다.

그러나 에러를 던지는 모든 함수는 중단되고 에러를 상위로 전파하며 코드의 정상적인 흐름을 방해합니다. 이게 작은 규모의 애플리케이션에서는 큰 문제가 아닌 듯 보일 수 있지만 코드가 계속해서 try...catch를 쌓아 올리면 가독성과 전반적인 성능이 저하될 것입니다.

날씨 예제에서 발생하는 에러를 try...catch 메서드로 해결해 봅시다.

const getWeather = async (city: string): Promise<{ temp: number; humidity: number }> => {
  try {
    const temp = await externalTemperatureAPI(city);
    try {
      const humidity = await externalHumidityAPI(city);
    } catch (error) {
      console.log(`Error fetching humidity for ${city}`);
      return new Error(`Error fetching humidity for ${city}`);
    }
    return { temp, humidity };
  } catch (error) {
    console.log(`Error fetching temperature for ${city}`);
    return new Error(`Error fetching temperature for ${city}`);
  }
};

try {
  const weather = await getWeather('Berlin');
} catch (error) {
  console.log('getWeather() failed');
}

위 코드 블록에서 externalTemperatureAPIexternalHumidityAPI에 접근하려고 시도하면 console.log에 두 개의 에러가 표시된 후 중단되고 여러 번 상위로 전파됩니다.

클래스 Result

위에서 논의한 두 가지 에러 핸들링 접근법 중 어느 것을 사용하든지 단순한 실수가 원래 에러 위에 불필요한 복잡성을 추가할 수 있습니다. null을 반환하고 try...catch를 던질 때 발생하는 문제는 코틀린(Kotlin), 러스트(Rust), 씨샵(C#)과 같은 다른 프런트엔드 언어에서도 흔하며 이 세 언어는 클래스 Result를 꽤 일반화된 해결책으로 사용합니다.

실행의 성공·실패 여부와 상관없이 클래스 Result는 주어진 함수의 결과를 캡슐화하여 함수 호출자가 에러를 예외가 아닌 정상적인 실행 흐름의 일부로 처리할 수 있게 합니다.

타입스크립트와 함께 사용될 때 클래스 Result는 함수의 결과로 반환되는 가능한 에러에 관하여 타입 안정성과 상세한 정보를 제공합니다. 함수의 에러 결과를 수정할 때 클래스 Result는 코드베이스의 영향을 받는 지점에서 컴파일 타임 에러를 제공합니다.

날씨 예제로 다시 돌아가 봅시다. 러스트의 ResultOption 객체의 타입스크립트 구현인 ts-results를 사용하겠습니다.

NeverThrow 같이 매우 유사한 API를 가진 다른 타입스크립트 패키지들도 있으니 자유롭게 실험해 보세요.

import { Ok, Err, Result } from 'ts-results';

type Errors = 'CANT_FETCH_TEMPERATURE' | 'CANT_FETCH_HUMIDITY';

const getWeather = async (city: string): Promise<Result<{ temp: number; humidity: number }, Errors>> => {
  const temp = await externalTemperatureAPI(city);
  if (!temp) return Err('CANT_FETCH_TEMPERATURE');

  const humidity = await externalHumidityAPI(city);
  if (!humidity) return Err('CANT_FETCH_HUMIDITY');

  return Ok({ temp, humidity });
};

const weatherResult = await getWeather('Berlin'); // `weatherResult`는 타입이 완전히 보장됩니다

if (weatherResult.err) console.log(`getWeather() failed: ${weatherResult.val}`);
if (weatherResult.ok) console.log(`Weather is: ${JSON.stringify(weather.val)}`);

함수에 타입이 안정된 결과를 추가하고 코드에서 에러 핸들링을 우선시하는 버전은 이전 예제보다 개선되었지만 아직 할 일이 남아 있습니다. 타입 확인을 완전하게 만드는 방법을 연구해 보겠습니다.

클래스 Result를 선호한다고 try...catch 구조를 사용하지 않는다는 의미가 아닙니다. try...catch 구조는 외부 패키지와 함께 작업할 때 여전히 필요합니다.

클래스 Result를 따르는 게 합당하다고 생각한다면 해당 접점을 모듈에 캡슐화하고 내부에서 클래스 Result를 사용해 볼 수 있습니다.

완전한 타입 확인을 추가하기

여러 에러를 반환하는 함수를 넘길 때 모든 에러 케이스를 다루는 타입 확인을 제공하면 도움이 될 수 있습니다. 이를 통하여 함수 호출자가 동적으로 에러 타입에 반응할 수 있으며 어떤 에러 케이스도 간과될 수 없다고 확신할 수 있습니다.

완전한 switch 문으로 이러한 확신을 달성할 수 있습니다.

// 완전한 switch 문 헬퍼
class UnreachableCaseError extends Error {
  constructor(val: never) {
    super(`Unreachable case: ${val}`);
  }
}

// ...

const weatherResult = getWeather('Berlin');
if (weatherResult.err) {
  // 에러 처리하기
  const errValue = weatherResult.val;
  switch (errValue) {
    case 'CANT_FETCH_TEMPERATURE':
      console.error('getWeather() failed with: CANT_FETCH_TEMPERATURE');
      break;
    case 'CANT_FETCH_HUMIDITY':
      console.error('getWeather() failed with: CANT_FETCH_HUMIDITY');
      break;
    default:
      // 👇 모든 에러를 잡기 위한 런타임 타입 확인
      throw new UnreachableCaseError(errValue);
  }
}

날씨 예제를 완전한 switch 문으로 실행하면 두 가지 상황에서 컴파일 타임 에러가 발생할 것입니다. 하나는 모든 에러 케이스가 처리되지 않은 경우고 다른 하나는 원본 함수의 에러가 변경된 경우입니다.

요약

이제 여러분은 타입스크립트에서 일반적인 에러를 처리하기 위한 개선된 해결책을 알게 되었습니다! 에러 핸들링이 얼마나 중요한지 알기에 이 방법을 사용하여 앱에서 발생하는 모든 에러에 관하여 가장 구체적인 정보를 얻을 수 있기를 바랍니다.

이 튜토리얼에서는 null 반환과 try...catch 방법과 같은 일부 일반적인 접근법의 단점을 다루었습니다. 끝으로 타입스크립트의 클래스 Result와 에러를 잡기 위한 완전한 switch 문을 함께 사용하는 방법을 배웠습니다.

More from this blog

나의 오픈 소스 시작 이야기

원문: TkDoDo, “My Open Source Origin Story“ 가끔씩 제가 받는 질문이 하나 있는데, 바로 오픈 소스와 리액트 쿼리(React Query)를 어떻게 시작하게 되었는지입니다. 저의 기본 원칙은 어떤 질문을 세 번 받으면 더 이상 답변할 필요가 없도록 질문에 대해 글로 쓴다는 것입니다. 하지만 이 질문은 주로 직접 만났을 때 받는 질문이라 글로 작성할 생각을 한 적이 없었습니다. 최근에 오프라인 컨퍼런스에 더 많이 참...

Jul 30, 2025
나의 오픈 소스 시작 이야기

이더넷이란?

원문: baeldung, “What Is Ethernet?“ 1. 소개 이 튜토리얼에서는 이더넷(Ethernet)과 이를 통해 이루어지는 데이터 전송에 대해 알아보겠습니다. 2. 이더넷이란? 이더넷은 근거리 통신망(LAN) 또는 광역 네트워크(WAN) 내에서 장치들이 데이터를 주고받고 통신하기 쉽게 만들어 주는 널리 사용되는 기술입니다. 컴퓨터, 프린터, 서버는 물론 스마트 홈 기기까지도 이더넷으로 연결됩니다. 가정이나 사무실처럼 제한된 공간...

Jul 20, 2025
이더넷이란?

포스트 개발자 시대

원문: Josh W. Comeau, "The Post-Developer Era" 2년 전 2023년 3월, "프런트엔드 개발의 종말"이라는 제목의 블로그 글을 발행했습니다. 이는 OpenAI가 GPT-4 쇼케이스를 발표한 직후였고, 당시 업계 분위기는 머지않아 인간 소프트웨어 개발자는 필요 없어지고 앞으로는 소프트웨어 개발을 AI가 전담하게 될 것이라는 전망이 지배적이었습니다. 저는 이런 주장에 회의적이었고 그 블로그 글에서 소프트웨어 개발...

Jul 10, 2025
포스트 개발자 시대

널리 사용되는 네트워크 프로토콜

원문: Subham Datta, "Popular Network Protocols" 1. 개요 이 튜토리얼에서는 가장 널리 사용되고 인기 있는 네트워크 프로토콜들을 소개합니다. 2. 네트워크 프로토콜 소개 의사소통과 정보 교환은 현대 사회에서 가장 중요하고 강력한 역량입니다. 컴퓨터 네트워킹이란 여러 대의 컴퓨터와 장치를 케이블이나 위성을 통해 서로 연결하여, 거리와 상관없이 정보·자원·데이터베이스 등을 공유할 수 있게 하는 것을 말합니다. 네...

Jun 20, 2025
널리 사용되는 네트워크 프로토콜

커맨드 라인에 편해지는 법

원문: Julia Evans, "What helps people get comfortable on the command line?" 가끔 커맨드 라인을 써야 하는 친구들과 이야기하다 보면 많은 이들이 여전히 터미널을 두려워하고 있다는 걸 느낍니다. 그럴 때마다 어떤 조언을 할지 잘 모르겠더라고요. 저는 워낙 오래전부터 터미널을 써왔기 때문이죠. 그래서 Mastodon에 이렇게 물어봤습니다. 최근 1~3년 사이에 터미널 공포(?)를 극복한 분...

Jun 10, 2025
커맨드 라인에 편해지는 법
C

CodeSnap

84 posts

한국어로 전달하는 웹 개발 번역 매거진