---
id: wiki-2026-0508-api-응답-및-상태-모델링-state-modeling-a
title: API Response & State Modeling
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [State Modeling, Discriminated Union, Result type, Tagged Union, exhaustive check]
duplicate_of: none
source_trust_level: B
confidence_score: 0.9
verification_status: applied
tags: [typescript, api-design, state-modeling, discriminated-union, result-type, exhaustive-check]
raw_sources: []
last_reinforced: 2026-05-09
github_commit: pending
tech_stack:
  language: TypeScript
  framework: TypeScript / Zod / TS-Result
---

# API Response & State Modeling

## 📌 한 줄 통찰
> **"매 invalid state 의 unrepresentable"**. Discriminated union + Result type. 매 compile-time exhaustive check. 매 runtime bug 의 prevent.

## 📖 핵심

### 매 problem (without modeling)
```ts
// ❌ Optional everything
type Response = {
  data?: User;
  error?: string;
  loading?: boolean;
};

// 매 combination 의 가능?
// loading=true, data=set ?
// error=set, data=set ?
// 매 inconsistent state.
```

### Solution: Discriminated Union (Tagged Union)
```ts
type Response =
  | { type: 'idle' }
  | { type: 'loading' }
  | { type: 'success'; data: User }
  | { type: 'error'; message: string };

// 매 state 의 explicit. 매 invalid 의 impossible.
```

### Result type
```ts
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

// Usage
async function fetchUser(id: string): Promise<Result<User>> {
  try {
    const user = await api.users.get(id);
    return { ok: true, value: user };
  } catch (e) {
    return { ok: false, error: e as Error };
  }
}

// Caller
const result = await fetchUser('123');
if (result.ok) {
  console.log(result.value.name);   // type: User
} else {
  console.error(result.error);
}
```

### Exhaustive check
```ts
function render(state: Response) {
  switch (state.type) {
    case 'idle': return <Idle />;
    case 'loading': return <Spinner />;
    case 'success': return <Profile user={state.data} />;
    case 'error': return <Error msg={state.message} />;
    default:
      const _: never = state;   // compile error if state added
      throw new Error('unreachable');
  }
}
```

→ 매 새 state 의 추가 시 compiler 의 exhaustive check.

### 매 application

#### React state
```tsx
const [state, setState] = useState<Response>({ type: 'idle' });

// 매 fetching
setState({ type: 'loading' });

const result = await fetchUser(id);
if (result.ok) {
  setState({ type: 'success', data: result.value });
} else {
  setState({ type: 'error', message: result.error.message });
}
```

#### TanStack Query (built-in)
```tsx
const { data, error, isLoading, isSuccess } = useQuery({...});

if (isLoading) return <Spinner />;
if (error) return <Error msg={error.message} />;
return <Profile user={data} />;
```

→ 매 query state 의 already discriminated.

#### State machine (XState)
```ts
import { createMachine } from 'xstate';

const machine = createMachine({
  id: 'fetch',
  initial: 'idle',
  states: {
    idle: { on: { FETCH: 'loading' } },
    loading: { on: { SUCCESS: 'success', ERROR: 'error' } },
    success: { on: { REFETCH: 'loading' } },
    error: { on: { RETRY: 'loading' } },
  },
});
```

### 매 API response shape

#### Success / Error envelope
```ts
type APIResponse<T> = 
  | { status: 'ok'; data: T }
  | { status: 'error'; code: string; message: string };
```

#### Pagination
```ts
type Paginated<T> = {
  data: T[];
  meta: { total: number; page: number; perPage: number };
};
```

#### Nullable vs Optional
```ts
// Nullable: 명시적 null
type User = { name: string; bio: string | null };

// Optional: 매 absent
type Update = { name?: string; bio?: string };
```

→ 매 different semantic.

### Validation (runtime, Zod)
```ts
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().min(0),
});

type User = z.infer<typeof UserSchema>;

// 매 API response 의 parse + validate
const result = UserSchema.safeParse(await response.json());
if (result.success) {
  // result.data: User
} else {
  console.error(result.error);
}
```

→ 매 type + runtime 의 둘 다.

### Branded types (extra safety)
```ts
type UserId = string & { __brand: 'UserId' };
type OrderId = string & { __brand: 'OrderId' };

function getUser(id: UserId) { ... }

const id = '123' as UserId;
getUser(id);          // OK
getUser('123');       // ❌ string ≠ UserId
```

## 💻 Code Pattern

### Result + chain
```ts
class Result<T, E> {
  constructor(
    public readonly ok: boolean,
    public readonly value?: T,
    public readonly error?: E
  ) {}
  
  map<U>(fn: (v: T) => U): Result<U, E> {
    return this.ok ? Result.ok(fn(this.value!)) : Result.err(this.error!);
  }
  
  flatMap<U>(fn: (v: T) => Result<U, E>): Result<U, E> {
    return this.ok ? fn(this.value!) : Result.err(this.error!);
  }
  
  static ok<T, E>(value: T): Result<T, E> { return new Result(true, value); }
  static err<T, E>(error: E): Result<T, E> { return new Result(false, undefined, error); }
}

// Usage
const result = await fetchUser('123');
const name = result.map(u => u.name).map(s => s.toUpperCase());
```

### Effect-TS (advanced FP)
```ts
import { Effect, pipe } from 'effect';

const program = pipe(
  Effect.tryPromise(() => api.users.get('123')),
  Effect.map(u => u.name),
  Effect.catchAll(e => Effect.succeed('Unknown')),
);

const result = await Effect.runPromise(program);
```

### React hook with state
```tsx
function useFetchUser(id: string) {
  const [state, setState] = useState<{
    status: 'idle' | 'loading' | 'success' | 'error';
    data?: User;
    error?: Error;
  }>({ status: 'idle' });
  
  useEffect(() => {
    setState({ status: 'loading' });
    fetchUser(id)
      .then(data => setState({ status: 'success', data }))
      .catch(error => setState({ status: 'error', error }));
  }, [id]);
  
  return state;
}
```

## 🤔 결정 기준

| 상황 | 추천 |
|---|---|
| Simple state | Discriminated union |
| Async result | Result type |
| Complex state | XState machine |
| API response | Envelope + Zod validation |
| Type identity | Branded type |
| FP heavy | Effect-TS |

**기본값**: Discriminated union + Result type + Zod validation.

## 🔗 Graph
- 부모: [[TypeScript]] · [[State-Management]] · [[API-Design]]
- 변형: [[Discriminated-Union]] · [[Tagged-Union]] · [[Result-Type]]
- 응용: [[XState]] · [[Effect-TS]] · [[Zod-Validation]]
- Adjacent: [[Branded-Types]] · [[Exhaustive-Check]] · [[TanStack-Query]]

## 🤖 LLM 활용
**언제**: 매 TypeScript app 의 state design. 매 API contract 의 type-safe.
**언제 X**: 매 simple primitive (boolean enough). 매 prototype.

## ❌ 안티패턴
- **Optional everything**: invalid state.
- **`any` for response**: type 의 가치 X.
- **No exhaustive check**: 매 새 state 의 missed.
- **Throw + catch 만**: Result type 의 더 explicit.
- **No runtime validation**: 매 wrong API response 의 silent.

## 🧪 검증 / 중복
- Verified (TypeScript handbook, neverthrow library docs).
- 신뢰도 B.
- Related: [[Tagged_Union_Discriminated_Types]] · [[TS_Schema_Validation_Comparison]].

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-09 | Manual cleanup — discriminated union + Result + state machine + code |
