TypeScript 알차게 활용하기
들어가며
최근 통합 테스트 과정을 거치면서 TypeScript를 잘 활용할수록 에러 발생률을 줄일 수 있음을 체감하였다. 그래서 TypeScript 활용 팁을 정리해보았는데, 이번 글에서는 Utility Types와 Enums의 활용 방법을 살펴보고자 한다.
Utility Type으로 간편하게 타입 정의하기
TypeScript는 다양한 Utility Type을 제공하고 있는데, 이를 사용하면 좀 더 간편하게 타입을 정의할 수 있다.
몇 가지 유용한 Utility Type을 추려서 정리하면 아래와 같다.
1) Partial
필수적이지 않은 property는 이름 뒤에 ?를 붙여서 optional한 속성값임을 표시할 수 있다.
그런데 모든 property가 optional한 경우 일일이 물음표를 붙여주는 것은 번거로운 작업이다.
이 때 Partial<Type>
을 사용하면 모든 속성이 optional한 타입을 간편하게 만들 수 있다.
interface User {
id: string,
email: string,
phone: string,
age: number,
}
type UserVo = Partial<User>;
// 아래 코드는 컴파일 단계에서 에러 발생
// "Property 'phone' is missing in type '{id: string; email: string; age: number;}', but required in type 'User'
const user: User = {
id: 'gildong.hong',
email: 'gdhong@nextree.io',
age: 20,
}
// 아래 코드는 에러가 발생하지 않음
const user: UserVo = {
id: 'gildong.hong',
email: 'gdhong@nextree.io',
age: 20,
}
2) Omit<Type, Keys>
특정 타입에서 몇 가지 속성만 제거하여 새로운 타입을 정의하고 싶을 때에는 Omit<Type, Keys>
를 사용하면 유용하다.
아래 코드로 예를 들어 살펴보자면 Student
타입은 Entity
타입을 상속한 Person
타입을 상속하고 있어 총 여덟 개의 속성을 가지고 있다.
그런데 Entity
의 속성은 제외하고 Person
과 Student
의 속성만 가진 타입을 정의하고 싶으면 어떻게 해야 할까?
물론 Person
과 Student
을 합친 여섯 가지 속성을 나열하여 새로운 타입을 정의할 수도 있다.
하지만 Person
과 Student
의 속성이 늘어날수록 위 과정은 번거로워지고, 코드는 비대해진다.
이러한 경우 Omit<Student, keyof Entity>
과 같이 타입을 정의하면 Entity
의 속성을 제거한 타입을 훨씬 간단하고 간결하게 정의할 수 있다.
interface Entity {
id: string,
registeredTime: number,
modifiedTime: number,
}
interface Person extends Entity {
name: string,
age: number,
gender: string,
}
interface Student extends Person {
className: string,
grade: number,
}
// Person과 Student의 속성을 합쳐 새로운 타입을 정의하는 방법 => Omit을 사용하는 경우보다 코드 양이 많음
// interface StudentInfo {
// name: string,
// age: number,
// gender: string,
// className: string,
// grade: number,
// }
type StudentInfo = Omit<Student, keyof Entity>;
const student: StudentInfo = {
name: "Gildong Hong",
age: 17,
gender: "male",
className: "A",
grade: 3,
}
3) Record<Keys, Type>
TypeScript에서는 Record<Keys, Type>
을 사용하여 Map 구조의 타입을 정의할 수 있다.
JavaScript의 index signature와 다른 점은 Key를 Union Type으로 정의할 수 있다는 것이다.
interface AddressBook {
name: string,
phone: string,
email: string,
}
type EmployeeName = "Kim" | "Lee" | "Park";
type EmployeeRecord = Record<EmployeeName, AddressBook>;
회사에 Kim, Lee, Park, 이렇게 세 명의 직원이 있다고 가정해보자.
Index Signature의 경우 직원 Park의 값이 빠져도 별도로 경고문을 띄우지 않는다.
// Index Signature
type EmployeeIndex = {[name: string] : AddressBook};
const employeesIndex: EmployeeIndex = {
"Kim": {name: "HJ Kim", phone: "010-0000-0000", email: "hjkim@nextree.io"},
"Lee": {name: "SK Lee", phone: "010-1111-1111", email: "sklee@nextree.io"},
}
반면, Record 타입은 특정 키의 값이 누락되면 컴파일 에러가 나면서 아래와 같이 경고문을 띄워준다.
따라서 모든 Key에 대한 값이 필수적인 경우 Record<Keys, Type>
을 사용하면 실수를 미연에 방지할 수 있다.
상황에 맞게 Enum 사용하기
enum은 서로 관련 있는 상수들을 모아 놓은 열거형 타입이다.
원래 JavaScript에는 enum 기능이 없지만, TypeScript에는 자체적으로 구현한 enum 기능이 있다.
이 기능을 사용하는 것이 좋은지 아닌지에 대해서는 개발자들 간의 의견차가 있는데,
TypeScript의 enum이 개발 편의성을 제공하는 동시에 몇 가지 제한점을 가지고 있기 때문이다.
이와 관련된 내용들을 살펴보자.
1) enum으로 의미 있는 값 매핑하기
enum은 각각의 key에 해당되는 value를 지정할 수 있어 편리하다.
국제전화의 경우 1, 82 이렇게 국가번호만 나열되어 있으면 어느 나라의 것인지 확인하기 어렵다.
따라서 아래 코드와 같이 각각의 국가번호에 국가명을 key로 붙여주면 해당되는 값을 찾기가 훨씬 편리하다.
enum PhoneCode {
UnitedStates = "1",
France = "33",
Japan = "81",
Korea = "82",
}
console.log(PhoneCode.Korea); // 82
**2) enum으로 오타 방지하기**
enum 기능을 사용하면 허용 가능한 값을 제한할 수 있고, 자동 완성 / 일괄 수정 등 IDE에서 지원하는 다양한 기능을 사용할 수 있다.
enum을 미리 정의해두면 아래와 같이 코드를 자동 완성해주기 때문에 오타로 인한 에러를 방지할 수 있다.
3) enum으로 key, value 순회하기
enum의 key에 대한 value를 string으로 지정하면 key와 value를 순회하며 사용할 수 있어 편리하다.
enum PhoneCode {
UnitedStates = "1",
France = "33",
Japan = "81",
Korea = "82",
}
// "UnitedStates", "France", "Japan", "Korea"
Object.keys(PhoneCode).forEach((country) => {
console.log(country);
});
// "1", "33", "81", "82"
Object.values(PhoneCode).forEach((phoneCode) => {
console.log(phoneCode);
});
한편, key에 대한 value를 별도로 지정하지 않으면, 첫 번째 key의 값을 0으로 하여 차례대로 1, 2, 3…의 값이 할당된다.
이때 유의해야 할 점은 enum의 value를 number로 지정하면 순회가 정상적이지 않은 방식으로 작동한다는 것이다.
Object.keys()
를 사용했을 때는 key 값만, Object.values()
를 사용했을 때는 value 값만 나와야하지만 numeric enum에서는 key와 value가 섞여 나오는 문제가 있다.
enum PhoneCode {
UnitedStates = 1,
France = 33,
Japan = 81,
Korea = 82,
}
// ["1", "33", "81", "82", "UnitedStates", "France", "Japan", "Korea"]
console.log(Object.keys(PhoneCode));
// ["UnitedStates", "France", "Japan", "Korea", 1, 33, 81, 82]
console.log(Object.values(PhoneCode));
4) enum? union type? const enum?
웹 성능 최적화의 관점에서 enum
보다는 union type
이나 const enum
을 사용하는 것이 권장되기도 한다.
enum은 JavaScript에는 없는 기능이기 때문에 JavaScript 코드로 컴파일 되면 아래와 같이 즉시 실행 함수(IIFE, Immediately Invoked Function Expression)를 포함한 형태가 된다.
var PhoneCode;
(function (PhoneCode) {
PhoneCode["UnitedStates"] = "1";
PhoneCode["France"] = "33";
PhoneCode["Japan"] = "81";
PhoneCode["Korea"] = "82";
})(PhoneCode || (PhoneCode = {}));
번들러는 번들링을 할 때 사용되지 않는 코드를 제거하는 tree-shaking 과정을 거치는데,
TypeScript의 enum은 즉시 실행 함수로 인해 실제로 사용되지 않음에도 불구하고 tree-shaking 과정에서 제거되지 않는 경우가 있다.
따라서 아래와 같이 tree-shaking이 가능한 union type
이나 const enum
을 사용하는 것이 권장되기도 한다.
하지만 const enum
은 key와 value에 대한 순회가 불가능하고, union type
은 상대적으로 코드 가독성이 떨어진다는 단점이 있다.
// Union Type
const PhoneCode = {
UnitedStates : "1",
France : "33",
Japan : "81",
Korea : "82",
} as const;
type PhoneCode = typeof PhoneCode[keyof typeof PhoneCode];
// const enum
const enum PhoneCode {
UnitedStates = "1",
France = "33",
Japan = "81",
Korea = "82",
}
한편, TypeScript의 enum이 tree-shaking 측면에서 비효율적인지 여부는 번들러의 종류나 버전에 따라 달라진다.
rollup 3.9.1 버전에서 테스트해보면 import 했지만 실제로는 사용하지 않는 enum이 tree-shaking 과정에서 제거되지 않고 그대로 남아 있는 것을 확인할 수 있었다.
반면, webpack 5.75.0 버전과 vite 4.0.3 버전에서 테스트 해보았을 때에는 import했지만 사용하지 않는 enum이 tree-shaking 과정에서 잘 제거됨을 확인할 수 있었다.
따라서 TypeScript enum을 사용할지 여부는 번들러의 종류 및 버전을 고려하여 결정하는 것이 적합할 것이다.