타입스크립트 상수 선언 : enum vs const enum vs as const

Y·2025년 7월 20일
post-thumbnail

이 글에서는 타입스크립트에서 상수를 선언하는 방법들과 각각의 장단점에 대해 다룹니다

목차

  1. 왜 이 글을 쓰게 되었나
  2. Enum
  3. Const Enum
  4. as Const
  5. 성능과 번들 사이즈 비교
  6. 실전 선택 가이드
  7. 마무리

왜 이 글을 쓰게 되었나

타입스크립트에서 상수를 관리할 때 항상 as const 를 사용 해왔습니다
enum 은 안 좋다고만 알고 있었는데 최근 동료가 enum을 왜 사용하지 않는지에 대해 물어 봤는데
명확히 답변을 하지 못 했습니다 따라서 각 방법의 정확한 동작 원리와 장단점을 이해하고자 이 글을 작성하게 되었습니다

주의: 이 글의 번들 사이즈 비교는 특정 환경에서의 측정값이며, 실제 프로젝트에서는 번들러 설정, 압축 방식 등에 따라 크게 달라질 수 있습니다.

Enum

Enum 사용법

TypeScript의 enum은 연관된 상수들을 그룹화하는 기능입니다.

Numeric Enum

// 기본 사용법 (자동 증가)
enum Direction {
  Up,      // 0
  Down,    // 1
  Left,    // 2
  Right    // 3
}

// 시작값 지정
enum Direction {
  Up = 1,
  Down,    // 2
  Left,    // 3
  Right    // 4
}

// 사용
const dir: Direction = Direction.Up;
console.log(dir); // 1

String Enum

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}

// 사용
const dir: Direction = Direction.Up;
console.log(dir); // "UP"

컴파일 결과

// TypeScript
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 = {}));

Enum 장점

1. 양방향 매핑 (Numeric Enum)

enum HttpStatus {
  OK = 200,
  BadRequest = 400,
  NotFound = 404
}

// 이름 → 값
console.log(HttpStatus.OK); // 200

// 값 → 이름 (리버스 매핑)
console.log(HttpStatus[200]); // "OK"

2. 런타임 객체로 활용 가능

enum Permission {
  Read = "READ",
  Write = "WRITE",
  Delete = "DELETE"
}

// 모든 권한 목록 가져오기
const allPermissions = Object.values(Permission);
console.log(allPermissions); // ["READ", "WRITE", "DELETE"]

// 권한 체크 함수
function hasPermission(userPermissions: string[], required: Permission): boolean {
  return userPermissions.includes(required);
}

3. 명확한 타입 안전성

enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE"
}

function processStatus(status: Status) {
  // status는 반드시 Status enum의 값이어야 함
}

processStatus(Status.Active);  // OK
processStatus("ACTIVE");       // Error

4. IDE 자동완성 지원

enum Theme {
  Light = "light",
  Dark = "dark",
  Auto = "auto"
}

// IDE에서 Theme. 입력 시 모든 옵션 표시
const userTheme = Theme.Dark;

Enum 단점

1. 번들 사이즈 증가

// 5개의 enum 멤버 = 약 200-300 bytes
enum Colors {
  Red = "#FF0000",
  Green = "#00FF00",
  Blue = "#0000FF",
  Yellow = "#FFFF00",
  Purple = "#800080"
}

// 사용하지 않아도 전체 객체가 번들에 포함됨

2. Tree Shaking 제한

// colors.ts
export enum Colors {
  Red = "red",
  Green = "green",
  Blue = "blue",
  // ... 100개의 색상
}

// main.ts
import { Colors } from './colors';
console.log(Colors.Red); // 전체 enum 객체가 번들에 포함됨

// 참고: TypeScript 5.0+ 및 최신 번들러에서는 부분적 개선이 있으나 
// 여전히 as const보다는 tree shaking이 제한적임
// (tree shaking 은 미사용 코드를 번들러가 제거해주는 기능으로 번들 크기를 줄여줍니다)

Const Enum

Const Enum 사용법

const enum은 컴파일 시점에 인라인되는 enum입니다.

const enum Direction {
  Up,
  Down,
  Left,
  Right
}

// 사용
const dir = Direction.Up;

// 컴파일 결과
const dir = 0 /* Up */;

Const Enum 장점

1. 런타임 오버헤드 없음 (tsc 사용 시)

// TypeScript
const enum HttpMethod {
  GET = "GET",
  POST = "POST",
  PUT = "PUT",
  DELETE = "DELETE"
}

const method = HttpMethod.GET;

// JavaScript (tsc로 컴파일 시)
const method = "GET" /* GET */;

// 주의: esbuild, SWC 등에서는 동작하지 않음

2. 최적의 성능

const enum MathConstants {
  PI = 3.14159,
  E = 2.71828
}

// 직접 값이 인라인되므로 객체 접근 오버헤드 없음
const area = radius * radius * 3.14159 /* PI */;

3. Enum의 타입 안전성 유지

const enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE"
}

function handleStatus(status: Status) {
  // 여전히 타입 체크 가능
}

handleStatus(Status.Active);  // OK
handleStatus("ACTIVE");       // Error

Const Enum 단점

1. isolatedModules 호환성 문제 (심각)

// tsconfig.json
{
  "compilerOptions": {
    "isolatedModules": true  // Next.js, CRA 등 대부분의 모던 프레임워크 기본값
  }
}

// 이 설정에서 const enum은 일반 enum처럼 동작
const enum Status {
  Active = "ACTIVE"
}

// 컴파일 결과: 인라인되지 않고 객체 생성됨
var Status;
(function (Status) {
    Status["Active"] = "ACTIVE";
})(Status || (Status = {}));

2. 빌드 도구별 지원 현황 (파편화)

// TypeScript 컴파일러 (tsc)
const enum Status { Active = 1 }
console.log(Status.Active); // 1로 인라인됨

// esbuild
// const enum 인라인 미지원 (isolatedModules와 동일하게 동작)
// GitHub Issue #128에서 2020년부터 논의 중

// SWC  
// 부분적 지원 (최신 버전에서 개선 중이나 여전히 불완전)
// 일부 케이스에서 빈 statement로 컴파일되는 버그 존재

// Babel
// const enum 문법 자체를 지원하지 않음
// babel-plugin-const-enum 같은 플러그인 필요

// Vite (esbuild 기반)
// const enum 인라인 미지원

3. 외부 라이브러리 문제 (치명적)

// 라이브러리에서 const enum 내보내기 - 절대 금지!
// library/index.d.ts
export const enum LibEnum {
  Value = 1
}

// app.ts
import { LibEnum } from 'library';
console.log(LibEnum.Value); // 컴파일 에러 또는 undefined

// TypeScript 팀 공식 권장사항:
// "라이브러리를 만들 때는 const enum을 사용하지 마세요"

4. 디버깅 어려움

const enum ErrorCode {
  NotFound = 404,
  ServerError = 500
}

// 런타임에서는 숫자만 보임
console.log(getError()); // 404 (어떤 에러인지 알기 어려움)

// 일반 enum은 리버스 매핑으로 확인 가능
console.log(ErrorCode[404]); // "NotFound"

5. 마이그레이션 비용

// 빌드 도구를 변경할 때마다 const enum 동작이 달라질 수 있음
// tsc → esbuild: const enum이 일반 enum처럼 동작
// esbuild → SWC: 예상치 못한 컴파일 결과 가능

as Const

as Const 사용법

as const는 TypeScript 3.4에서 도입된 const assertion입니다.

// 기본 사용법
const Colors = {
  Red: "#FF0000",
  Green: "#00FF00",
  Blue: "#0000FF"
} as const;

// 타입 추출
type Colors = typeof Colors[keyof typeof Colors];
// type Colors = "#FF0000" | "#00FF00" | "#0000FF"

// 사용
function setColor(color: Colors) {
  console.log(color);
}

setColor(Colors.Red);    // OK
setColor("#FF0000");     // OK (리터럴 값도 가능)

As Const 장점

1. JavaScript 친화적

// 순수 JavaScript 객체이므로 누구나 이해 가능
const Status = {
  Active: "active",
  Inactive: "inactive"
} as const;

// JavaScript 개발자도 바로 이해 가능한 코드

2. Tree Shaking 가능

// colors.ts
export const Colors = {
  Red: "#FF0000",
  Green: "#00FF00",
  Blue: "#0000FF",
  // ... 100개의 색상
} as const;

// main.ts
import { Colors } from './colors';
console.log(Colors.Red); // Red만 번들에 포함

3. 유연한 타입 시스템

const Config = {
  api: {
    baseUrl: "https://api.example.com",
    timeout: 5000
  },
  features: {
    darkMode: true,
    notifications: false
  }
} as const;

// 중첩된 타입 추출 가능
type ApiConfig = typeof Config.api;
type FeatureFlags = typeof Config.features;

// 유틸리티 타입과 조합
type ConfigKeys = keyof typeof Config;
type ConfigValues = typeof Config[ConfigKeys];

4. 런타임 기능 모두 사용 가능

const Permissions = {
  Read: "READ",
  Write: "WRITE",
  Delete: "DELETE"
} as const;

// 모든 JavaScript 기능 사용 가능
Object.keys(Permissions); 
Object.values(Permissions);
Object.entries(Permissions);

// 동적 접근
const permission = "Read";
console.log(Permissions[permission]);

As Const 단점

1. 타입 정의 복잡성

// Enum - 타입이 자동 생성
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE"
}

// As Const - 타입을 별도로 정의해야 함
const Status = {
  Active: "ACTIVE",
  Inactive: "INACTIVE"
} as const;
type Status = typeof Status[keyof typeof Status];

// 리팩토링 시 둘 다 관리해야 함

2. 런타임 수정 가능성

const Status = {
  Active: "ACTIVE",
  Inactive: "INACTIVE"
} as const;

// TypeScript는 막지만 런타임에서는 가능
(Status as any).Active = "MODIFIED"; // 런타임 에러 위험

// enum은 원천적으로 불가능
enum StatusEnum {
  Active = "ACTIVE"
}
// StatusEnum.Active = "MODIFIED"; // 컴파일 자체가 불가능

3. 리터럴 값 직접 사용 가능

const Status = {
  Active: "ACTIVE",
  Inactive: "INACTIVE"
} as const;

type Status = typeof Status[keyof typeof Status];

function handleStatus(status: Status) {
  console.log(status);
}

// 의도와 다르게 리터럴 값 직접 사용 가능
handleStatus("ACTIVE"); // OK
handleStatus("RANDOM"); // Error

// 하지만 오타나 복사-붙여넣기 실수 위험
handleStatus("ACTIV"); // 컴파일 에러지만 실수하기 쉬움

4. IDE 리팩토링 지원 부족

// enum: "Active" 이름 변경 시 모든 참조 자동 업데이트
enum Status { Active = "ACTIVE" }

// as const: 객체 속성과 타입을 각각 수동으로 관리
const StatusValues = { Active: "ACTIVE" } as const;
type Status = typeof StatusValues[keyof typeof StatusValues];

성능과 번들 사이즈 비교

번들 사이즈 비교

실제 측정 결과를 바탕으로 한 번들 크기 비교:

// 1. Enum (10개 항목)
enum LargeEnum {
  Option1 = "OPTION_1",
  Option2 = "OPTION_2",
  // ... 10개 항목
}
// 컴파일 후: 약 425 bytes
// Minified: 약 240 bytes
// Gzipped: 약 120-150 bytes

// 2. Const Enum (10개 항목)
const enum LargeConstEnum {
  Option1 = "OPTION_1",
  Option2 = "OPTION_2",
  // ... 10개 항목
}
// 컴파일 후: 0 bytes (인라인됨)
// 주의: isolatedModules: true 환경에서는 일반 enum처럼 동작

// 3. As Const (10개 항목)
const LargeConst = {
  Option1: "OPTION_1",
  Option2: "OPTION_2",
  // ... 10개 항목
} as const;
// Minified: 약 200 bytes
// Tree shaking 시: 사용한 속성만 포함

// 4. Union Type (언급되지 않은 대안)
type Options = "OPTION_1" | "OPTION_2" | /* ... */ "OPTION_10";
// 컴파일 후: 0 bytes (타입만 존재)

중요: 실제 번들 크기는 번들러 설정, 압축 알고리즘, 사용 패턴에 따라 크게 달라집니다. 몇 백 바이트 차이가 결정적 요인이 되는 경우는 드뭅니다.

Tree Shaking 테스트

// shared.ts
export enum Colors {
  Red = "#FF0000",
  Green = "#00FF00",
  Blue = "#0000FF"
}

export const ColorsConst = {
  Red: "#FF0000",
  Green: "#00FF00",
  Blue: "#0000FF"
} as const;

// main.ts
import { Colors, ColorsConst } from './shared';

console.log(Colors.Red);      // 전체 Colors enum이 번들에 포함
console.log(ColorsConst.Red); // ColorsConst.Red만 번들에 포함

실전 선택 가이드

프로젝트 유형별 권장사항

라이브러리 개발

// 권장: enum (명확한 API 제공)
export enum LogLevel {
  Debug = 0,
  Info = 1,
  Warning = 2,
  Error = 3
}

// 피하세요: const enum (사용자 환경에서 문제 발생)
export const enum BadEnum { ... }

애플리케이션 개발

// 권장: as const (유연성과 tree shaking)
export const Theme = {
  colors: {
    primary: "#007bff",
    secondary: "#6c757d"
  }
} as const;

// 대안: Union Type (간단한 경우)
type Status = "active" | "inactive" | "pending";

레거시 시스템 통합

// 권장: enum (기존 코드와의 호환성)
enum ApiErrorCode {
  Unauthorized = 401,
  Forbidden = 403,
  NotFound = 404
}

성능이 정말 중요한가?

실제 성능 차이는 미미합니다:

  • enum vs as const: 나노초 단위 차이
  • 번들 크기: 수백 바이트 차이 (gzip 후 더 작음)
  • 더 중요한 것: 코드 가독성, 유지보수성, 팀 컨벤션

Union Type - 놓치기 쉬운 대안

// 간단한 상수 집합에는 Union Type도 좋은 선택
type Direction = "up" | "down" | "left" | "right";

// 장점: 번들 크기 0, 타입 안전성 확보
// 단점: 런타임 값 없음, 자동완성 시 문자열 입력 필요

마무리

처음에는 단순히 enum은 나쁘다고만 생각했지만, 실제로는 각 방법마다 명확한 장단점과 사용 시나리오가 있었습니다.

실제 트레이드오프

  • Enum: 최고의 타입 안전성, 약간의 번들 크기 비용
  • Const Enum: 빌드 도구 의존성이 심해 실무에서는 권장하지 않음
  • As Const: 유연하지만 타입 정의 복잡성 존재
  • Union Type: 간단한 경우에 가장 깔끔한 해결책

빌드 도구별 const enum 지원 현황

  • tsc: 완벽 지원
  • esbuild: 미지원 (Vite 포함)
  • SWC: 부분 지원 (불안정)
  • Babel: 플러그인 필요

핵심 교훈
1. 번들 크기 몇 백 바이트로 기술 결정을 하는 것은 과도한 최적화일 수 있습니다
2. 각 프로젝트의 요구사항과 제약사항을 고려하여 선택하세요
3. 팀의 개발 경험과 유지보수성이 더 중요한 경우가 많습니다
4. const enum은 빌드 도구 호환성 문제로 피하는 것이 좋습니다

profile
타입스크립트를 기반으로 프론트엔드와 백엔드 개발을 하고 있습니다. 인프라와 DevOps 영역도 틈틈이 공부하고 있고, 기술적 구현과 함께 비즈니스 관점에서의 고민도 놓치지 않으려 합니다

0개의 댓글