피벗 테이블을 개발 중 연산과 관련된 타입들의 선언이 필요했다. 아래와 같이 요청 body에 대한 유니온 타입을 선언했다.
// pivotRequest.ts
export type FiltersOperators = 'eq' | 'ne' | 'le' | 'gte' | 'lte' | 'between' | 'like' | 'isnull';
export type AggregationTypes = 'sum' | 'count' | 'avg' | 'max' | 'min' | 'stddev' | 'stddev_samp' | 'stddev_pop' | 'variance' | 'var_samp' | 'var_pop';
그런데 이 연산자들을 순회하여 select box 옵션 값들로도 넣어 주어야 했다. 따라서, 아래와 같이 option 객체를 한번 더 만들어줘야 했다.
// pivotOptions.ts
export const AGG_OPTIONS: AggregationTypes[] = ['sum', 'count', 'avg', 'max', 'min', 'stddev', 'stddev_samp', 'stddev_pop', 'variance', 'var_samp', 'var_pop',];
export const OPERATOR_OPTIONS: FiltersOperators[] = ['eq', 'ne', 'le', 'gte', 'lte', 'between', 'like', 'isnull'];
전 회사에서 프로젝트를 개발할 때는 enum 타입을 적용했었는데, 두 방법의 차이와 고려해 볼만한 다른 타입 선언 방법이 있는지 알아보았다.
고려할 수 있는 타입 선언 방법은 3가지가 있다.
유니온 타입
먼저 유니온 타입인데, 내가 사용했었던 방법이다.
type Role = "ADMIN" | "USER" | "GUEST";
// 사용
function canEdit(r: Role) { return r === "ADMIN"; }
// 순회/표시용 리스트는 따로 둬야 함
export const ROLES = ["ADMIN", "USER", "GUEST"] as const;
해당 방법은 순회가 필요하다면, 내가 했던 것처럼 별도의 리스트를 따로 만들어주어야 한다는 단점이 있지만, 오타를 통해 타입 안정성은 확실하게 관리가 가능하다.
하지만, 내가 어떤 타입을 가졌는지 전부 기억해야 하고, 변경이 필요하면 사용되는 곳을 모두 찾아서 바꿔야 하는 단점이 있다. String 타입의 유니온 타입은 리펙터링하기에 번거러운 점이 많다고 생각한다.
Enum 타입
export enum Role {
Admin = "ADMIN",
User = "USER",
Guest = "GUEST",
}
function canEdit(r: Role) { return r === Role.Admin; }
Object.values(Role); // 순회 가능
Enum 타입은 유니온 타입과 달리, 런타임 객체가 생기기 때문에 값과 타입으로 모두 사용이 가능하다.
하지만, 객체가 만들어지기 때문에 코드가 즉시실행함수(IIFE) 형태로 항상 생성되기 때문에, 불필요한 런타임 오버헤드가 생길 수 있다. 또한, 번들 크기도 커지기도 한다.
// 아래와 같은 즉시 실행 함수가 enum 객체를 런타임에 생성한다.
export var Role;
(function (Role) {
Role["Admin"] = "ADMIN";
Role["User"] = "USER";
Role["Guest"] = "GUEST";
})(Role || (Role = {}));
Const Enum 타입
const enum Role {
Admin = "ADMIN",
User = "USER",
Guest = "GUEST",
}
const r: Role = Role.Admin;
console.log("USER"); // Role.User -> "USER"
Const Enum 타입은 런타임 객체가 없기 때문에 IIFE가 없다. 하지만, 직접적인 값으로 치환되기 때문에 (런타임 객체가 생기지 않아서) 순회가 불가능하다.
유니온과 enum 타입을 어떻게 사용할 것인가에 대한 의견은 다양한 것으로 알고 있다. 각각의 장단점이 있기 때문에 당연히 정답은 없다고 생각한다.
나도 고민을 하던 중, Gpt가 아래와 같은 좋은 방안을 알려주었다.
export const Role = {
Admin: "ADMIN",
User: "USER",
Guest: "GUEST",
} as const;
export type Role = (typeof Role)[keyof typeof Role];
// 필요 시 순회
export const ROLES = Object.values(Role); // ["ADMIN","USER","GUEST"]
위의 방법이 처음에는 이해하기가 어려웠었는데, 풀어보면 다음과 같다. typeof Role은 결국 아래처럼 나타낼 수 있다. (as const 덕분에 readonly로 표현된다.)
{
readonly Admin: "ADMIN";
readonly User: "USER";
readonly Guest: "GUEST";
}
keyof typeof Role은 그 타입의 키 유니온으로 아래와 같다.
"Admin" | "User" | "Guest"
런타임 객체가 평범한 객체라 번들러가 사용 안 하면 쉽게 제거할 수 있고, 순회도 가능하다. 하지만 이 방법도 타입을 따로 선언해주어야 하기 때문에 불편하다는 의견이 있었다.
결국, 각각의 장단점을 숙지하고 팀의 목적에 맞게 방법을 선택하는 것이 중요할 것이다.
'Typescript' 카테고리의 다른 글
| Exclude 활용법 (0) | 2025.10.30 |
|---|---|
| const assertions 이해하기 (0) | 2025.10.24 |
| React Flow에서 제네릭(Generic) 이해하기 (0) | 2025.10.19 |
| unknown 타입과 any의 차이 (0) | 2025.02.06 |
| 왜 타입스크립트를 쓰나요? (1) | 2024.12.26 |