타입 시스템을 상태 모델의 계약으로 삼고 런타임 검증을 결합하면 예측 가능하고 디버그 가능한 애플리케이션 상태를 구축할 수 있습니다.
1. Discriminated Union
공통 태그 필드를 기준으로 상태/액션을 구분하면 컴파일 타임에 누락 케이스를 탐지하고 안전한 exhaustive 체크를 수행할 수 있습니다.
// state.ts
type Idle = { tag: "Idle" };
type Loading = { tag: "Loading" };
type Success = { tag: "Success"; data: User[] };
type Failure = { tag: "Failure"; error: string };
export type AsyncState = Idle | Loading | Success | Failure;
export function fold(s: AsyncState) {
switch (s.tag) {
case "Idle": return "...";
case "Loading": return "Loading...";
case "Success": return s.data.length + " users";
case "Failure": return "Error: " + s.error;
default: const _exhaustive: never = s; return _exhaustive;
}
}
2. Reducer 패턴
액션 또한 태그 기반으로 정의해 reducer 내부에서 케이스별 변이를 명확히 표현하고 사이드이펙트는 미들웨어로 격리합니다.
// actions.ts
type Fetch = { type: "Fetch" };
type Resolve = { type: "Resolve"; payload: User[] };
type Reject = { type: "Reject"; payload: string };
export type Action = Fetch | Resolve | Reject;
// reducer.ts
export function reducer(state: AsyncState, action: Action): AsyncState {
switch (action.type) {
case "Fetch": return { tag: "Loading" };
case "Resolve": return { tag: "Success", data: action.payload };
case "Reject": return { tag: "Failure", error: action.payload };
}
}
3. Zod로 런타임 검증
외부 입력은 스키마 기반으로 파싱해 타입 단언을 제거하고 잘못된 형태의 데이터가 상태 그래프에 유입되는 것을 차단합니다.
// schema.ts
import { z } from "zod";
export const UserSchema = z.object({
id: z.string(),
name: z.string(),
});
export const UsersSchema = z.array(UserSchema);
// service.ts
export async function fetchUsers() {
const res = await fetch("/api/users");
const json = await res.json();
return UsersSchema.parse(json);
}
4. 실전 팁
- Reducer는 순수 함수로 유지하고 비동기 처리는 호출부에서 래핑합니다.
- 태그 값은 문자열 상수로 통일하고 생성기는 헬퍼로 노출해 오타를 방지합니다.
- fold 유틸을 제공해 UI 단에서 상태별 분기를 간결하게 표현합니다.
5. 마무리
태그드 유니온과 Zod 검증의 결합은 타입 안전성과 실행 안전성을 동시에 충족하며 유지보수성을 체계적으로 향상시킵니다.