[TypeScript] Discriminated Union (차별화된 유니온 타입)

Chan의 기술 블로그·2025년 9월 18일

TypeScript

목록 보기
1/10

이 글은 Chat GPT로 TypeScript를 공부하며 정리한 글입니다.

Discriminated Union으로 안전한 상태 관리하기

Discriminated Union이란?

TypeScript의 유니온 타입(Union Type) 은 여러 타입 중 하나를 가질 수 있는 타입이다.

이때 Discriminated Union(차별화된 유니온 타입) 을 사용하면,
모든 타입에 공통된 식별 필드(discriminant) 를 두고
그 값의 literal 타입(예: "loading" | "success" | "error")을 기준으로 안전하게 타입을 좁힐 수 있다.

그러나 유니온 내의 각 타입이 서로 완전히 다른 구조를 가지면
“현재 어떤 타입이 선택됐는지”를 좁히기(narrowing) 어려울 수 있다.

비동기 데이터 상태 관리 예시

비동기 요청의 상태를 타입으로 표현해보자.

type FetchState<T> =
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string };

interface Post {
  id: number;
  title: string;
  body: string;
}

type PostState = FetchState<Post[]>;

FetchState<T>는 다음 세 가지 상태를 표현한다

상태설명
"loading"데이터를 불러오는 중
"success"요청 성공, data 보유
"error"요청 실패, error 메시지 보유

여기서 <T>는 제네릭으로, 상태에 담길 데이터 타입을 외부에서 지정할 수 있다.

타입 좁히기 (Type Narrowing)

아래처럼 status 기반 조건문을 작성하면,
TypeScript는 control-flow analysis로 자동 narrowing 을 수행한다.

function render(state: FetchState<Post[]>) {
  if (state.status === "loading") {
    return "로딩중...";
  }

  if (state.status === "error") {
    return `에러 발생: ${state.error}`;
  }

  if (state.status === "success") {
    return state.data.map(p => p.title).join(", ");
  }
}

TypeScript는 각 분기에서 다음과 같이 타입을 자동 추론한다.

status 값자동 추론 타입
"loading"{ status: "loading" }
"error"{ status: "error"; error: string }
"success"{ status: "success"; data: T }

이 구조 덕분에 error"error" 상태에서만, data"success" 상태에서만 접근 가능해진다.
즉, 타입 안정성(type safety) 이 확실하게 보장된다.

핵심 요약

개념설명
Discriminated Union공통된 literal 필드(status)로 타입을 구분
Type Narrowing조건문을 통해 타입을 안전하게 좁히는 과정
장점타입 안전성, 명확한 로직, 런타임 에러 감소

실무에서 자주 쓰이는 예시

비동기 상태 외에도 다음 상황에서 자주 활용된다.

  • 폼 검증 결과 타입
  • 서버 응답 분기
  • 복잡한 상태 머신(XState 등)
  • Redux / Zustand 구조적 상태 정의
type ValidationResult =
  | { valid: true; value: string }
  | { valid: false; message: string };

function validate(input: string): ValidationResult {
  if (input.trim() === "") {
    return { valid: false, message: "값이 비어 있습니다." };
  }
  return { valid: true, value: input };
}

여기서 valid 필드가 구분자(discriminant) 역할을 한다.


⚛️ React에서 Discriminated Union 활용하기

프론트엔드 개발에서는 비동기 요청 상태가 자주 등장한다.

  • 데이터를 불러오는 중 (loading)
  • 요청 성공 (success)
  • 요청 실패 (error)

이를 Discriminated Union으로 표현하면
React 컴포넌트의 상태 관리가 훨씬 더 안전하고 명확해진다.

1️⃣ 상태 타입 정의

type FetchState<T> =
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string };

interface Post {
  id: number;
  title: string;
  body: string;
}

2️⃣ useState로 상태 관리

import { useEffect, useState } from "react";

export default function PostList() {
  const [state, setState] = useState<FetchState<Post[]>>({ status: "loading" });

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then(res => {
        if (!res.ok) throw new Error("요청 실패");
        return res.json();
      })
      .then(data => setState({ status: "success", data }))
      .catch(err => setState({ status: "error", error: err.message }));
  }, []);

3️⃣ 상태에 따라 안전하게 렌더링

  if (state.status === "loading") {
    return <p>로딩 중...</p>;
  }

  if (state.status === "error") {
    return <p>에러 발생: {state.error}</p>;
  }

  if (state.status === "success") {
    return (
      <ul>
        {state.data.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    );
  }

  return null; // 현재 유니온 구조에서는 사실상 도달되지 않음
}

장점

장점설명
타입 안정성상태에 따라 TypeScript가 타입을 정확하게 좁힘
오타 미검출 방지"sucess" 같은 오타는 literal type 덕에 즉시 오류
명확한 로직로딩/성공/실패 상태가 구조적으로 분리됨
확장성"idle", "refetching" 등 상태 추가가 쉬움

정리

Discriminated Union은
비동기 상태 관리, 폼 검증, 서버 응답 처리 등
“조건 분기 기반 로직”을 타입 안전하게 만들 때 가장 강력한 기법 중 하나다.

React 뿐 아니라 Zustand, Redux Toolkit, TanStack Query 같은 상태 관리 라이브러리에서도
상태 객체를 구조적으로 설계할 때 매우 유용하다.

profile
퍼블리셔에서 프론트앤드로 전향하기

0개의 댓글