올해 버려야 할 TS 나쁜 버른 10 가지
📄

올해 버려야 할 TS 나쁜 버른 10 가지

Created
May 1, 2021 02:07 PM
Tags
js
ts
performance
 
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한 값이 아니라 nullundefined 값만을 보정한다.
따라서 더운 명확하고, 실수 없이 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);
});
 
다만 이는 당장에 편할 수 있을지 몰라도 iduser_id 로 바뀐다거나,
createEmailTest()idemail 까지 반드시 요구하도록 명세가 바뀌게 된다면?
그럼 일일이 위와 같이 작성한 코드를 모두 변경해줘야 할 것이다.
 
따라서, 다음과 같이 재사용이 가능하게끔 코드를 작성하자.
 
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';
}
 
countOfNewMsgnumber 타입이다. 따라서 실제로 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';
}
 
!= nullnullundefined 를 동시에 검사하기에 편할 수도 있다.
그러나 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를 사용하게 되었는지 다시 한번 생각해보자.
 

Loading Comments...