타입 설계 베스트 프랙티스

좋은 타입을 설계하는 핵심 원칙들을 정리한다. 불가능한 상태를 표현 불가능하게 만드는 판별 유니온, 타입은 정확한 만큼만 넓게 잡는 절제, 외부 입력은 경계에서만 검증하는 패턴, 추론을 믿되 공개 API는 명시하는 균형까지, 실전에서 코드를 더 안전하고 읽기 쉽게 만드는 설계 습관을 다룬다.

· 6 min read · PALDYN Team

지난 글에서 컴파일러의 에러 메시지를 빠르게 읽는 법을 다뤘다. 그런데 가장 좋은 에러는 애초에 만들 수 없는 코드를 만들 수 없게 막는 것이다. 타입을 어떻게 설계하느냐에 따라, 어떤 버그는 런타임에 터지는 대신 컴파일조차 되지 않는다. 이번 글은 그동안 배운 도구들을 어떻게 조합해 좋은 타입을 설계할지, 그 핵심 원칙을 정리한다.

불가능한 상태를 표현 불가능하게

타입 설계의 가장 강력한 원칙 하나를 꼽으라면 “make illegal states unrepresentable”이다. 모순된 상태가 애초에 타입으로 표현될 수 없게 만들면, 그 모순을 다루는 방어 코드도, 그걸 깜빡해서 생기는 버그도 사라진다.

불가능한 상태를 표현 불가능하게

데이터 로딩 상태를 불리언 플래그로 표현하면 모순이 새어 든다.

// ❌ loading=true 인데 data 도 error 도 있는 상태가 가능
interface State {
  loading: boolean;
  error?: Error;
  data?: User;
}

이 타입은 loading, error, data의 조합으로 여덟 가지 상태를 표현하지만, 그중 대부분은 무효다. “로딩 중이면서 데이터도 에러도 있는” 상태가 타입상 허용된다. 판별 유니온으로 바꾸면 유효한 상태만 남는다.

// ✅ 각 상태가 자기 데이터만 가진다
type State =
  | { tag: "loading" }
  | { tag: "error"; error: Error }
  | { tag: "ok"; data: User };

이제 ok가 아닌 상태에서 data에 접근하는 코드는 컴파일되지 않는다. 모순은 표현 자체가 불가능해졌다.

네 가지 설계 원칙

좋은 타입 설계는 몇 가지 반복되는 판단으로 요약된다. 도구가 화려한지보다, 이 원칙들을 일관되게 지키는지가 코드의 품질을 가른다.

타입 설계 4원칙

첫째, 정확한 만큼만 넓게 잡는다. 실제로 "asc""desc"만 들어온다면 string이 아니라 "asc" | "desc"로 좁힌다. 타입이 넓을수록 잘못된 값이 끼어들 틈이 생긴다.

// 넓다 — 아무 문자열이나 통과
function sort(order: string) {}

// 좁다 — 두 값만 허용, 자동완성도 된다
function sort(order: "asc" | "desc") {}

둘째, 상태는 유니온으로 모델링한다. 앞서 본 대로 플래그 조합 대신 판별 유니온을 쓴다. 셋째, 경계에서만 검증한다. 외부에서 들어오는 값(API 응답, 폼 입력)은 시스템 입구에서 한 번 좁혀 신뢰할 수 있는 타입으로 만들고, 내부 코드는 그 타입을 믿고 깔끔하게 짠다. 곳곳에서 방어적으로 if (x == null)을 흩뿌리는 대신 경계 한 곳에 검증을 모은다.

추론과 명시의 균형

넷째 원칙은 추론을 믿되 경계는 명시하는 것이다. 모든 변수에 타입을 다는 것은 오히려 노이즈다. 지역 변수는 컴파일러의 추론에 맡기는 편이 코드도 깔끔하고, 구현이 바뀌면 타입도 따라 바뀐다.

// 지역 변수 — 추론에 맡긴다
const total = items.reduce((a, b) => a + b.price, 0);

// 공개 함수 — 시그니처는 명시해 계약을 고정한다
export function calcTotal(items: Item[]): number {
  return items.reduce((a, b) => a + b.price, 0);
}

명시가 가치 있는 곳은 모듈 경계다. export 되는 함수의 매개변수와 반환 타입은 다른 코드와 맺는 계약이므로 분명히 적는다. 그러면 구현을 바꿔도 계약이 깨지면 컴파일러가 즉시 알려 주고, 앞 글에서 본 성능 측면에서도 추론 비용이 함수 밖으로 새지 않는다.

무엇을 기억할까

좋은 타입 설계의 목표는 화려한 타입 묘기가 아니라, 틀린 코드를 작성하기 어렵게 만드는 것이다. 불가능한 상태를 판별 유니온으로 봉쇄하고, 타입은 정확한 만큼만 좁게 잡고, 검증은 경계에 모으고, 추론과 명시는 역할에 맞게 나눈다. 이 원칙들이 몸에 배면, 타입은 귀찮은 의무가 아니라 설계를 또렷하게 비추는 도구가 된다. 다음 글에서는 지금까지 배운 것을 실무에서 바로 꺼내 쓸 수 있는 팁으로 압축해 마무리한다.


지난 글: 자주 만나는 에러 메시지 읽기

다음 글: 이펙티브 타입스크립트: 시리즈 마무리


읽어주셔서 감사합니다. 😊