CTRL K
좋은 타입은 선언 수가 아니라 경계에서 나온다: enum 남용에서 벗어나기
TypeScript를 도입하면 흔히 이런 흐름이 생깁니다.
- 공통 enum 파일을 만든다
- 여러 계층(UI/API/도메인)에서 같이 쓴다
- 처음엔 편한데, 시간이 갈수록 변경 영향이 커진다
문제는 타입이 많아서가 아니라, 타입이 있어야 할 경계에 있지 않기 때문입니다.
enum 남용이 왜 위험할까?
enum 자체가 문제는 아닙니다. 문제는 “전역 공용 enum”이 너무 많은 책임을 떠안는 순간 시작돼요.
예시:
- API 응답 상태
- UI 표시 상태
- 도메인 비즈니스 상태
이 3가지를 같은 enum으로 묶으면, 한 군데 변경이 전체 레이어에 전파됩니다.
결과:
- 관련 없는 파일까지 수정
- 테스트 범위 확대
- 사이드 이펙트 증가
경계를 기준으로 타입 나누기
실무에서는 타입을 “기술”이 아니라 “책임” 기준으로 나누는 게 안전합니다.
1) API 모델 (외부 계약)
- 백엔드 계약에 맞춘 형태
- 변경 가능성이 높고, 방어 코드 필요
2) 도메인 모델 (비즈니스 의미)
- 제품 규칙이 담긴 내부 모델
- UI와 API 사이의 중간 언어
3) UI 모델 (표현/상호작용)
- 컴포넌트 렌더링에 최적화된 형태
- 로딩/에러/선택 상태 등 화면 관점
핵심은 “같은 값”이라도 레이어가 다르면 타입도 분리한다는 점입니다.
실전 리팩터링 패턴
before: 공용 enum 하나로 모든 곳에서 사용
export enum Status {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
PENDING = 'PENDING',
}after: 레이어별 타입 분리 + 매핑 함수
// api/types.ts
export type ApiUserStatus = 'ACTIVE' | 'INACTIVE' | 'PENDING'
// domain/user.ts
export type UserStatus = 'enabled' | 'disabled' | 'waiting'
// ui/user.ts
export type UserBadgeState = 'positive' | 'neutral' | 'warning'
export function mapApiStatusToDomain(status: ApiUserStatus): UserStatus {
if (status === 'ACTIVE') return 'enabled'
if (status === 'INACTIVE') return 'disabled'
return 'waiting'
}매핑 계층이 생기면 처음엔 코드가 늘어나는 것처럼 보이지만, 장기적으로는 변경 영향이 명확해지고 회귀 범위가 줄어듭니다.
면접에서 말할 포인트
- “타입 안정성은 선언 개수보다 경계 설계가 중요하다고 봅니다.”
- “API/도메인/UI 모델을 분리해 변경 전파 범위를 줄였습니다.”
- “공용 enum 남용 대신 매핑 계층을 도입해 사이드 이펙트를 통제했습니다.”
이 답변은 단순 문법 지식보다 아키텍처 레벨 사고를 보여줍니다.
적용 체크리스트
- 공용 enum이 3개 이상 레이어에서 재사용되고 있는가?
- API 응답 타입이 UI 컴포넌트까지 직접 들어오는가?
- 타입 변경 시 수정 파일 수가 비정상적으로 큰가?
- 매핑 계층(anti-corruption layer)이 존재하는가?
2개 이상 Yes면 리팩터링 후보입니다.
포트폴리오에 어떻게 남길까?
이 주제는 “보여주기 어려운 리팩터링”이라서, 문서화 방식이 중요합니다.
추천 구성:
- 문제 상황(전역 enum 결합)
- 분리 전략(API/도메인/UI)
- 매핑 함수 도입 이유
- 변경 영향 축소 사례(정성/정량)
이렇게 정리하면 코드가 화려하지 않아도 설계 역량을 충분히 전달할 수 있습니다.
마무리
TypeScript에서 좋은 설계는 “타입을 많이 만드는 것”이 아니라 타입을 올바른 경계에 두는 것에서 시작합니다.
enum은 도구일 뿐이고, 진짜 품질은 경계와 의존 방향에서 결정됩니다.
이 관점이 잡히면 프론트엔드 코드의 변경 비용과 리뷰 난이도가 확실히 줄어듭니다.