[Typescript] Template Literal Type의 빛과 어둠 (Feat. Date String)

NinjaJuunzzi·2022년 12월 4일
21

typescript

목록 보기
1/1
post-thumbnail

안녕하세요. 준찌입니다. 이번 포스팅에서는 Template Literal Type 문법으로 타입을 작성했을 때 얻을 수 있는 장점과 단점에 대해 알아보려고 합니다. 해당 개념에 대해 간략히 소개하고, Date String 타입을 Template Literal Type으로 커버해 봄으로써 Template Literal Type 으로 타입 작성하기 의 장, 단점을 알아보려고 합니다. 재미있게 봐주세요!

TL;DR

  • Template Literal Type 문법을 통해 형식에 맞는 문자열만 허용하도록 할 수 있다.
  • 따라서 타입 Safety한 어플리케이션을 작성할 수 있다.
  • 하지만 복잡한 타입은 TS Server의 동작을 느리게한다.

Template Literal Type으로 string 타입 작성하기

Template Literal Type

Template Literal Type이란 string 타입을 javascript의 template literal string 과 유사한 문법을 통해 더 좁은 string 타입으로 축소시키거나, 같은 형식의 여러 문자열들을 허용하는 타입으로 확장하는 타입스크립트의 문법을 말합니다. 다음과 같은 코드 조각은 Template Literal Type 문법을 활용하여 작성된 코드 조각입니다.

// Template Literal Type 예시 1

type greeting = `hello ${string}` // hello juunzzi, hello lokba 와 같은 문자열만을 허용한다. 
// Template Literal Type 예시 2 : string 타입을 좁히기

// Good : Template Literal Type을 활용하여 string 타입 보다 좁은 문자열 타입을 작성한다. 타입 안정성을 높인다.
type greetingString = `hello ${string}`

const parseNameAtGreetingString = (greeting: greetingString) => {
  	const [, name] = greeting.split(' ')
    return name
}

parseNameAtGreetingString('hello juunzzi');
parseNameAtGreetingString('hello lokba');
parseNameAtGreetingString('hellobae'); // Type Error

use case of Template Literal Type

간략히 use case 를 쫓아보자면, 문자열의 형식이 동일한 경우 해당 형식을 유지하는 문자열 값만을 허용하고자 할 때 해당 문법을 사용해 볼 수 있습니다. (아래 예시는 임시로 작성해본 예시입니다. 더 좁은 타입으로 만들어 낼 수도 있으나 본문의 직관적인 이해를 위해 간략히 작성합니다.)

// 들어올 수 있는 문자열의 형식 혹은 가짓수가 정해져 있는 경우, 제한하기 위해

type PhoneNumber = `010-${DDDD}-${DDDD}`

type Email = `${string}@${string}.${string}` 

type YYYYMMDD = `${YYYY}-${MM}-${DD}`

Date string Type을 Template Literal Type으로 좁혀보자

위에서 설명한 바와 같이 문자열의 형식을 유지하고자 할 때 Template Literal Type 을 사용할 수 있습니다. 그렇기에 핸드폰 번호, 이메일, Date String 에 이를 활용하여 안전한 타입 시스템을 구축할 수 있습니다. 이번 프로젝트에서도 Date String(ex. 1997.01.26)을 활용할 일이 있었기에 더 안전한 타입 시스템 아래에서 코드를 작성하고자 Template Literal Type 문법을 활용해 보았습니다. 우선 Template Literal Type을 활용하지 않았던 기존 코드입니다.

기존 코드 (Date String is string)

  • 코드

    // `YYYY.MM.DD` 형식의 년도 문자열을 입력받으면, `D-Day(number)`를 구해 반환하는 함수
    // Ex) Input: 2022.12.02, Output: 10(number)
    export const generateDDayFromtYYYYMMDD = (YYYYMMDD: string) => {
        const [year, month, date] = YYYYMMDD.split('.');
    
        const dateInstance = new Date(Number(year), Number(month) - 1, Number(date));
    
        const todayDateInstance = new Date();
    
        return Math.ceil(
        (dateInstance.getTime() - todayDateInstance.getTime()) / (1000 * 60 * 60 * 24),
      );
    };
  • 문제 상황
    위 두 개의 함수는 1997.01.26 와 같은 문자열이 들어오면 정상적으로 동작합니다. 하지만 string 타입으로 선언되어 있기 때문에 YYYY.MM.DD 형식이 아닌 문자열 또한 들어올 수 있게 됩니다. 따라서 다음과 같은 함수 호출문은 타입 에러를 출력하지 않습니다. 따라서 예측할 수 없는 런타임 동작이 발생할 수 있습니다.

     // 다음 호출문은 타입 에러가 출력되지 않는다. 예측할 수 없는 런타임 동작을 맞이하게 됩니다.
    generateDDayFromtYYYYMMDD('01.26')
    generateDDayFromtYYYYMMDD('1997-01-26')
    generateDDayFromtYYYYMMDD('ㅁㄴㄹㄴㄹㅁㄴㅎ')

    이렇듯 typestring과 같은 넓은 타입으로 선언하게 되면 더 많은 문자열 타입의 값을 인자로 호출하는 함수 호출문을 허용하게 되고 타입 안정성, 더 나아가 런타임 안정성 역시 떨어지게 됩니다. 이러한 경우에 Template Literal Type을 활용하여 string 타입보다 좁은 문자열 타입을 선언하여 커버할 수 있습니다.

Template Literal Type으로 리팩터링 된 코드 (Date String is YYYYMMDD)

  • 타입 선언

    // date.d.ts
    type oneToNine = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
    type zeroToNine = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
    
    type YYYY = `19${zeroToNine}${zeroToNine}` | `20${zeroToNine}${zeroToNine}`;
    
    type MM = `0${oneToNine}` | `1${0 | 1 | 2}`;
    
    type DD = `${0}${oneToNine}` | `${1 | 2}${zeroToNine}` | `3${0 | 1}`;
    
    type YYYYMMDD = `${YYYY}.${MM}.${DD}`;
  • 리팩터링된 코드

    // `YYYY.MM.DD` 형식의 년도 문자열을 입력받으면, `D-Day(number)`를 구해 반환하는 함수
    // Ex) Input: 2022.12.02, Output: 10(number)
    export const generateDDayFromtYYYYMMDD = (YYYYMMDD: YYYYMMDD) => {
        const [year, month, date] = YYYYMMDD.split('.');
    
        const dateInstance = new Date(Number(year), Number(month) - 1, Number(date));
    
        const todayDateInstance = new Date();
    
        return Math.ceil(
        (dateInstance.getTime() - todayDateInstance.getTime()) / (1000 * 60 * 60 * 24),
      );
    };
  • 해결된 문제 상황
    위와 같이 작성하게 되면, 당연하게도 다음과 같은 상황에서 타입 에러를 출력하여 해당 함수의 런타임 안정성을 높일 수 있습니다.

    // 다음 호출문은 타입 에러가 출력됩니다.
    generateDDayFromtYYYYMMDD('01.26')
    generateDDayFromtYYYYMMDD('1997-01-26')
    generateDDayFromtYYYYMMDD('ㅁㄴㄹㄴㄹㅁㄴㅎ')

이렇게 보면 Template Literal Type을 통해 더 좁은 문자열 타입을 선언하여, 인자로 들어올 수 있는 문자열의 형식을 맞추는 일은 이점만을 가져다주는 것 처럼 보입니다. 하지만 프로그래밍의 세계에서 은탄환은 없습니다.

🌚 잃게되는 점

Template Literal Type이 문자열 타입을 좁게 정의하여 안정적으로 함수를 사용할 수 있게 합니다. 하지만, 타입이 너무 복잡해지기에 VScode's TS server의 동작이 느려지기도합니다. 지금부터 확인하고자 하는 것은 해당 동작이 얼마나 느려지는지입니다. 다음 이미지는 ide(vscode)의 TS server가 타입 체킹을 수행하고 난 이후 출력해 주는 에러입니다.

이번 포스팅을 통해 말하고 싶은 부분은 Template Literal Type은 TS Server의 동작을 얼마나 느리게 하는가이므로, 이를 체크하기 위해 VScode의 TS server 옵션을 일부 수정하여 이후 검증을 진행해 보겠습니다. 다음과 같이 vscode 설정 파일에 "typescript.tsserver.trace": "messages" 옵션을 추가하게 되면 TS server 터미널에는 다음과 같이 TS server의 동작을 추적한 결과물이 출력됩니다. (ide 사용자 이벤트가 발생하고, TS server의 결과물이 응답되는 동작)

옵션의 정의는 다음과 같습니다.

typescript.tsserver.trace : TS 서버로 전송한 메시지 추적을 사용하도록 설정합니다. 이 추적은 TS 서버 문제를 진단하는 데 사용될 수 있습니다. 추적에는 파일 경로, 소스 코드 및 프로젝트에서 잠재적으로 중요한 기타 정보가 포함될 수 있습니다.

이제 ide에서 활동하는 TS Server의 동작 성능을 확인할 수 있게 되었습니다. 따라서 파일 변경 사항을 발생시키거나, 타입 구문에 호버 하여 타입을 확인하는 등의 행동에 따라 TS server 응답 값이 만들어지는 시간을 얻어낼 수 있습니다. 이제 string 타입으로 Date String을 커버할 때Template Literal Type으로 작성된 타입으로 커버할 때를 비교해 보겠습니다.

파일의 변경 사항에 따라 결과물이 만들어지는 시간이 얼마나 차이가 나는지 아래 호출문을 파일에 추가해 보며 확인해 보겠습니다.

  • 추가할 코드
    generateDDayFromtYYYYMMDD("1990.01.10");
  • Template Literal Type 으로 작성된 타입으로 Date String을 커버할 때의 요청이 처리되는 시간

  • string 타입으로 Date String을 커버할 때의 요청이 처리되는 시간

타입스크립트로 서비스 프로덕트를 빌드하고 있다면, 개발자들은 해당 함수의 호출 시그니처를 확인하기 위해 마우스 커서를 올려보기도 합니다. 이에 요청이 처리되는 시간은 다음과 같습니다.

  • 함수 인자가 Template Literal Type으로 선언된 함수의 호출 호출 시그니처를 호버하여 확인할 때 요청이 처리되는 시간
  • 함수 인자가 string으로 선언된 함수의 호출 시그니처를 호버하여 확인할 때 요청이 처리되는 시간

같은 프로젝트에서 YYYYMMDD라는 Template Literal Type으로 작성된 타입을 활용할 때 명확히 응답 시간이 느려짐을 확인할 수 있었습니다. 문을 작성하고 타입에 에러가 있는 지를 확인하는 일, 마우스 커서를 호버하여 타입을 확인하는 일 모두 개발 영역에서 자주 발생하는 개발자 행동들이며, 해당 시간의 지연은 결국 개발 비용의 상승을 초래 할 수 있습니다.

정리하자면

Template Literal Type을 활용하면, 철옹성 같은 타입을 작성할 수 있게 되어 타입 안정성 및 런타임 안정성을 높일 수 있습니다. 하지만, 위에서 확인했듯이 TS Server의 Type Checking, Type Inference 에 소요되는 비용이 커지게 되어 불필요한 개발 비용의 상승을 초래할 수도 있습니다.

따라서 좁은 타입 선언을 위해 Template Literal Type을 무작정 활용할 것이 아니라, 런타임 유효성 검증 코드를 작성한 이후에 안전한 타입이 필요하다면 Template Literal Type 문법을 활용하는 것이 더 나은 방법이지 않을까 생각해 보며 포스팅을 마무리합니다...

(다시 한번 되새기고 갑니다. Typescript Design Goals)

Non-goals : Apply a sound or "provably correct" type system. Instead, strike a balance between correctness and productivity.

profile
Frontend Ninja

9개의 댓글

comment-user-thumbnail
2022년 12월 4일

잘 읽고 갑니다.

답글 달기
comment-user-thumbnail
2022년 12월 5일

은탄은 없군요..

답글 달기
comment-user-thumbnail
2022년 12월 6일

👍

답글 달기
comment-user-thumbnail
2022년 12월 7일

좋은 글 감사합니다.
두 가지 케이스의 시간 차이까지 비교한 것.. 대단한데요. 🥷

답글 달기
comment-user-thumbnail
2022년 12월 8일

역시..찌준!
잘 읽었슈

답글 달기
comment-user-thumbnail
2022년 12월 10일

Glad you like it. We have been using this for several weeks now and it seems to be going nicely.

답글 달기
comment-user-thumbnail
2022년 12월 10일

무작정 template literal 타입을 썻는데 성능상 문제가 있는지는 몰랐네용.. 고마워요 준찌쿤!!

답글 달기
comment-user-thumbnail
2022년 12월 13일

오우 타입스크립트를 사용하며 성능 상의 관점은 생각해보지 못했는데 이런 상황도 고려해 볼 수 있었군요. .. 좋은 글 잘 읽고 갑니다

답글 달기
comment-user-thumbnail
2023년 1월 7일

좋은 글 잘 읽고 갑니다~

답글 달기