TypeScript 프로젝트에서 상수 집합을 관리할 때 enum, const enum, 객체 리터럴 중 어떤 방식을 선택해야 할지 고민하게 됩니다. 각 방식은 컴파일 결과, 타입 안전성, 런타임 동작에서 뚜렷한 차이를 보이며, 잘못된 선택은 번들 크기 증가나 모듈 경계 문제를 초래할 수 있습니다.
이 글에서는 세 가지 방식의 컴파일 동작을 비교하고, 프로젝트 상황에 맞는 최적의 선택 기준을 제시합니다.
TypeScript 컴파일러는 enum을 런타임에 존재하는 JavaScript 객체로 변환합니다. 이 객체는 키에서 값으로, 값에서 키로의 양방향 조회를 지원하는 특수한 구조를 가집니다.
enum Direction {
Up,
Down,
Left,
Right,
}위 코드는 컴파일 후 다음과 같은 JavaScript 객체로 변환됩니다:
var Direction;
(function (Direction) {
Direction[(Direction['Up'] = 0)] = 'Up';
Direction[(Direction['Down'] = 1)] = 'Down';
Direction[(Direction['Left'] = 2)] = 'Left';
Direction[(Direction['Right'] = 3)] = 'Right';
})(Direction || (Direction = {}));양방향 매핑 덕분에 키와 값 모두로 조회가 가능합니다:
console.log(Direction.Up); // 0
console.log(Direction[0]); // "Up"장점:
단점:
const enum은 컴파일 시점에 모든 참조를 상수 값으로 치환합니다. 런타임에는 어떠한 객체도 생성되지 않습니다.
const enum Direction {
Up,
Down,
Left,
Right,
}
const direction = Direction.Up;컴파일 결과는 다음과 같이 값만 남습니다:
const direction = 0; /* Up */모듈 경계 문제:
const enum은 같은 파일 내에서만 안전하게 동작합니다. 외부 모듈에서 import하면 타입 정보는 유지되지만 컴파일러가 값을 인라인할 수 없어 런타임 에러가 발생할 수 있습니다.
// constants.ts
export const enum Status {
Active = 'active',
Inactive = 'inactive'
}
// app.ts
import { Status } from './constants';
console.log(Status.Active); // 컴파일 에러 또는 undefinedisolatedModules 설정과의 충돌:
tsconfig.json에서 isolatedModules: true로 설정하면(Babel, esbuild 사용 시 권장) const enum은 경고를 발생시킵니다. 각 파일이 독립적으로 컴파일되는 환경에서는 cross-module inlining이 불가능하기 때문입니다.
디버깅의 어려움:
런타임에 숫자 상수로만 보이므로 값의 의미를 파악하기 어렵습니다:
// 디버거에서 보이는 내용
direction = 0 // 이게 Up인지 Down인지 알 수 없음| 특징 | Enum | Const Enum |
|---|---|---|
| 컴파일 결과 | JavaScript 객체 생성 | 값으로 인라인 치환 |
| 번들 크기 | 증가 (IIFE 코드 포함) | 최소화 |
| 런타임 접근 | 가능 | 불가능 |
| 양방향 매핑 | 지원 | 지원 안 함 |
| 모듈 간 사용 | 안전 | 위험 (에러 가능) |
| 디버깅 | 쉬움 (키 이름 확인) | 어려움 (숫자만 보임) |
| isolatedModules | 호환 | 경고 발생 |
TypeScript 3.4에서 도입된 as const assertion을 활용하면 enum의 장점을 취하면서도 단점을 제거할 수 있습니다.
const Direction = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
type Direction = (typeof Direction)[keyof typeof Direction];객체 리터럴은 단순한 객체로 컴파일되며, enum보다 훨씬 간결합니다:
// 컴파일 결과
const Direction = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
};IIFE나 양방향 매핑 코드가 생성되지 않아 번들 크기가 작습니다.
as const를 사용하면 각 속성이 리터럴 타입으로 추론됩니다:
const Direction = {
Up: 0,
Down: 1,
} as const;
// Direction.Up의 타입은 0 (number가 아닌 리터럴 타입 0)
// Direction.Down의 타입은 1
type Direction = (typeof Direction)[keyof typeof Direction];
// type Direction = 0 | 1
function move(direction: Direction) {
// 타입 안전한 함수
}
move(Direction.Up); // [권장] 통과
move(0); // [권장] 통과
move(2); // [주의] 타입 에러| 장점 | 설명 |
|---|---|
| 모듈 안전성 | import/export로 안전하게 공유 가능 |
| 타입 안전성 | 리터럴 타입으로 엄격한 타입 체크 |
| 번들 최적화 | 단순 객체로 컴파일되어 크기 최소화 |
| 런타임 접근 | 객체로 존재하여 순회/조회 가능 |
| 확장성 | 메타데이터나 메서드 추가 용이 |
| 호환성 | isolatedModules와 충돌 없음 |
| 디버깅 | 의미 있는 키 이름으로 확인 가능 |
런타임 순회가 필요한 경우:
const HttpStatus = {
OK: 200,
Created: 201,
BadRequest: 400,
Unauthorized: 401,
NotFound: 404,
} as const;
type HttpStatus = (typeof HttpStatus)[keyof typeof HttpStatus];
// 모든 성공 응답 코드 확인
const successCodes = Object.values(HttpStatus).filter(code => code < 300);
// [200, 201]
// 상태 코드에서 이름 찾기
function getStatusName(code: HttpStatus): string {
const entry = Object.entries(HttpStatus).find(([_, value]) => value === code);
return entry ? entry[0] : 'Unknown';
}
console.log(getStatusName(200)); // "OK"메타데이터 확장:
const LogLevel = {
Debug: { value: 0, color: 'gray' },
Info: { value: 1, color: 'blue' },
Warn: { value: 2, color: 'yellow' },
Error: { value: 3, color: 'red' },
} as const;
type LogLevelKey = keyof typeof LogLevel;
type LogLevelValue = (typeof LogLevel)[LogLevelKey]['value'];
function log(level: LogLevelKey, message: string) {
const { value, color } = LogLevel[level];
console.log(`[${level}:${value}] ${message}`, `color: ${color}`);
}
log('Error', 'Something went wrong'); // [Error:3] Something went wrong| 특징 | Enum | Const Enum | 객체 리터럴 (as const) |
|---|---|---|---|
| 컴파일 결과 | IIFE 객체 | 인라인 치환 | 단순 객체 |
| 번들 크기 | 큼 | 최소 | 작음 |
| 런타임 접근 | [지원] 가능 | [미지원] 불가능 | [지원] 가능 |
| 양방향 매핑 | [지원] 지원 | [미지원] 불가능 | [부분] 수동 구현 가능 |
| 모듈 간 사용 | [지원] 안전 | [미지원] 위험 | [지원] 안전 |
| isolatedModules | [지원] 호환 | [미지원] 경고 | [지원] 호환 |
| 타입 안전성 | [지원] 강함 | [지원] 강함 | [지원] 강함 |
| 확장성 | [부분] 제한적 | [미지원] 불가능 | [지원] 자유로움 |
| 디버깅 | [지원] 쉬움 | [미지원] 어려움 | [지원] 쉬움 |
| 추천도 (2024+) | [부분] 제한적 | [미지원] 권장 안 함 | [지원] 권장 |
// [권장] 양방향 매핑이 필요한 경우
enum Color {
Red,
Green,
Blue
}
function getColorName(value: number): string {
return Color[value]; // 값으로 이름 조회
}추천 시나리오:
// [참고] 단일 파일 내에서만 사용
const enum InternalFlag {
None = 0,
Active = 1 << 0,
Pending = 1 << 1,
}
// 같은 파일 내에서만 사용
const flags = InternalFlag.Active | InternalFlag.Pending;추천 시나리오:
// [권장] 대부분의 경우 권장되는 현대적 패턴
export const ApiEndpoint = {
Users: '/api/users',
Posts: '/api/posts',
Comments: '/api/comments',
} as const;
export type ApiEndpoint = (typeof ApiEndpoint)[keyof typeof ApiEndpoint];추천 시나리오:
// Before
enum Status {
Active = 'active',
Inactive = 'inactive',
}
// After
const Status = {
Active: 'active',
Inactive: 'inactive',
} as const;
type Status = (typeof Status)[keyof typeof Status];// Before
export const enum Priority {
Low = 0,
Medium = 1,
High = 2,
}
// After
export const Priority = {
Low: 0,
Medium: 1,
High: 2,
} as const;
export type Priority = (typeof Priority)[keyof typeof Priority];TypeScript에서 상수 집합을 관리하는 방식은 프로젝트의 특성에 따라 선택해야 합니다. 2024년 현재, 대부분의 경우 객체 리터럴과 as const를 활용하는 방식이 가장 권장됩니다.
핵심 권장사항:
as const (모듈 안전성, 확장성, 호환성)프로젝트의 요구사항과 팀의 코딩 스타일을 고려하여 일관된 방식을 선택하고, 코드베이스 전체에 적용하는 것이 중요합니다. 특히 라이브러리를 개발하는 경우, 외부 모듈에서 안전하게 사용할 수 있는 객체 리터럴 방식을 강력히 권장합니다.
TypeScript의 고급 타입 시스템과 제네릭을 활용한 확장 가능한 팩토리 패턴 설계 가이드
Infer 키워드를 활용한 정교한 타입 추론과 실전 활용 패턴