TS가 지속적으로 진화하며 여러 가지가 바뀌었다. 뭐 애초에 의미가 없었던 것도 있었고.
여기서는 TS를 사용하며 반드시 고쳐야 할 나쁜 습관 10 가지를 보도록 하겠다.
참고로 언급하는 "올바른 코드"란, 논의되었던 이슈에 대해서만 해결하는 것을 목적으로 하고 있다.
따라서 다른 종류의 Code smells가 포함되었을 수 있으나, 이는 논외로 하도록 하겠다.
Bad habits 1 :: Not using strict
mode
tsconfig.json
을 보면, 다음과 같이 strict
모드를 사용하지 않는 경우가 있다.{ "compilerOptions": { "target": "ES2015", "module": "commonjs" } }
이렇게 하는 것은 좋지 않다.
다음과 같이
strict
모드를 설정하도록 한다.{ "compilerOptions": { "target": "ES2015", "module": "commonjs", "strict": true } }
물론
strict
로 인해 '간혹' 불편한 상황이 발생될 수 있겠으나...타입 체킹을 하지 않을 것이라면 왜 TS를 사용하나?
적어도 Type safety를 위해 TS를 사용하는 경우라면 반드시
strict
를 사용하도록 하자.Strict mode를 사용함으로써 코드를 쉽게 파악하고 수정할 수 있도록 구성할 수 있기에,
이 과정을 진행하며 소모되었던 시간들은 향후 실제로 코드를 파악하고 수정할 때 돌려받게 될 것이다.
Bad habits 2 :: Defining default values with ||
기존에는
||
를 이용해 Default value를 설정하곤 했을 것이다.function createBlogPost(text: string, author: string, date?: Date) { return { text, author, date: date || new Date(), }; }
앞으로는 이 대신
??
를 사용하도록 하자.function createBlogPost(text: string, author: string, date?: Date) { return { text, author, date: date ?? new Date(), }; }
물론 다음과 같이 Params level에서 Default value를 정의해 줄 수도 있다.
function createBlogPost(text: string, author: string, date: Date = new Date()) { return { text, author, date, }; }
근데, 굳이 왜? 무슨 차이가 있길래 이러는 것일까.
??
는 ||
와 달리 Falsy한 값이 아니라 null
과 undefined
값만을 보정한다.따라서 더운 명확하고, 실수 없이 Default value operator 사용이 가능하게 된다.
참고로
??
는 TS 뿐만 아니라 JS에서도 사용이 가능.Bad habits 3 :: Using any
as type
종종 사용하는 데이터의 구조를 파악하기 힘든 경우
any
를 사용하곤 하는데...async function loadProducts(): Promise<Product[]> { const resp = await fetch('https://api.mysite.com/products'); const products: any = await resp.json(); return products; }
이 대신 다음과 같이
unknown
을 사용하도록 한다.async function loadProducts(): Promise<Product[]> { const resp = await fetch('https://api.mysite.com/products'); const products: unknown = await resp.json(); return products as Product[]; }
또 왜?
일단
any
는 기본적으로 모든 Type checking을 무력화시킨다.따라서 런타임 시에만 오류가 발생되며, 이는 버그를 찾기 힘들게 하기 때문.
Bad habits 4 :: val as SomeType
바로 위에서 사용했던, 타입을 강제로 추론하게 하는
as
대신에async function loadProducts(): Promise<Product[]> { const resp = await fetch('https://api.mysite.com/products'); const products: unknown = await resp.json(); return products as Product[]; }
Type guard를 사용하도록 한다.
async function loadProducts(): Promise<Product[]> { const resp = await fetch('https://api.mysite.com/products'); const products: unknown = await resp.json(); if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response'); } return products; } function isArrayOfProducts(obj: unknown): obj is Product[] { return Array.isArray(obj) && obj.every(isProduct); } function isProduct(obj: unknown): obj is Product { return obj != null && typeof (obj as Product).id === 'string'; }
JS에서 TS로 마이그레이션 하는 과정에서 종종
as
를 사용했을 수 있는데,뭐 당장에는 괜찮아 보일 수는 있어도...
향후 누군가 코드를 이동하거나 할 때 예기치 못한 상황이 발생될 수 있기 때문.
따라서 위와 같이 Type gurad를 이용해 모든 것을 명시적으로 검증하도록 하자.
Bad habits 5 :: as any
in Tests
Test 작성 시, 인스턴스의 일부 프로퍼티만을 이용하는 경우에는
as any
를 이용할 수 있다.interface User { id: string email: string } test('createEmailText returns text that greats the user by id', () => { const user: User = { id: 'John', } as any; expect(createEmailText(user)).toContain(user.id); });
다만 이는 당장에 편할 수 있을지 몰라도
id
가 user_id
로 바뀐다거나,createEmailTest()
가 id
랑 email
까지 반드시 요구하도록 명세가 바뀌게 된다면?그럼 일일이 위와 같이 작성한 코드를 모두 변경해줘야 할 것이다.
따라서, 다음과 같이 재사용이 가능하게끔 코드를 작성하자.
interface User { id: string email: string } class MockUser implements User { id = 'id' email = 'email@email.com' } test('createEmailText returns text that greats the user by id', () => { const user = new MockUser(); expect(createEmailText(user)).toContain(user.id); });
Bad habits 6 :: Optional properties
프로퍼티가 있을 수도 있고, 없을 수도 있다면 optional로 만드는 방법이 있는데,
interface Product { id: string type: 'digital' | 'physical' weightInKg?: number sizeInMb?: number }
물론 코드량도 적어지고, 편하고, 쉬운 방법은 맞다.
그러나 이를 위해서는
Product
interface에 대한 이해가 필요하며,또 나중에라도
Product
구조가 변경되는 경우 Optional proeprties를 쉽사리 건들 수 없게 될그런 가능성이 있다.
따라서 다음과 같이 '명시적으로' 구성한다.
interface Product { id: string type: 'digital' | 'physical' } interface DigitalProduct extends Product { type: 'digital' sizeInMb: number } interface PhysicalProduct extends Product { type: 'physical' weightInKg: number }
이렇게 구현하면 코드가 조금 길어질 수는 있겠으나, 컴파일 시 타입 검증이 가능하다는 장점이 있다.
Bad habits 7 :: One letter generics
자바에서도 그러했듯이, Generic을 문자 하나로 네이밍하곤 했을텐데
function head<T> (arr: T[]): T | undefined { return arr[0]; }
이러지 말자. 왜? Generic type value도 결국 '변수'기 때문.
뭐 한 두개 정도야 어렵지 않게 의미를 파악할 수는 있겠지만, 그 이상이 된다면 말이 달라진다.
다음의 코드를 보자. 위 코드와 Generic naming만 다를 뿐이다.
function head<Element> (arr: Element[]): Element | undefined { return arr[0]; }
자, 'Generic type values' 이다.
일반적으로, 의미가 있는 변수를 선언할 때 이름을
a
, s
이렇게 작성하지는 않을 것이다.Generic 역시 의미를 쉽게 파악할 수 있도록 구성해주도록 하자.
Bad habits 8 :: Non-boolean checks
number
와 같이 Non-boolean 값을 그냥 boolean
처럼 사용하지 않는다.function createNewMsgResponse(countOfNewMsg?: number): string { if (countOfNewMsg) { return `you have ${countOfNewMsg} new messages`; } return 'Error: could not retrieve number of new messages'; }
countOfNewMsg
는 number
타입이다. 따라서 실제로 undefined
인지 검사하도록 한다.또한 위와 같이 구현해버리면
createNewMsgResponse
함수는 0
을 구별하지 못하고 넘어갈 것이다.function createNewMsgResponse(countOfNewMsg?: number): string { if (countOfNewMsg !== undefined) { return `you have ${countOfNewMsg} new messages`; } return 'Error: could not retrieve number of new messages'; }
Bad habits 9 :: The Bang-bang operator
가끔 보면 Non-boolean을
boolean
으로 변환하기 위해 !!
를 사용하는 것을 마주하는데function createNewMsgResponse(countOfNewMsg?: number): string { if (!!countOfNewMsg) { return `you have ${countOfNewMsg} new messages`; } return 'Error: could not retrieve number of new messages'; }
이러지 말자. 편한 것은 맞다. 그러나
countOfNewMsg := 0
인 경우 의도치 않은 결과가 초래된다.function createNewMsgResponse(countOfNewMsg?: number): string { if (countOfNewMsg !== undefined) { return `you have ${countOfNewMsg} new messages`; } return 'Error: could not retrieve number of new messages'; }
마찬가지로, 무엇을 확인할 것인지 명확하게 검사하도록 하자.
참고로
!!null
!!''
!!undefined
!!false
모두 false
를 반환한다.Bad habits 10 :: != null
마지막으로...
!= null
사용하지 말자.function createNewMsgResponse(countOfNewMsg?: number): string { if (countOfNewMsg != null) { return `you have ${countOfNewMsg} new messages`; } return 'Error: could not retrieve number of new messages'; }
!= null
은 null
과 undefined
를 동시에 검사하기에 편할 수도 있다.그러나
null
은 '아직 값이 할당되지 않은 것'이고, undefined
는 '선언되지 않은 것'이다.가령,
user.firstName === null
은 '유저의 이름이 없는 것'이고,user.firstName === undefined
는 'firstName
프로퍼티가 존재하지 않는 것'이다.이 둘의 차이 역시 명확하게 구분하여 검사하도록 한다.
function createNewMsgResponse(countOfNewMsg?: number): string { if (countOfNewMsg !== undefined) { return `you have ${countOfNewMsg} new messages`; } return 'Error: could not retrieve number of new messages'; }
끝으로
왜 이렇게 귀찮게 구느냐?
물론 프로젝트가 작고, 모든 Side-effects를 완벽하게 파악하고 있으며, 혼자 개발한다면 상관 없다.
그러나 한 번이라도 상용 웹 서비스 개발에 참여해봤다면 알겠지만 현실은 녹록치가 않다.
당장에 귀찮고 시간이 없다고 위와 같은 사항들을 하나 둘 씩 무시하고 넘어가다 보면...
자신도 모르게 어느 순간 스파게티 코드가 되어버릴 것이다.
그럼 그 다음은 어떻게 되냐고? 결과야 뻔하다. 당장에 생각나는 것도 한무더기.
- TS를 사용했음에도 런타임 시 알 수 없는
TypeError
가 발생
- 어떤 Side-effects가 발생될지 몰라 버그가 있는 코드를 우회하는 방식으로 수정
- 리팩터링조차 불가능하게 되어 프로젝트를 갈아엎어야 하는 상황 발생
그럼에도 이해가 잘 되지 않는다면 왜 TypeScript를 사용하게 되었는지 다시 한번 생각해보자.