typescript enum을 쓰면 안된다는 말에 대해

dante Yoon·2022년 12월 3일
5

js/ts

목록 보기
5/14
post-thumbnail

안녕하세요, 단테입니다.

오늘은 typescript enum에 대한 이야기를 나눠보려고 합니다.

enum에 대한 사람들의 인식은 마냥 호의적이진 않은 것 같습니다.

어떠한 점 때문에 그런지 한번 알아보겠습니다.

어떤 점이 안좋을까?

타입 오염 (Heterogeneous enums)

enum은 숫자형 (numberic enums), 문자열형(string enums)로 나뉘어 집니다.

먼저 string 타입을 참조하기 위해 다음과 같이 enum을 선언하는 경우를 예시로 들겠습니다.

enum NamePolicy {
  phoneNumber = 'phoneNumber',
  countryCode = 'countryCode',
  tag = 'tag',
  ssn = 'ssn'
}

선언한 NamePolicy를 함수 인자에서 활용하려고 하면 아래처럼 의도에 맞게 자동완성이 되는데요

인자에 명시적으로 enum 값이 아닌 "phoneNumber"를 넣었을 때는 아래와 같이 에러를 발생합니다.

object literal 형식일 경우 "phoneNumber"도 인자로 선언할 수 있는데요, enum은 다르게 동작함을 알 수 있습니다.

바로 위의 상황에서 너그럽게 phoneNumber를 인자로 받아들여지지 않게 하는 것이 의도였든 의도이지 않았든 NamePolicy 값인 phoneNumber , countryCode, tag, ssngetNamePolicy의 인자로 사용할 수 있다는 것은 최소한 보장이 되었습니다.

그런데 말입니다.

다음처럼 임의의 number 타입이 NamePolicy에 들어가는 경우를 보겠습니다.

Before

enum NamePolicy {
  phoneNumber = 'phoneNumber',
  countryCode = 'countryCode',
  tag = 'tag',
  ssn = 'ssn',
}

console.log(Object.keys(NamePolicy)) //  ["phoneNumber", "countryCode", "tag", "ssn"] 

After

enum NamePolicy {
  phoneNumber = 'phoneNumber',
  countryCode = 'countryCode',
  tag = 'tag',
  ssn = 'ssn',
  pollute = 1
}

console.log(Object.keys(NamePolicy)) //  ["1", "phoneNumber", "countryCode", "tag", "ssn", "pollute"] 

Object.keys, Object.values의 반환 값에 pollute, 1이 함께 포함되는 것을 볼 수 있습니다. 예상치 못한 결과입니다.

After의 NamePolicy 경우처럼 string과 number 형을 함께 사용하는 enum을 이종 열거형(Heterogeneous enum)이라고 부르는데요, Object.values를 사용하지 않으면 되지 않나라는 생각이 들 수 있지만 함수 호출 부에서도 문제가 발생합니다.

pollute가 enum 값으로 들어가버린 후로 getNamePolicy는 1 뿐만 아니라 그 어떤 숫자형도 런타임에 인자값으로 받아들일 수 있게 되었습니다.

그 누구도 NamePolicy에 number 타입을 넣는 것을 막을 수 없습니다.

직렬화 문제

만약 숫자형만 사용한다고 하더라도 직렬화에서 문제가 발생합니다.

enum RainbowColorCode {
  red, // 0
  orange, // 1
  yellow,
  green,
  blue
}

무지개 색을 순서대로 값에 매핑시켰을 때 red는 0, orange는 1... 이런 식으로 되는데요,

red와 orange 사이에 누가 purple을 넣게 되면 orange 부터는 기존 값보다 1씩 증가된 형태가 됩니다.

enum RainbowColorCode {
  red, // 0
  purple, // 1
  orange, // 2
  yellow,
  green,
  blue
}

RainbowColorCode.Orange // 이제 이 오렌지는 2입니다.

숫자형 값을 사용하기 위한 직렬화에 enum을 사용한다면 이렇게 예상치 못한 경우를 막을 수가 없습니다.
코드 리뷰에서도 무심코 건너가버릴 수 있겠죠.

대안 - object literal

위의 문제를 예방하기 위해서 object literral 형식을 사용할 수 있습니다.

const rainbowColorCode = {
  red: 0,
  purple: 1,
  orange: 2,
  yellow: 3,
  green: 4,
  blue: 5
} 

type RainbowColorCodeValue =  typeof rainbowColorCode[keyof typeof rainbowColorCode];

function getColor(code: RainbowColorCodeValue) {
  console.log(code);
}

getColor 의 파라메터 타입은 RainbowColorCodeValue 로 선언되어있지만 RainbowColorCodeValue 은 number 타입이기 때문에 getColor(10) 와같이 [0..5] 이 아닌 다른 number 타입을 넣을 수 있습니다.

위 상황을 피하기 위해서
3.4 version 이상부터 지원되는 const assertion 를 사용합니다.

object literal 형식을 사용하면 key, value 값을 원하는대로 범위를 좁혀 사용할 수 있습니다.

앞서 선언한 rainbowColorCode에서 key, value만 타입으로 추출해 사용할 수 있습니다.

type RainbowColorCodeValue =  typeof rainbowColorCode[keyof typeof rainbowColorCode];

function getColor(code: RainbowColorCodeValue) {
  console.log(code);
}


type RainbowColorCodeKey = keyof typeof rainbowColorCode;

function pickColorValue(codeKey: RainbowColorCodeKey) {
  console.log(rainbowColorCode[codeKey])
}

vs string union

keyof typeof rainbowColorCode; 과 같이 object literal key 값 타입 추출은 string union 타입과 비교했을 때 어떤 장점이 있을까요?

type RainbowColorCodeKey = "red" | "purple" | "orange" | "yellow"

검색 용이성

string union 타입을 사용했을 때 IDE 에서 RainbowColorCodeKey를 사용한 흔적을 찾으려면 어떻게 해야할까요?
"red" | "purple" | "orange" | "yellow" 의 각 글자를 검색해야 할 것입니다.

이것 보다는 object literal 형식으로 작성된 rainbowColorCode 를 검색하는 것이 더 찾기 쉽고, 각 키 값에 매핑되는 값을 사용할 때도 IDE의 자동완성 기능을 사용할 수 있어 더 편리할 것입니다.

글을 마치며

오늘은 ts enum 사용 시 조심해야 할 점에 대해 알아보았습니다.
읽어주셔서 감사합니다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글