본문 바로가기
프로그래밍 언어/TypeScript

[TypeScript] dto enum 값 as const + 유니온 조합으로 설정하기

by BK0625 2025. 7. 11.
반응형

 

 

프로젝트 진행 도중 dto 값 중 하나의 필드가 ai 혹은 user만 들어가야 되는 상황이였다. 여기서 전통적인 타입스크립트에서의 방법은 바로 enum을 사용하는 것이다.

 

export enum Role {
  USER = 'user',
  AI = 'ai',
}

export class CreateChatDto {
  @IsEnum(Role)
  role: Role;
}

 

 

타입스크립트에서의 enum은 편리하지만 문제가 있어 요새는 잘 쓰이지 않는다고 하는데 이유는 다음과 같다.

  • 런타임 시 js 코드로 남음(컴파일 후 객체가 됨)
    • 예시→ 컴파일 후
    • var Role; (function (Role) { Role["User"] = "user"; Role["Ai"] = "ai"; })(Role || (Role = {}));
    • enum Role { User = 'user', Ai = 'ai', }
    • 이렇게 런타임에 사용될 객체 코드 생성
    • 이는 번들 크기 증가, 트리쉐이킹 불가, 성능 저하로 이어질 수 있다.
  • enum은 타입이면서 값이여서 혼동됨
    • enum은 타입과 값이 같기 때문에, 코드에서 사용 시 혼동되기 쉽다.
    • 이중 역할을 하기 때문에 코드 이해가 어려워지고, 테스트/리팩터링이 어려워질 수 있다.
  • enum Color { Red = 'red', } function getColor(c: Color) { ... } getColor(Color.Red); // ✅ 값으로 전달
  • enum은 자바스크립트 표준이 아니기 때문에 js 개발자와의 협업이나 외부 라이브러리 사용 시 문제가 발생할 수 있다.
  • 트리쉐이킹이 잘 안됨(사용하지 않아도 코드에 남아있음)
  • typeof 사용 시 불편함

 

따라서 좀 더 모던하게 as const + 유니온으로 구현해보자

 

export const ROLES = ['user', 'ai'] as const;
export type Role = (typeof ROLES)[number];

export class CreateChatDto {
  @IsIn(ROLES)
  role: Role;
}

 

 

이렇게 하면 컴파일 후 코드가 거의 남지 않으며(트리쉐이킹 친화적) 타입 정의와 값 정의가 동시에 가능하며 좀 더 유연하고 가볍다.

as const

배열이나 객체를 리터럴 타입으로 고정시킨다.

여기서 리터럴 타입이란 타입스크립트에서 값 그 자체를 타입으로 사용할 수 있게 하는 개념이다. 기본적인 타입보다 더 구체적이고 제한적인 타입을 명시할 수 있게 해준다. 따라서 string, number, boolean 중 하나의 구체적인 값만 허용하는 타입

 

let role: 'user';

role = 'user'; // ✅ OK
role = 'ai';   // ❌ Error: 'ai'은 'user' 타입이 아님

// 여기서 'user'는 값이 아닌 그 값 자체를 타입으로 본 것임

 

이렇게 되면 기존 string 타입은 아무 문자열이나 혀용하지만 ‘role’은 오직 ‘user’만 허용하게 되어 더욱 더 제한적이고 명시적인 타입을 제시할 수 있다.

 

그러니까 여기서는 ‘ai’와 ‘user’ 값들이 타입 배열으로 고정된 것이다.

그러면 dto를 정의해보자.

 

import { IsString, IsIn } from 'class-validator';

/** 1. Role 값을 정의하고 타입으로도 쓰기 */
export const ROLES = ['user', 'ai'] as const;
export type Role = (typeof ROLES)[number]; // "user" | "ai"

/** 2. DTO에 적용 */
export class CreateChatDto {
  @IsString()
  @IsIn(ROLES, { message: 'role must be either "user" or "ai"' })
  role: Role;

  // 예시: message 내용
  @IsString()
  message: string;
}

 

 

우선 앞서 설명한 as const로 ‘user’와 ‘ai’ 를 타입 배열로 고정했다.

그 뒤 type Role 에 typeof ROLES[number]를 할당해 그 배열 요소 중 하나 즉 “user”|”ai”를 의미하게 된다.

 

 

항목 설명
가볍고 빠름 enum은 컴파일 시 JS 객체로 남지만, as const는 그렇지 않음
타입과 값 일치 한 번 정의로 타입 검사도 되고 값 검증도 가능
NestJS와 호환 @IsIn()으로 런타임 유효성 검사도 충분히 커버 가능
IDE 지원 좋음 "user"
트리쉐이킹 불필요한 JS 코드 제거가 쉬움 (빌드 최적화에 유리)

 

주의할 점

  • @IsEnum() 데코레이터는 사용할 수 없고 @IsIn()을 써야 함
  • 숫자 기반 enum처럼 인덱싱 접근이 필요한 경우에는 부적합

 

공부하면서 정리한 내용입니다. 모든 지적 감사히 받겠습니다:)

반응형