타입스크립트에서 enum 대신 Object.freeze를 사용하기

1. 타입스크립트 enum

타입스크립트의 enum은 간단하고 직관적입니다. 다음과 같이 사용할 수 있습니다.

export enum Color {
  RED = 'red',
  BLUE = 'blue',
  GREEN = 'green',
}

Color.RED 바로 사용할 수 있고, 타입도 잘 잡힙니다. 하지만 이 enum에는 몇 가지 문제점이 존재합니다.

문제 1: 런타임에 실제 객체가 생성됩니다

타입스크립트의 enum은 단순히 타입만 만들어주는 것이 아니라, 컴파일 후 자바스크립트 코드에도 실제 객체로 남습니다. 예를 들어 컴파일 후 코드를 보면 다음과 같습니다.

export var Color
;(function (Color) {
  Color['RED'] = 'red'
  Color['BLUE'] = 'blue'
  Color['GREEN'] = 'green'
})(Color || (Color = {}))

문제 2: 트리 셰이킹에 불리합니다

enum은 런타임 객체로 변환되기 때문에, 모듈 번들러(예: Webpack, Vite 등)가 안 쓰는 코드를 잘라내는 트리 셰이킹을 할 때도 전체가 포함돼 버립니다.

문제 3: 자바스크립트와의 호환성

enum은 타입스크립트만의 기능이라서, 순수 자바스크립트 환경에서는 다루기 불편할 수 있습니다. 예를 들어 라이브러리를 만들 때 JS 환경에서도 사용해야 한다면 제약이 될 수 있습니다.

2. as const vs Object.freeze

타입스크립트의 enum을 바로 사용하는 것보다 객체 + 타입 추론 방식으로도 enum을 구현할 수 있습니다. 그러면 위에 있는 문제점들을 해결 할 수 있습니다.

  • 런타임 비용이 없습니다 객체 그대로 사용하기 때문에 컴파일 후에도 불필요한 추가 코드가 붙지 않습니다.

  • 트리 셰이킹에 유리합니다 필요한 값만 가져오면 나머지는 자연스럽게 번들에서 제거됩니다.

  • 자바스크립트와 완벽하게 호환됩니다 객체는 자바스크립트에서도 자연스럽게 사용할 수 있으므로, 타입스크립트/자바스크립트 혼합 환경에서도 문제가 없습니다.

먼저 as const 라는 타입스크립트 전용 기능을 이용하여 enum을 만들어 보겠습니다.

export const Color = {
  RED: 'red',
  BLUE: 'blue',
  GREEN: 'green',
} as const

as const는 타입스크립트 컴파일 타임에서만 작동합니다. 각 프로퍼티의 타입을 리터럴 타입으로 고정해서 타입 안전성을 높여줍니다. 하지만 주의할 점이 있습니다: 실행 시에는 아무 일도 일어나지 않습니다. 즉, 런타임에서는 그냥 평범한 객체일 뿐이므로 실제 실행 중에는 객체를 변경할 수 있습니다.

// 컴파일 환경
Color.RED = 'something else' // 타입 오류 발생!
// 런타입 환경
Color.RED = 'SOMETHING_ELSE' //  런타임에서는 변경됨!
console.log(Color.RED) // 'SOMETHING_ELSE'

위 문제점을 보완한 것이 Object.freeze 입니다.

Object.freeze는 자바스크립트의 런타임 기능입니다. 이걸 쓰면 프로퍼티의 추가, 삭제, 변경이 모두 막힙니다. (엄격 모드에서는 에러가 발생하고, 그렇지 않으면 무시됩니다.)

const Color = Object.freeze({
  RED: 'red',
  BLUE: 'blue',
  GREEN: 'green',
})

한가지 더 알아야 할 부분이 있습니다. Object.freeze 를 활용하여 만든 enum을 사용해보겠습니다.

function colorFunction(color: typeof Color) {
  return color
}

colorFunction('red')

이렇게 사용 할경우 아래와 같은 타입 오류가 발생합니다. 현재 해당 함수의 파라미터 Color는 기대하는 건 “red”라는 문자열이 아니라 전체 객체 그 자체 를 원하고 있습니다.

그래서 아래와 같이 타입을 유니온 타입으로 변경하는 작업이 필요합니다.

import type { ValueOf } from 'type-fest'

export const Color = Object.freeze({
  RED: 'red',
  BLUE: 'blue',
  GREEN: 'green',
})

export type ColorType = ValueOf<typeof Color> //  'red' | 'blue' | 'green'

function colorFunction(color: ColorType) {
  return color
}

colorFunction('red') // 정상 동작!

또 하나의 주의할 점이 남았습니다.

Object.freeze는 얕은(shallow) 고정만 합니다.

const obj = Object.freeze({
  a: 1,
  b: { c: 2 },
})

obj.a = 999 // 변경되지 않습니다!
obj.b.c = 999 //  변경 됩니다!

이처럼 객체 안에 중첩된 객체가 있으면 그 안쪽 값들은 변경할 수 있습니다. 그러나 보통 enum처럼 쓰는 객체는 단순히 문자열이나 숫자만 포함되기 때문에 이런 얕은 고정 문제는 발생하지만 사용할때는 이 부분의 주의가 필요합니다.