...
고급 타입 Enum
enum은 C, Java와 같은 언어를 다뤄봤으면 한번쯤 들어보는 흔하게 쓰이는 타입으로 특정 값(상수)들의 집합을 의미한다.
타입스크립트의 튜플 타입이 특정 타입이나 값을 고정하는 배열이라면, Enum은 특정 값을 고정하는 또다른 독립된 자료형이라고 보면 된다.
enum Color {
Red,
Green,
Blue,
} // enum 타입
let c: Color; // enum 타입 변수 선언
c = Color.Green; // enum 타입 변수에 enum 값 할당
c = 'Hello'; // Error - enum 타입 변수에는 반드시 설정된 enum 값 (Red, Green, Blue)만 올수 있음
let d: Color.Red; // enum 값을 타입 자체로도 사용할 수가 있음
Enum 인덱싱
기본적으로 enum은 0부터 시작하여 멤버들의 번호를 매긴다는 특징을 가지고 있다. (마치 배열 인덱스 같이)
enum Week {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat
}
하지만 enum의 멤버 번호들은 반드시 0부터 시작할 필요가 없다.
이처럼 수동으로 값을 명시 할 수 있으며, 값을 변경한 부분부터 다시 1씩 증가하게 매겨진다.
enum의 재미있는 기능 중 하나는, 역방향으로 매겨진 번호로 enum 멤버의 이름을 알아낼 수 있다는 점이다.
예를 들어 위의 예제에서 2라는 번호가 위의 어떤 Week enum 멤버와 매칭되는지 알 수 없을 때, 이에 일치하는 이름을 알아낼 수 있다
enum Week {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat
}
console.log(Week.Sun); // 0
console.log(Week['Sun']); // 0
console.log(Week[0]); // 'Sun'
let weekName: string = Week[0];
console.log(weekName); // 값이 5인 금요일이 출력 -> Fri
추가로, enum은 꼭 숫자 번호 뿐만 아니라 문자열 값으로 초기화할 수 있다.
단, 주의할 점은 문자열로 열거할 경우 역방향 매핑(Reverse Mapping)을 지원하지 않으며 개별적으로 반드시 초기화해야 하는 단점이 있다. 그리고 숫자형은 자동 증가가 되지만, 당연히 문자혈 enum은 그런 기능 따위 없다.
대신 디버깅을 할 때 숫자형 이넘의 값은 가끔 불명확하게 나올 떄가 있지만 문자형 이넘은 항상 명확한 값이 나와 읽기 편하다는 장점이 있다.
enum Color {
Red = 'red',
Green = 'green',
Blue = 'blue',
}
console.log(Color.Red); // red
console.log(Color['Green']); // green
Enum 헬퍼 함수
enum에 특정한 타입 값이 있는지 순회하며 검사하는 헬퍼 함수이다.
여기서 눈 여겨 볼 것은 Object.keys() 의 결과값인데, [ '0', '1', '2', 'High', 'Medium', 'Low' ] 이런식으로 나오는 이유는 위에서 언급했듯이 enum은 Priority.High 혹은 Priority[0] 둘다 접근할수 있기 때문이다.
enum Priority {
High,
Medium,
Low,
}
// 어떤 Enum에 특정 value가 있는지 검사해주는 함수
export function getIsValidEnumValue(enumObject: any, value: number | string) {
return Object.keys(enumObject)
.filter((key) => isNaN(Number(key)))
.some((key) => enumObject[key] === value);
}
/*
함수 실행 순서 :
Object.keys(enumObject) 하면 -> [ '0', '1', '2', 'High', 'Medium', 'Low' ] 로 변환됨
.filter((key) => isNaN(Number(key))) -> 여기서 숫자문자 요소를 제거해주고
.some((key) => enumObject[key] === value); -> 여기서 인수값을 받은 두번째 매개변수와 비교해서 enum에 값이 있는지 없는지 검사해줌
*/
const result = getIsValidEnumValue(Priority, Priority.High);
console.log('result: ', result); // true
const result2 = getIsValidEnumValue(Priority, 1);
console.log('result: ', result2); // true
Enum 매핑 하기
다음과 같은 Priority 라는 enum 타입이 있다고 하자.
이 Priority 이넘에는 High, Medium, Low 라는 세가지 값이 있는데, 이넘 출력을 영문이 아닌 한글로 출력하고 싶다.
하지만 이넘 자체 데이터 값을 한글로 바꿔줄수는 없다.
enum Priority {
High,
Medium,
Low,
}
이때 자바스크립트에서 기본으로 제공하는 속성변수 기능인 computed proprety 문법을 사용하면 된다.
enum Priority {
High,
Medium,
Low,
}
//* priority 이넘 타입을 한글로 출력하기 위한 매핑
export const PRIORITY_NAME_MAP = {
[Priority.High]: '높음',
[Priority.Medium]: '중간',
[Priority.Low]: '낮음',
};
class Todo {
priority;
constructor(priority: Priority) {
this.priority = priority;
}
getPriority() {
console.log(`${PRIORITY_NAME_MAP[this.priority]}`); // 한글 매핑 시킨 객체 속성으로 enum 값을 줌
}
}
const t: Todo = new Todo(Priority.High);
t.getPriority(); // 높음
Enum 타입 가드
맵드 타입을 이용하면 Enum 타입의 활용도를 높일 수 있다.
enum Fruit {
Apple,
Banana,
Orange,
}
const FRUIT_PRICE: { [key in Fruit]: number } = {
[Fruit.Apple]: 1000,
[Fruit.Banana]: 1500,
[Fruit.Orange]: 2000,
};
예를 들어서 위의 코드와 같이 Fruit 라는 enum 타입이 있는데, FRUIT_PRICE 라는 상수에서 모든 과일 가격을 관리한다고 가정을 해보면, 만약 enum 타입이 변화가 있을때 다음과 같이 빨간줄로 경고를 해준다.
FRUIT_PRICE 상수의 타입으로 : { [key in Fruit]: number } 라는 요상한 모양이 바로 맵드(mapped) 타입이라는 것이다. 이처럼 enum 타입에 코드 추가/수정이 필요하면 맵드 타입에 의해서 빨간줄이 나와 실수를 줄일 수 있다.
상수 Enum
enum을 사용하면 컴파일 후에도 객체가 남기 때문에 번들 파일이 불필요하게 커질수 있다는 단점이 있다.
만일 enum 객체에 빈번하게 접근하지 않는다면, 굳이 컴파일 후에 객체로 남겨 놓을 필요가 없다. 이때 사용하는 것이 const enum 이다.
예를들면, 생 enum 같은 경우 자바스크립트로 컴파일 되면 다음과 같이 된다.
enum Bool {
True,
False,
FileNotFound
}
let value = Bool.FileNotFound;
var Bool;
(function (Bool) {
Bool[(Bool["True"] = 0)] = "True";
Bool[(Bool["False"] = 1)] = "False";
Bool[(Bool["FileNotFound"] = 2)] = "FileNotFound";
})(Bool || (Bool = {}));
let value = Bool.FileNotFound;
반면 const Enum을 사용하면 다음과 같이 컴파일 시점에 상수로 치환된다.
const enum Bool {
True,
False,
FileNotFound
}
let value = Bool.FileNotFound;
let value = 2; /* FileNotFound */
이처럼 const enum은 내부 상수값들이 전부 compile 단계에서 내부 필드를 전부 상수로 변경하기 때문에 런타임에 의존 모듈의 영향을 받지 않게 되며, 코드 크기가 더 적기 때문에 더 선호된다고 한다.
이렇게 보면 무조건 const enum 이 좋다고 생각되어지지만, 꼭 그렇지 않다.
예를들면 const enum은 다음과 같이 reverse mapping 이 불가능하다. 실제로 Bool[3] 이런식으로 접근하면 에러가 난다.
const enum Bool {
True,
False,
FileNotFound
}
let value = Bool.FileNotFound;
let value2 = Bool[3]; // ERROR! - A const enum member can only be accessed using a string literal.
정리하자면, 생 enum은 reverse mapping이 필요한 경우에만 사용하는 것이 좋고, 그 외의 경우에는 const enum으로 사용하는 것이 좋다고 볼 수 있다. (실무에서도 왠만한 상황이 아니면 reverse mapping을 사용할 일이 없기 때문에 const enum 이 더 선호된다)
Enum 대체하기
이처럼 Enum은 여러개의 변수들을 하나의 그룹으로 묶어주고 싶을때 사용하는데, 현재 여러분들의 머릿속에 당장 예시가 안떠오르듯이 사실 실무에서도 그렇게 꼭 써야하는 필요한 자료형이 아니긴 하다.
심지어 Enum을 완전 대체할 수 있는 방법도 존재한다.
const enum EDirection {
Up,
Down,
Left,
Right,
}
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
console.log(EDirection.Left); // 2
console.log(ODirection.Right); // 3
// Enum을 타입으로 사용
function walk(dir: EDirection) {
console.log(dir);
}
// 객체를 타입으로 사용하기 위해선 typeof 와 keyof 파라미터를 사용해야 한다
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {
console.log(dir);
}
walk(EDirection.Left); // 2
run(ODirection.Right); // 3
이런식으로 const 객체를 선언해주고 뒤에 as const 타입 단언을 사용하면 Enum 흉내가 가능하다.
다만 const enum 객체 같은 경우 컴파일되면 사라지지만, as const 객체 같은 경우 생 자바스크립트 이기 때문에 컴파일되도 사라지지 않는다. 그래도 컴파일된 후의 용량은 Enum에 비해 작은 편이다.
보너스로 저 해괴망측한 타입 형식은 어떤 원리로 구성되어 있는 것일까?
Enum을 쓰기 싫어서 익숙한 const 객체로 대체 구현하여 썼더니, 함수에 타입으로서 사용하려면 저런식으로 typeof와 keyof라는 파라미터를 조합해서 사용해야 한다. (사실 저렇게 쓰기 싫어서 Enum을 추가한 것이라고 이해해도 된다)
변환 문법에 대해선 다음 포스팅을 참고하도록 하자.
# 참고자료
https://heropy.blog/2020/01/27/typescript/
https://developer-talk.tistory.com/200
https://typescript-kr.github.io/pages/enums.html
이재승 타입스크립트 시작하기
제로초 타입스크립트 올인원
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.