enum
enum의 개념
enum은 열거형 타입(Enumerated Type)을 앞 글자를 따와서 만들어진 단어이다. Javascript에는 존재하지 않는 Typescript 만의 몇 안되는 기능이다. Typescript에서는 숫자기반 뿐만 아니라 문자열 기반의 열거형까지도 지원한다.
enum의 대표적인 예로 boolean Type을 생각할 수 있다.
일반적으로 JS에서 숫자 1은 True에, 숫자 0은 False에 대응되는데, 일종의 Built-in enum인 셈이다.
enum booleanType {
False = 0,
True = 1
}
위 코드를 JS로 컴파일하면 아래와 같이 바뀐다.
"use strict";
var booleanType;
(function (booleanType) {
booleanType[booleanType["False"] = 0] = "False";
booleanType[booleanType["True"] = 1] = "True";
})(booleanType || (booleanType = {}));
코드가 복잡해 보일 수 있지만, 위 코드는 결국 아래 코드와 같은 객체를 반환하는 즉시실행함수를 구현했을 뿐이다.
var booleanType = {
"0": "False",
"1": "True",
"False": 0,
"True": 1
}
즉, enum은 JS로 컴파일되는 과정에서 일종의 필요충분조건처럼 Key-Value의 관계가 양방향으로 구현된다.
따라서 콘솔에 찍어보면 다음과 같은 결과를 얻을 수 있다.
console.log(booleanType['False']) //0
console.log(booleanType['True']) //1
console.log(booleanType[0]) //"False"
console.log(booleanType[1]) //"True"
단, 주의할 점은 enum의 Member name, 그러니까 Key 값으로 Numeric Type이 올 수는 없다. 즉, 아래와 같이 작성하면 에러를 볼 수 있다.
const enum booleanType {
1='True', //An enum member cannot have a numeric name.
'0'='False', //An enum member cannot have a numeric name.
}
enum의 장점
사실 enum은 JS에 없을 뿐이지, C++, Java, Python 등의 언어에서는 이미 지원되고 있는 기능이다. 많은 언어에서 enum을 지원하는 이유는 아래와 같은 장점 때문이다.
1. 생산성과 가독성이 높아진다.
enum이라는 키워드를 보는 순간 코드 작성자의 의도를 파악할 수 있고, Key-Value의 양방향 관계를 간단히 구현할 수 있기 때문에 생산성이 증대된다. 참고로 Key-Value의 양방향 관계는 서로 연관된 상수들의 쌍을 하나의 식별자(객체)에 묶어서 취급하고 싶을때 주로 활용된다.
2. 정의한 Key-Value 관계만 성립할 수 있기 때문에 예기치 못한 에러를 방지할 수 있다.
앞서 말한 것처럼 Key-Value가 양방향이 되면, 사실상 필요충분조건으로 작동하기 때문에, Value의 값으로 다른 값이 올 수 없게 되어 마치 readonly처럼 작동한다.
enum의 단점
1. 컴파일시 코드의 양이 증가한다.
만약, Key-Value의 양방향 정의가 굳이 필요하지 않다면, 양방향 JS코드로 구현되는 특징은 단점이 된다.
2. Tree-shaking이 되지 않는다.
위 단점의 연장선이긴 한데, 좀 더 강조하고자 따로 빼어서 다뤄본다.
Tree-shaking이란, 나무를 흔들어서 죽은 잎사귀나, 상태가 좋지 못한 나뭇가지 등을 떨어뜨려 나무를 정리하는 모습에서 차용한 표현이다. 그렇기 때문에, 개발에서 Tree-shaking이란 사용하지 않는 코드를 제거하여 코드를 가볍게 만드는 최적화 과정을 말한다.
이는 보통 컴파일 단계에서 수행되는데, 앞서 살펴본 것처럼, 컴파일 과정에서 enum은 IIFE(즉시 실행 함수)로 컴파일 되기 때문에 enum의 구현과정에서 코드는 자체적으로 쓰이게 된다. 따라서 enum을 선언만 해두고 사용하지 않는다하더라도 실제 컴파일된 코드에서는 enum 코드가 살아 있게 된다.
enum Member의 종류 : Value의 확정성에 따라 구분
Typescript의 enum의 Member에는 Constant Member와 Computed Member 두 개가 존재한다.
전자는, 어떤 고정된 값을 갖는 Member를 의미하고, 후자는 어떤 프로세싱(compute)를 통해 도출된 값을 갖는 Member를 의미한다.
enum의 종류 : Value의 Type에 따라 구분
- 숫자형 (Numeric) : Member의 Value가 숫자인 enum
- 문자열 (String) : Member의 Value가 문자열인 enum
- 혼합형 (Heterogeneous) : Member의 Value가 숫자와 문자열이 섞인 enum
enum의 자동 할당기능
첫 번째 Member의 경우, 초기화되지 않는다면 자동으로 Key의 Value는 0이 된다.
또한, 어떤 Member가 초기화 되지 않았지만, 바로 이전 Member가 숫자 상수였다면, 해당 Member는 '이전 Member의 값 +1' 을 값으로 갖는다.
참고로 Member는 Key-Value의 양방향 관계 그 자체를 일컫는다고 이해하면 된다. Key와 같은 의미가 아닌가 싶을 수 있지만, 엄밀히 말하면 조금 다른 개념이다. Key보다 더 상위 개념.
enum DirectionNoInit {
Up, //0이 할당됨
Down, //1이 할당됨
Left, //2가 할당됨
Right, //3이 할당됨
}
enum DirectionFirstMemberInit {
Up = 1,
Down, //2가 할당됨
Left, //3이 할당됨
Right, //4가 할당됨
}
const enum
enum의 단점을 개선하고자 존재하는 개념이 바로 const enum이다. Typescript 공식사이트에서는 아래와 같이 설명하고 있다.
In most cases, enums are a perfectly valid solution. However sometimes requirements are tighter. To avoid paying the cost of extra generated code and additional indirection when accessing enum values, it’s possible to use const enums. Const enums are defined using the const modifier on our enums.
그럼 공식 사이트의 예제를 살펴보자.
const enum Enum {
A = 1,
B = A * 2,
}
// .JS
"use strict";
enum Enum {
A = 1,
B = A * 2,
}
// .JS
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 1] = "A";
Enum[Enum["B"] = 2] = "B";
})(Enum || (Enum = {}));
위는 const enum이고 아래는 enum이다. 동일한 코드를 작성했음에도 const modifier를 붙인다면 JS로의 컴파일 후 그 어떠한 코드도 남지 않는 것을 볼 수 있다. 오 완전 신기하다!!
그렇다면 실제 코딩시 enum 대신 const enum 을 쓰면 되는걸까? 당연히 아니다. 똑같은 기능을 수행한다면 굳이 enum과 const enum을 구분하여 사용하고 있을 이유가 없다.
심지어 때에 따라서는 const enum을 쓰지 않을 것을 권장하기도 한다.(const enum의 함정)
const enum의 특징
Const enums can only use constant enum expressions and unlike regular enums they are completely removed during compilation. Const enum members are inlined at use sites. This is possible since const enums cannot have computed members.
우선 const enum은 Computed Member를 갖지 못하고, 오직 Constant Member만 가질 수 있다. 따라서, Value로는 String이나 숫자형 같은 정적인 값(원시값)만 가능하다. 이는, 컴파일시 코드가 사라져버리기 때문에 Constant하지 않은 Member는 갖지 못한다고 이해할 수 있다.
하지만 무엇보다도 가장 주목해야할 부분은 'Const enum members are inlined at use sites.' 부분이다.
Inline에 관하여
Inline 개념에 대해 여기서 길게 설명하긴 곤란하니, 다음에 기회가 된다면 상세한 포스팅을 하는 걸로 하고, 여기서는 핵심만 언급하겠다. Inlining을 하면, 해당 코드가 호출되는 곳에 곧장 Inlining 대상 코드를 치환하여 컴파일하게 된다.
나무위키 (인라인함수)의 예를 보자.
(갑자기 C++ 코드가 나와서 당황할 순 있지만.. stdio.h 같은건 무시하고 큰 틀에서의 코드만 보면 된다.)
#include <stdio.h>
inline void print()
{
printf("Hello, world!\n");
}
int main()
{
print();
return 0;
}
inline 처리된 함수 print()가 main() 함수에서 호출되고 있다. 이를 컴파일하면 아래와 같이 코드가 바뀐다.
// 컴파일시
#include <stdio.h>
int main()
{
printf("Hello, world!\n");
return 0;
}
보시다시피, main() 함수에서 print() 함수 부분에 print() 함수의 코드가 치환되어 들어가 있다. 이렇게 되면 우리는 함수의 호출 비용이나, 컴파일 후의 코드 양을 줄이는 등 성능 측면의 최적화를 이끌어 낼 수 있다. 하지만, 장점만 존재하는 것은 아니다. 인라인 함수는 말그대로 함수 호출 과정을 제거하는 것이므로, 함수 호출 과정을 통해 얻게 되는 부가적인 이점이 사라지게 된다.
Typescript의 const enum 과 inline
다시 Typescript의 const enum으로 돌아와서, 공식사이트의 예제를 살펴보자.
const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
// .JS
"use strict";
let directions = [
0 /* Direction.Up */,
1 /* Direction.Down */,
2 /* Direction.Left */,
3 /* Direction.Right */,
];
위 코드에서 .JS에 적힌 각주까지가 컴파일의 결과이다. 즉, Direction 이라는 enum이 Inlined 되어 처리되었음을 친절히 알려주는 각주까지 자동으로 생성된다.
이 때, string literal이 아닌 방법으로 enum을 참조하려고하면 아래와 같은 에러가 뜨니까 주의하자.
A const enum member can only be accessed using a string literal.
어쨌거나, 이상의 논의를 통해 const enum 은 컴파일 단계에서 enum이 사용되는 곳에 직접적으로 inline되기 때문에, enum자체의 구현 코드는 남아있지 않게된다는 것을 알 수 있다.
const enum의 쓰임
그렇다면 const enum은 도대체 언제 쓰는 걸까? const enum이 어디에 주로 쓰이는지 알기 위해서는 다음과 같은 몇가지 특징을 잘 떠올려보면 도움이 될 것이다.
- enum과 다르게 Key-Value의 관계가 양방향이 되지 않는다.
- inlined 되기 때문에, 코드가 가벼워지고, Tree-shaking도 가능하다.
- inlined 되기 때문에, 사실상 readonly와 같이 동작한다.(수정 불가능)
이러한 특징 때문에, 굳이 Key-Value간의 양방향 Mapping이 필요 없는 상황에서, Object의 Key로 접근하여 Value만을 가져와서 그 값을 Type으로 정해주고 싶을 때 주로 사용된다. 그냥 Value 값을 받아오는게 아니라, 그 Value 값을 Type으로 정해주기 위해서 const enum과 같은 문법이 필요한 것.
하. 지. 만.
앞서 잠깐 언급한 const enum pitfall(Good bye, TypeScript enum) 문제로 인해 const enum의 활용은 그리 권장되진 않는다. 그래서 3.4 버전 이후의 모던 타입스크립트에서부터는 as const 가 const enum의 역할을 완전히 대체하는 추세이다.
as const
as const 는 Typescript 3.4 버전에서 추가된 const assertion 기능을 활용하기 위한 문법적 표현이다.
const assertion의 말 뜻은 '상수라는 주장'인데, 이 뉘앙스로부터 '원래 상수가 아닌 것을 상수인 것으로 선언하는 주는 기능이다' 라고 유추해 볼 수 있다.
그렇다면, 상수가 아닌 것을 왜 상수로 만들어야 할까? 이 의문을 해결하려면 상수와 변수에 대한 Typescript적 이해에서부터 출발해야 한다.
let과 const의 Typescript적 이해
위 두 사진을 통해서, let으로 선언하면 할당된 값의 Type에서 추론해오지만, const의 경우 할당된 값 그 자체를 Type으로 받아온다는 점을 알 수 있다. 그렇기 때문에, const로 선언된 변수에 다른 값을 재할당하게 되면 Type 에러가 뜬다.
참고로 할당된 특정 값 그 자체를 Type으로 취급하는 것을 Literal Type이라고 부른다. 특정 문자열을 Type으로 취급한다면 String Literal Type이 되는 식이다. 'const enum의 쓰임' 목차에서 말한 내용도, 축약하자면 Literal Type을 편하게 지정해주기 위해서 쓴다고 말할 수 있다.
그렇다면 let으로 선언한 변수에 대해서 as const 문법으로 '이건 const야!' 를 주장하면 어떻게 될까?
const로 선언한 것과는 좀 다른 에러 메세지를 띄우긴하지만, 원리적으로는 동일하다. let으로 선언되었든 말든간에 as const 문법을 붙여주었기 때문에 Typescript는 wantToLearn을 const로 인식하게 되고, 따라서 'NLP'라는 Type만이 올 수 있다고 받아들이게 되는 것을 볼 수 있다.
Object의 Value는 상수가 아니다
근데 사실 let을 굳이 as const를 써서 const로 바꿀 이유는 없다. 그냥 const 키워드로 변수를 선언하면 되니까 말이다.
자 그럼 이 쯤에서 처음 질문으로 돌아가보자. '상수가 아닌 것을 왜 상수로 만들어야 할까?'에서 이 논의는 출발하였다. 그러면 '상수가 아닌 것'을 좀 더 파고 들어봐야한다.
Object의 Value는 상수가 아니다. 다시 말해, 특정 Object에 특정 Key가 존재해야 한다고 지정해줄 수는 있지만, 특정 Key에 와야만 하는 Value를 구체적으로 지정할 수는 없다는 뜻이다. 이는 Object가 본디, 구조화된 데이터 형태로서 가변적인 데이터 값을 저장해야 하기 위해 탄생했기 때문이다.
하지만 Object를 Object 본래 용도가 아닌 enum스럽게 활용하고 싶을 때, 이러한 가변성이 발목을 잡게 된다.
따라서, 이럴 때 쓰는 것이 바로 as const 문법이다.
참고로 Class, Array 등도 JS에서는 결국 Object이기 때문에 위 논의는 공통으로 적용된다.
as const 를 설정하지 않은 Object
위와 같이, 속성의 값으로 어떤 값이 와 있든, 그 값을 통해 추론된 Type이기만 하면 Typescript는 에러를 띄우지 않게 된다. 따라서 아래와 같이 제멋대로 Value 값을 바꿀 수 있다.
as const 를 설정한 Object (1) : 개별 속성에 대한 설정
반면, as const 문법을 통해 상수 주장을 한다면 어떨까?
first의 Type이 "NLP"라는 String Literal Type으로 설정되어 있다.
따라서 아래와 같이 Type Error가 뜨게 된다.
as const 를 설정한 Object (2) : Object 자체에 설정
한편, as const는 객체 차원에서 설정해줄 수도 있다. 이런 경우 readonly 옵션이 자동으로 붙게 된다.
비록 Type Error 가 아니라, readonly 값은 변경할 수 없다는 에러를 띄우긴 하지만, 본질은 동일하다. readonly가 붙지 않아도, 사실 String Literal Type 자체가 readonly의 기능을 하기 때문이다. 단지, readonly를 붙여서 readonly로 에러처리를 하는 이유는 Typescript의 컴파일 성능과 관련된 기술적 이유 때문이라고 생각하면 된다.
응용 (1) - Discriminated Unions 와 함께 생각해보기
const myCar = {
numberOfTires: 4,
brand: 'hyundai',
ev: 'EV'
};
const myBike = {
numberOfTires: 2,
brand: 'BMW',
};
type Transportation = typeof myCar | typeof myBike;
const myTransportation = (transportation: Transportation) => {
(transportation.numberOfTires === 4)
? console.log(`My Car Brand is ${transportation.brand} and It is the ${transportation.ev}!`)
: console.log(`My Bike Brand is ${transportation.brand}`)
}
위 코드와 같이, 타입에 따라 서로 다른 속성을 갖는 경우, Typescript는 에러를 띄우게 된다.
그 이유는 바로 존재할 수도 있고 존재하지 않을 수도 있다고 인식하기 때문. 이 문제를 해결하려면 Discriminated Unions 의 개념을 알아야한다.
type Shape =
| { kind: "circle"; radius: number } // 가장 첫 번째이므로 | 생략가능
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
function area(s: Shape) {
if (s.kind === "circle") {
return Math.PI * s.radius * s.radius;
} else if (s.kind === "square") {
return s.x * s.x;
} else {
return (s.x * s.y) / 2;
}
}
Shape 부분이 바로 Discriminated Unions 이다. 참고로 가장 첫 번째 ' | '는 생략가능하다.
위 문법으로 코드를 작성하면 특정 프로퍼티의 값을 통해 어떤 type을 참조해야하는지 Typescript가 판별가능하다고 해서 판별(Discriminated) 유니언이라고 부른다.
따라서, Discriminated Unions으로 인식되면, string으로 작성된 모든 Value는 String Literal Type으로 처리된다.
자 다시 원래 예제로 돌아오자.
type Transportation을 우리는 Discriminated Union으로 쓰고 싶은 상황이다. 하지만 여기서 myCar(myBike도 같다)의 numberOfTires가 number라는 Type으로 인식되고 있다.
앞서 우리는 as const 문법을 쓴다면, Value Type이 Literal Type으로 처리된다는 것을 보았다. 따라서 as const를 numberOfTires에 붙여주자.
numberOfTires의 Value Type이 Literal Type이 된 것을 볼 수 있다. 그와 함께 Error 역시도 자연스럽게 사라진 것을 볼 수 있다. 참고로 as const 역시 readonly의 효과가 있기 때문에 추후 값의 재할당은 불가능하다.
이 때, myCar 와 myBike Object 자체에 as const를 해줘도 된다. 더 나아가 둘 중 하나에만 as const를 해줘도 된다. 이는 Discriminated Union의 특성인데, myCar라면 myBike가 아닐 것이고 myBike라면 myCar가 아닐 것이기 때문에 어느 하나만 판별할 수 있어도 나머지 경우를 고려할 수 있기 때문이다. (배타적 선언)
응용 (2) : const enum을 대체하기 위한 코드 테크닉
// .TS
const booleanType = {
False: 0,
True: 1,
} as const;
type ForReplaceEnum =
typeof booleanType[keyof typeof booleanType];
enum으로 구현했던 booleanType을 as const로도 구현해보았다.
enum과 다른 점은 양방향 Mapping이 이루어지지 않는다는 점이다. 위 코드를 컴파일하면 아래와 같은 JS코드를 얻게 된다.
// .JS
const booleanType = {
False: 0,
True: 1,
};
뭐야? 주인장, as const는 도대체 왜하는거야? 싶을 수 있다. 위와 같이 코드를 작성하는 이유는 소제목에 밝혔지만, const enum을 대체하기 위함이다.
const enum의 예제로 다시 올라가보면, 결국 우리는 어떤 Object의 Value를 받아와서 Literal Type으로 지정해주는 코드를 보다 편하게 구현하기 위해 const enum이라는 개념을 필요로 했다.
다시 as const 예제로 돌아와서, type ForReplaceEnum을 뜯어보자. 이 코드는 booleanType의 typeof의 keyof를 Key로 갖는 booleanType의 typeof 를 의미한다. (...) 당황하지말자.... 하나씩 짚어나가면 별 것 아니다.
우선 booleanType의 typeof는 object이다. 그리고 이 object의 keyof는 바로 False와 True 이다.
이제 booleanType[False]의 typeof 와 booleanType[True]의 typeof가 바로 ForReplaceEnum의 Type이 된다.
따라서, type ForReplaceEnum = 0 | 1 이 된다.
이런 방법을 통해, const enum을 써서 얻고자 했던 목적을 달성할 수 있다. 비록 inline은 포기해야하지만..
결론 : enum 과 as const 중에서 목적에 맞는 선택을 하자
enum 의 탄생 배경은 앞서 말한 것처럼 어떤 Key-Value의 관계를 양방향으로 선언해주기 위함이다. 이를 통해 하나의 식별자에 이들 관계성을 저장하여 추상화할 수 있다는 것이 큰 특징이다.
enum의 단점으로, 사용하지 않는 코드도 존재할 수 있다는 비효율성을 지적했지만, 거꾸로 말하면, enum을 선언해두고 잘 활용해먹기만 한다면 크게 문제되지 않는다고도 생각가능하다.
따라서, 양방향 Mapping이 필요하고, 잘 써먹을 자신만 있다면 enum을 쓰는 것이 좋다. enum이라는 키워드를 보는 순간 개발자의 의도를 바로 짚어낼 수 있다는 점은 네이밍 부담을 줄여주는 아주 강력한 장점이다.
반면, const enum은 여러 이유에서 배척되고 있는게 현 주소이다. 그러니 const enum을 쓰기보다는 as const를 쓰는 것을 추천한다. const enum의 가장 큰 특징이자 장점이 inlined 이지만, 바로 이 점 때문에 수많은 단점이 파생되고 있기 때문에 차라리 inlined를 포기하고 as const를 쓰는 것을 권장한다.
as const는 앞서 살펴본 바와 같이 Type의 추론 범위를 줄여서 해당 값 자체를 Type으로 만들어준다. 비록 양방향 바인딩을 해주진 못하지만, 상수가 아닌 것을 상수로 만들어 주기 때문에, 이를 활용한 여러가지 코드 테크닉을 쓸 수 있고, 실질적으로 const enum 문법을 대체하고 있는 중이다.
따라서 양방향 Mapping이 굳이 필요 없고, Value만 받아와서 Literal Type으로 지정하고 싶다면 as const 문법을 쓰면 충분할 것이고, 양방향 Mapping이 필요하다면 enum을 쓰면 되겠다. const enum? 이건 그냥 잊어버리자.
'Learning-Log > Computer Science' 카테고리의 다른 글
[NextJS] ReactJS를 품은 프레임워크, NextJS를 알아보자 (0) | 2022.08.08 |
---|---|
[도서/AI] '구글 브레인 팀에게 배우는 딥러닝 with TensorFlow.js' 개발자 리뷰 (0) | 2022.08.07 |
[WSL2] Vmmem의 RAM 점유율 해결 방법 (0) | 2022.07.17 |
[ReactJS] react-router의 useParams 에 대해 알아보자 (0) | 2022.07.16 |
[JS] 모듈을 받아오는 import와 모듈을 내보내는 export (0) | 2022.07.15 |