← 전체 글로 돌아가기

TypeScript

TypeScript의 union type으로 상태를 더 명확히 표현하기

상태를 제대로 표현하면 런타임 버그를 많이 줄일 수 있고, 코드를 읽는 사람도 의도를 쉽게 이해한다.

상태 관리는 모든 웹 애플리케이션의 핵심이다. 로딩, 성공, 실패 같은 상태를 어떻게 표현하는지에 따라 코드의 복잡도가 달라진다.

일반적인 상태 표현의 문제점

// ❌ 이런 식으로 여러 boolean을 사용하면
type DataState = {
  isLoading: boolean;
  isError: boolean;
  isSuccess: boolean;
  data?: T;
  error?: string;
};

// 문제: isLoading과 isError가 동시에 true일 수 있다
// 이 상태가 유효한가? 코드를 읽는 사람이 혼란해진다

Union type으로 상태를 명확히 하기

// ✅ 각 상태가 상호 배타적임이 명확하다
type DataState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

이 방식의 장점:

  1. 한 번에 하나의 상태만 가능: success일 때는 반드시 data가 있다
  2. 타입 안정성: TypeScript가 각 상태에서 사용 가능한 속성을 자동으로 제한한다
  3. 코드 가독성: 상태의 의도가 명확하다

패턴 매칭으로 상태 처리하기

function handleState<T>(state: DataState<T>) {
  switch (state.status) {
    case 'idle':
      return <div>로드 대기 중...</div>;

    case 'loading':
      return <div>로딩 중...</div>;

    case 'success':
      // 여기서 state.data에 접근할 수 있다
      // TypeScript가 data의 존재를 보장한다
      return <div>{state.data.name}</div>;

    case 'error':
      // 마찬가지로 state.error가 있음이 보장된다
      return <div>에러: {state.error}</div>;
  }
}

React에서 사용하기

function UserProfile() {
  const [state, setState] = useState<DataState<User>>({ status: 'idle' });

  useEffect(() => {
    setState({ status: 'loading' });
    fetchUser()
      .then(data => setState({ status: 'success', data }))
      .catch(error => setState({ status: 'error', error: error.message }));
  }, []);

  return handleState(state);
}

이벤트 흐름 추적하기

Union type을 사용하면 상태 전이가 명확해진다.

idle → loading → success (또는 error)

이 흐름에서 벗어나는 상태 전이는 타입 체크 단계에서 발견된다.

실수를 줄이기 위한 체크리스트

  • 모든 가능한 상태를 나열했는가?
  • 각 상태가 상호 배타적인가?
  • 상태마다 필요한 데이터를 포함했는가?
  • 모든 상태를 처리하는 코드를 작성했는가? (switch문이 완전한가?)

TypeScript의 union type을 제대로 사용하면, 런타임에 일어날 수 있는 많은 버그를 컴파일 타임에 잡을 수 있다. 상태를 표현하는 방식 하나가 코드의 품질을 크게 좌우한다.