2.4 열거형(Enum)
Enum(열거형)은 관련된 상수 집합에 의미 있는 이름을 붙이는 방법이다. 예를 들어 방향을 0, 1, 2, 3 대신 Direction.Up, Direction.Down, Direction.Left, Direction.Right로 표현하면 코드의 가독성과 안전성이 크게 높아진다. TypeScript는 숫자 enum, 문자열 enum, const enum, 이종(heterogeneous) enum을 지원하며, 각각 사용 목적과 컴파일 결과가 다르다.
그러나 enum은 JavaScript에 존재하지 않는 TypeScript 고유 구문이라 런타임 객체를 생성하고 트리 쉐이킹이 어렵다는 단점도 있다. 이 장에서는 enum의 모든 형태를 이해하고, as const 객체 패턴이라는 현대적 대안까지 함께 다룬다.
숫자 Enum
선언할 때 값을 지정하지 않으면 첫 번째 멤버부터 0으로 시작해 1씩 자동 증가한다.
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
const move: Direction = Direction.Up;
console.log(move); // 0
console.log(Direction[0]); // "Up" — 역방향 매핑
console.log(Direction["Up"]); // 0
초기값 지정
특정 멤버에 초기값을 지정하면 이후 멤버들은 그 값부터 1씩 증가한다.
enum HttpStatus {
// 2xx 성공
OK = 200,
Created, // 201
Accepted, // 202
NoContent = 204,
// 4xx 클라이언트 오류
BadRequest = 400,
Unauthorized, // 401
Forbidden, // 403 — 이렇게 되면 잘못된 값! 402를 건너뜀
NotFound = 404,
// 5xx 서버 오류
InternalServerError = 500,
BadGateway = 502,
ServiceUnavailable = 503,
}
console.log(HttpStatus.OK); // 200
console.log(HttpStatus.Created); // 201
console.log(HttpStatus.NotFound); // 404
역방향 매핑 (Reverse Mapping)
숫자 enum은 컴파일 후 양방향 매핑 객체가 생성된다. 이름으로 값을 얻을 수도, 값으로 이름을 얻을 수도 있다.
enum Status {
Pending = 1,
Active, // 2
Inactive, // 3
}
console.log(Status.Active); // 2 — 이름 → 값
console.log(Status[2]); // "Active" — 값 → 이름 (역방향 매핑)
// 컴파일된 JavaScript (설명용)
// var Status;
// (function (Status) {
// Status[Status["Pending"] = 1] = "Pending";
// Status[Status["Active"] = 2] = "Active";
// Status[Status["Inactive"] = 3] = "Inactive";
// })(Status || (Status = {}));
역방향 매핑은 디버깅 시 숫자 코드를 사람이 읽을 수 있는 이름으로 변환할 때 유용하다.
function describeStatus(code: number): string {
const name = Status[code];
return name ?? `알 수 없는 상태 (${code})`;
}
console.log(describeStatus(2)); // "Active"
console.log(describeStatus(99)); // "알 수 없는 상태 (99)"
문자열 Enum
각 멤버를 명시적인 문자열 값으로 초기화한다. 자동 증가 기능이 없으므로 모든 멤버에 값을 직접 지정해야 한다.
enum HttpMethod {
Get = "GET",
Post = "POST",
Put = "PUT",
Patch = "PATCH",
Delete = "DELETE",
}
const method: HttpMethod = HttpMethod.Post;
console.log(method); // "POST" — 사람이 읽을 수 있는 값
문자열 enum의 장점
- 직렬화 안전: JSON으로 변환하거나 API에 전달할 때 의미 있는 문자열이 그대로 사용된다.
- 디버깅 가독성: 숫자
2대신"POST"가 로그에 찍히므로 즉시 의미를 알 수 있다. - 역방향 매핑 없음: 문자열 enum은 역방향 매핑이 생성되지 않아 런타임 객체가 작다.
enum LogLevel {
Debug = "DEBUG",
Info = "INFO",
Warn = "WARN",
Error = "ERROR",
Fatal = "FATAL",
}
function log(level: LogLevel, message: string): void {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] ${message}`);
// [2026-03-21T00:00:00.000Z] [WARN] 디스크 사용량이 높습니다
}
log(LogLevel.Warn, "디스크 사용량이 높습니다");
const Enum — 컴파일 후 인라인 치환
일반 enum은 컴파일 후 JavaScript 객체로 남아 런타임에 존재한다. const enum은 컴파일 시점에 모든 멤버 참조를 해당 값으로 인라인 치환하고 객체 자체는 제거한다. 번들 크기를 줄이고 런타임 접근 비용을 없애는 최적화 기법이다.
const enum Color {
Red = 0,
Green = 1,
Blue = 2,
}
// 사용 코드
const myColor = Color.Green;
const isRed = myColor === Color.Red;
컴파일된 JavaScript:
// Color 객체 자체는 생성되지 않음
const myColor = 1 /* Color.Green */;
const isRed = myColor === 0 /* Color.Red */;
const enum의 제약
const enum Permission {
Read = 1,
Write = 2,
Execute = 4,
}
// 동적 접근 불가 — 객체가 없으므로
// Permission[1]; // 오류: A const enum member can only be accessed using a string literal
// 배열 인덱스로 사용 — OK (인라인 치환됨)
const perms: number[] = [Permission.Read, Permission.Write];
// isolatedModules: true 환경(Babel, esbuild 등)에서는 const enum 사용 불가
// 각 파일을 독립적으로 컴파일하는 도구는 const enum 값을 다른 파일에서 참조할 수 없다
이종(Heterogeneous) Enum
숫자 값과 문자열 값을 혼합한 enum이다. 실무에서 거의 사용되지 않으며, 권장하지 않는다.
enum Mixed {
No = 0,
Yes = "YES",
}
// 사용 가능하지만 혼란스럽고 유지보수가 어렵다
// 대부분의 경우 숫자 enum이나 문자열 enum 중 하나를 선택하는 것이 낫다
Enum의 단점
1. 트리 쉐이킹 불가
모던 번들러(Webpack, Rollup, esbuild)는 사용되지 않는 코드를 제거하는 트리 쉐이킹을 수행한다. 그러나 일반 enum은 즉시 실행 함수(IIFE)로 컴파일되어 번들러가 사용 여부를 정적으로 분석하기 어렵다. 따라서 enum이 포함된 모듈은 통째로 번들에 포함될 수 있다.
// 컴파일된 enum — IIFE 패턴으로 번들러가 분석하기 어렵다
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 = {}));
2. 숫자 enum의 타입 안전성 취약
숫자 enum은 범위 밖의 숫자도 할당을 허용한다.
enum Direction { Up = 0, Down = 1, Left = 2, Right = 3 }
function move(dir: Direction): void {
console.log(dir);
}
move(Direction.Up); // OK
move(99); // OK — 오류가 나야 하지만 TypeScript가 허용한다!
// 숫자는 모두 Direction에 할당 가능하다 (설계상의 결함)
문자열 enum은 이 문제가 없다.
enum HttpMethod { Get = "GET", Post = "POST" }
function request(method: HttpMethod): void {}
request(HttpMethod.Get); // OK
request("GET"); // 오류: Argument of type '"GET"' is not assignable to parameter of type 'HttpMethod'
3. JavaScript 생태계와의 이질감
Enum은 TypeScript 고유 구문이다. TypeScript를 JavaScript로 단순히 타입을 제거하는 방식으로 변환하는 도구(예: @babel/plugin-transform-typescript, esbuild의 기본 모드)에서 const enum은 동작하지 않는다.
as const 객체 — enum의 현대적 대안
as const 객체 패턴은 런타임 일반 JavaScript 객체처럼 동작하면서도 타입 수준에서 enum과 동등한 안전성을 제공한다. 트리 쉐이킹도 잘 동작하고 isolatedModules 환경에서도 문제없다.
// 기존 enum 방식
enum DirectionEnum {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
// as const 대안 방식
const Direction = {
Up: "UP",
Down: "DOWN",
Left: "LEFT",
Right: "RIGHT",
} as const;
// 타입 추출
type Direction = typeof Direction[keyof typeof Direction];
// Direction = "UP" | "DOWN" | "LEFT" | "RIGHT"
타입 추출 패턴 상세
const Direction = {
Up: "UP",
Down: "DOWN",
Left: "LEFT",
Right: "RIGHT",
} as const;
// keyof typeof Direction = "Up" | "Down" | "Left" | "Right" (키 유니온)
// typeof Direction[keyof typeof Direction] = "UP" | "DOWN" | "LEFT" | "RIGHT" (값 유니온)
type DirectionType = typeof Direction[keyof typeof Direction];
function move(dir: DirectionType): void {
console.log(`Moving ${dir}`);
}
move(Direction.Up); // OK: "UP"
move("UP"); // OK: 문자열 리터럴도 허용
move("DIAGONAL"); // 오류: Argument of type '"DIAGONAL"' is not assignable
// 런타임에 모든 값 열거 가능
const allDirections = Object.values(Direction);
// ["UP", "DOWN", "LEFT", "RIGHT"]
숫자 값 as const 패턴
const HttpStatus = {
OK: 200,
Created: 201,
NoContent: 204,
BadRequest: 400,
Unauthorized: 401,
NotFound: 404,
InternalServerError: 500,
} as const;
type HttpStatusCode = typeof HttpStatus[keyof typeof HttpStatus];
// 200 | 201 | 204 | 400 | 401 | 404 | 500
function handleResponse(status: HttpStatusCode): void {
switch (status) {
case HttpStatus.OK:
console.log("성공");
break;
case HttpStatus.NotFound:
console.log("리소스를 찾을 수 없음");
break;
default:
console.log(`상태 코드: ${status}`);
}
}
실전 예제 1: HTTP 상태 코드와 방향
// 문자열 enum — API 메서드
enum HttpMethod {
Get = "GET",
Post = "POST",
Put = "PUT",
Patch = "PATCH",
Delete = "DELETE",
}
interface ApiRequest {
url: string;
method: HttpMethod;
body?: unknown;
}
async function apiCall(request: ApiRequest): Promise<unknown> {
const response = await fetch(request.url, {
method: request.method,
body: request.body ? JSON.stringify(request.body) : undefined,
headers: { "Content-Type": "application/json" },
});
return response.json();
}
// 사용
const req: ApiRequest = {
url: "/api/users/1",
method: HttpMethod.Get,
};
// 게임 방향 — const enum (번들 크기 최적화)
const enum GameDirection {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
interface Player {
x: number;
y: number;
}
function movePlayer(player: Player, dir: GameDirection): Player {
switch (dir) {
case GameDirection.Up: return { ...player, y: player.y - 1 };
case GameDirection.Down: return { ...player, y: player.y + 1 };
case GameDirection.Left: return { ...player, x: player.x - 1 };
case GameDirection.Right: return { ...player, x: player.x + 1 };
}
}
실전 예제 2: 권한(Role) 시스템
// as const 패턴으로 역할 정의
const Role = {
Guest: "GUEST",
User: "USER",
Moderator: "MODERATOR",
Admin: "ADMIN",
SuperAdmin: "SUPER_ADMIN",
} as const;
type RoleType = typeof Role[keyof typeof Role];
const RoleLevel: Record<RoleType, number> = {
[Role.Guest]: 0,
[Role.User]: 1,
[Role.Moderator]: 2,
[Role.Admin]: 3,
[Role.SuperAdmin]: 4,
};
function hasPermission(userRole: RoleType, requiredRole: RoleType): boolean {
return RoleLevel[userRole] >= RoleLevel[requiredRole];
}
function requireRole(userRole: RoleType, requiredRole: RoleType): void {
if (!hasPermission(userRole, requiredRole)) {
throw new Error(
`권한 부족: ${requiredRole} 이상 필요, 현재 ${userRole}`
);
}
}
// 사용
const currentUser = { name: "Alice", role: Role.Moderator };
requireRole(currentUser.role, Role.User); // OK
requireRole(currentUser.role, Role.Moderator); // OK
// requireRole(currentUser.role, Role.Admin); // Error: 권한 부족
실전 예제 3: 비트 플래그 권한
// 숫자 enum으로 비트 플래그 표현
enum Permission {
None = 0,
Read = 1 << 0, // 1
Write = 1 << 1, // 2
Execute = 1 << 2, // 4
Delete = 1 << 3, // 8
Admin = Read | Write | Execute | Delete, // 15
}
function hasFlag(permissions: number, flag: Permission): boolean {
return (permissions & flag) === flag;
}
function addFlag(permissions: number, flag: Permission): number {
return permissions | flag;
}
function removeFlag(permissions: number, flag: Permission): number {
return permissions & ~flag;
}
// 사용자 권한 조합
let userPerms = Permission.Read | Permission.Write; // 3 (0b0011)
console.log(hasFlag(userPerms, Permission.Read)); // true
console.log(hasFlag(userPerms, Permission.Execute)); // false
userPerms = addFlag(userPerms, Permission.Execute); // 7 (0b0111)
console.log(hasFlag(userPerms, Permission.Execute)); // true
userPerms = removeFlag(userPerms, Permission.Write); // 5 (0b0101)
console.log(hasFlag(userPerms, Permission.Write)); // false
고수 팁
팁 1: 문자열 enum보다 as const + 타입 추출을 선호하라
// 문자열 enum — 외부 문자열 값과 호환 안 됨
enum Status { Active = "ACTIVE" }
const s: Status = "ACTIVE"; // 오류!
// as const — 문자열 리터럴과 호환됨
const Status = { Active: "ACTIVE" } as const;
type Status = typeof Status[keyof typeof Status];
const s: Status = "ACTIVE"; // OK
팁 2: const enum은 선언 위치에 주의하라
// const enum은 같은 파일 또는 d.ts 파일에서 선언해야 한다
// isolatedModules 환경에서는 const enum을 외부에서 import해서 쓸 수 없다
// 팀 설정에 따라 일반 enum이나 as const로 대체하는 게 안전하다
팁 3: enum 완전성 검사는 never와 함께 사용하라
const enum Season { Spring, Summer, Autumn, Winter }
function describe(s: Season): string {
switch (s) {
case Season.Spring: return "봄";
case Season.Summer: return "여름";
case Season.Autumn: return "가을";
case Season.Winter: return "겨울";
default: {
const _exhaustive: never = s; // 모든 케이스 처리 시 never
return _exhaustive;
}
}
}
팁 4: enum 값의 유효성을 런타임에 검사하는 함수
enum Color { Red = "RED", Green = "GREEN", Blue = "BLUE" }
function isColor(value: string): value is Color {
return Object.values(Color).includes(value as Color);
}
const input = "RED";
if (isColor(input)) {
const color: Color = input; // 타입 가드 통과
console.log(color); // Color.Red
}
팁 5: 대규모 상수 집합은 enum 대신 Map이나 Record를 사용하라
// 수백 개의 에러 코드처럼 큰 상수 집합은 객체보다 Record가 유연하다
const ERROR_MESSAGES: Record<number, string> = {
1000: "인증 실패",
1001: "토큰 만료",
1002: "권한 없음",
// ...
};
function getErrorMessage(code: number): string {
return ERROR_MESSAGES[code] ?? `알 수 없는 오류 (${code})`;
}
비교 표
| 구분 | 숫자 Enum | 문자열 Enum | const Enum | as const 객체 |
|---|---|---|---|---|
| 자동 증가 | O | X | O (숫자) | X |
| 역방향 매핑 | O | X | X | X |
| 직렬화 가독성 | 낮음 | 높음 | 낮음 | 높음 |
| 컴파일 결과 | JS 객체 | JS 객체 | 인라인 치환 | JS 객체 |
| 트리 쉐이킹 | 어려움 | 어려움 | 완전 제거 | 쉬움 |
| 타입 안전성 | 부분적 | 완전 | 완전 | 완전 |
| isolatedModules | 호환 | 호환 | 불호환 | 호환 |
| 권장 상황 | 비트 플래그 | 일반 enum 필요 시 | 성능 최적화 | 대부분의 경우 |
다음 장에서는 TypeScript의 타입 추론 시스템을 깊이 파헤친다. 변수 초기화, 함수 반환값, 문맥적 타이핑, 타입 넓히기와 좁히기, 그리고 as const로 추론을 정밀하게 제어하는 방법을 배운다.