한국어

강력하고 타입-세이프한 상태 머신을 구축하는 강력한 도구인 TypeScript 구별된 유니온을 탐색해 보세요. 상태 정의, 전환 처리, 그리고 코드 신뢰성 향상을 위한 TypeScript의 타입 시스템 활용법을 배워보세요.

TypeScript 구별된 유니온: 타입-세이프 상태 머신 구축하기

소프트웨어 개발 영역에서 애플리케이션 상태를 효과적으로 관리하는 것은 매우 중요합니다. 상태 머신은 복잡한 상태 기반 시스템을 모델링하기 위한 강력한 추상화를 제공하여, 예측 가능한 동작을 보장하고 시스템 로직에 대한 추론을 단순화합니다. TypeScript는 강력한 타입 시스템을 통해 구별된 유니온(태그된 유니온 또는 대수적 데이터 타입이라고도 함)을 사용하여 타입-세이프한 상태 머신을 구축하는 환상적인 메커니즘을 제공합니다.

구별된 유니온(Discriminated Unions)이란 무엇인가요?

구별된 유니온은 여러 다른 타입 중 하나가 될 수 있는 값을 나타내는 타입입니다. 유니온의 멤버로 알려진 각 타입들은 구별자(discriminant) 또는 태그(tag)라고 불리는 공통적이고 고유한 속성을 공유합니다. 이 구별자는 TypeScript가 현재 유니온의 어떤 멤버가 활성화되어 있는지 정확하게 파악할 수 있게 하여, 강력한 타입 검사와 자동 완성을 가능하게 합니다.

신호등을 생각해보세요. 신호등은 빨강, 노랑, 초록 세 가지 상태 중 하나일 수 있습니다. '색상' 속성이 구별자 역할을 하여 신호등이 정확히 어떤 상태에 있는지 알려줍니다.

상태 머신에 구별된 유니온을 사용하는 이유는 무엇인가요?

구별된 유니온은 TypeScript에서 상태 머신을 구축할 때 몇 가지 주요 이점을 제공합니다:

구별된 유니온으로 상태 머신 정의하기

주문 처리 시스템이라는 실용적인 예제를 통해 구별된 유니온을 사용하여 상태 머신을 정의하는 방법을 설명하겠습니다. 주문은 대기 중(Pending), 처리 중(Processing), 배송 중(Shipped), 배송 완료(Delivered)와 같은 상태를 가질 수 있습니다.

1단계: 상태 타입 정의하기

먼저, 각 상태에 대한 개별 타입을 정의합니다. 각 타입은 구별자(discriminant) 역할을 하는 `type` 속성과 함께 상태별 데이터를 가질 것입니다.


interface Pending {
  type: "pending";
  orderId: string;
  customerName: string;
  items: string[];
}

interface Processing {
  type: "processing";
  orderId: string;
  assignedAgent: string;
}

interface Shipped {
  type: "shipped";
  orderId: string;
  trackingNumber: string;
}

interface Delivered {
  type: "delivered";
  orderId: string;
  deliveryDate: Date;
}

2단계: 구별된 유니온 타입 생성하기

다음으로, `|` (유니온) 연산자를 사용하여 이러한 개별 타입들을 결합하여 구별된 유니온을 만듭니다.


type OrderState = Pending | Processing | Shipped | Delivered;

이제 `OrderState`는 `Pending`, `Processing`, `Shipped`, 또는 `Delivered` 중 하나가 될 수 있는 값을 나타냅니다. 각 상태 내의 `type` 속성은 구별자 역할을 하여 TypeScript가 이들을 구별할 수 있게 합니다.

상태 전환 처리하기

이제 상태 머신을 정의했으므로, 상태 간 전환을 위한 메커니즘이 필요합니다. 현재 상태와 액션을 입력으로 받아 새로운 상태를 반환하는 `processOrder` 함수를 만들어 보겠습니다.


interface Action {
  type: string;
  payload?: any;
}

function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    case "pending":
      if (action.type === "startProcessing") {
        return {
          type: "processing",
          orderId: state.orderId,
          assignedAgent: action.payload.agentId,
        };
      }
      return state; // 상태 변경 없음

    case "processing":
      if (action.type === "shipOrder") {
        return {
          type: "shipped",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // 상태 변경 없음

    case "shipped":
      if (action.type === "deliverOrder") {
        return {
          type: "delivered",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // 상태 변경 없음

    case "delivered":
      // 주문이 이미 배송 완료됨, 추가 작업 없음
      return state;

    default:
      // 철저성 검사로 인해 이 경우는 발생하지 않아야 함
      return state; // 또는 오류 발생
  }
}

설명

철저성 검사 활용하기

TypeScript의 철저성 검사는 상태 머신의 모든 가능한 상태를 처리하도록 보장하는 강력한 기능입니다. 만약 `OrderState` 유니온에 새로운 상태를 추가하고 `processOrder` 함수를 업데이트하는 것을 잊으면, TypeScript는 오류를 표시할 것입니다.

철저성 검사를 활성화하려면 `never` 타입을 사용할 수 있습니다. switch 문의 `default` 케이스 내부에서 상태를 `never` 타입의 변수에 할당하세요.


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (이전 케이스들) ...

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // 또는 오류 발생
  }
}

`switch` 문이 모든 가능한 `OrderState` 값을 처리하면, `_exhaustiveCheck` 변수는 `never` 타입이 되어 코드가 컴파일됩니다. 그러나 `OrderState` 유니온에 새로운 상태를 추가하고 `switch` 문에서 이를 처리하는 것을 잊으면, `_exhaustiveCheck` 변수는 다른 타입이 되어 TypeScript가 컴파일 타임 오류를 발생시켜 누락된 케이스를 알려줍니다.

실용적인 예제 및 응용 분야

구별된 유니온은 단순한 주문 처리 시스템을 넘어 다양한 시나리오에 적용할 수 있습니다:

예제: UI 상태 관리

API에서 데이터를 가져오는 UI 컴포넌트의 상태를 관리하는 간단한 예제를 살펴보겠습니다. 다음과 같은 상태를 정의할 수 있습니다:


interface Initial {
  type: "initial";
}

interface Loading {
  type: "loading";
}

interface Success {
  type: "success";
  data: T;
}

interface Error {
  type: "error";
  message: string;
}

type UIState = Initial | Loading | Success | Error;

function renderUI(state: UIState): React.ReactNode {
  switch (state.type) {
    case "initial":
      return 

데이터를 로드하려면 버튼을 클릭하세요.

; case "loading": return

로딩 중...

; case "success": return
{JSON.stringify(state.data, null, 2)}
; case "error": return

오류: {state.message}

; default: const _exhaustiveCheck: never = state; return _exhaustiveCheck; } }

이 예제는 구별된 유니온을 사용하여 UI 컴포넌트의 여러 상태를 효과적으로 관리하고, 현재 상태에 따라 UI가 올바르게 렌더링되도록 보장하는 방법을 보여줍니다. `renderUI` 함수는 각 상태를 적절하게 처리하여 UI를 관리하는 명확하고 타입-세이프한 방법을 제공합니다.

구별된 유니온 사용을 위한 모범 사례

TypeScript 프로젝트에서 구별된 유니온을 효과적으로 활용하려면 다음 모범 사례를 고려하세요:

고급 기법

조건부 타입(Conditional Types)

조건부 타입은 구별된 유니온과 결합하여 훨씬 더 강력하고 유연한 상태 머신을 만들 수 있습니다. 예를 들어, 조건부 타입을 사용하여 현재 상태에 따라 함수의 다른 반환 타입을 정의할 수 있습니다.


function getData(state: UIState): T | undefined {
  if (state.type === "success") {
    return state.data;
  }
  return undefined;
}

이 함수는 간단한 `if` 문을 사용하지만, 특정 타입이 항상 반환되도록 보장하기 위해 조건부 타입을 사용하여 더 견고하게 만들 수 있습니다.

유틸리티 타입(Utility Types)

TypeScript의 유틸리티 타입인 `Extract`와 `Omit`은 구별된 유니온으로 작업할 때 유용할 수 있습니다. `Extract`는 조건에 따라 유니온 타입에서 특정 멤버를 추출할 수 있게 하고, `Omit`은 타입에서 속성을 제거할 수 있게 합니다.


// UIState 유니온에서 "success" 상태 추출
type SuccessState = Extract, { type: "success" }>;

// Error 인터페이스에서 'message' 속성 생략
type ErrorWithoutMessage = Omit;

다양한 산업 분야의 실제 사례

구별된 유니온의 힘은 다양한 산업과 애플리케이션 영역으로 확장됩니다:

결론

TypeScript 구별된 유니온은 상태 머신을 구축하는 강력하고 타입-세이프한 방법을 제공합니다. 가능한 상태와 전환을 명확하게 정의함으로써 더 견고하고, 유지보수하기 쉬우며, 이해하기 쉬운 코드를 만들 수 있습니다. 타입 안정성, 철저성 검사, 향상된 코드 완성의 조합은 구별된 유니온을 복잡한 상태 관리를 다루는 모든 TypeScript 개발자에게 귀중한 도구로 만듭니다. 다음 프로젝트에서 구별된 유니온을 채택하고 타입-세이프한 상태 관리의 이점을 직접 경험해 보세요. 전자상거래에서 헬스케어, 물류에서 교육에 이르기까지 다양한 예시에서 보여주었듯이, 구별된 유니온을 통한 타입-세이프 상태 관리 원칙은 보편적으로 적용 가능합니다.

간단한 UI 컴포넌트를 만들든 복잡한 기업용 애플리케이션을 만들든, 구별된 유니온은 상태를 더 효과적으로 관리하고 런타임 오류의 위험을 줄이는 데 도움이 될 수 있습니다. 그러니, TypeScript로 타입-세이프한 상태 머신의 세계를 탐험해 보세요!