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 };
이 방식의 장점:
- 한 번에 하나의 상태만 가능:
success일 때는 반드시data가 있다 - 타입 안정성: TypeScript가 각 상태에서 사용 가능한 속성을 자동으로 제한한다
- 코드 가독성: 상태의 의도가 명확하다
패턴 매칭으로 상태 처리하기
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을 제대로 사용하면, 런타임에 일어날 수 있는 많은 버그를 컴파일 타임에 잡을 수 있다. 상태를 표현하는 방식 하나가 코드의 품질을 크게 좌우한다.