그럴거면 TypeScript를 왜 쓰나요?

jyunzzz·2023년 11월 22일
47

Safety Type Zone

목록 보기
2/2
post-thumbnail

들어가는 글

지금의 JS 개발자들에게 있어서 TS의 입지는 굉장히 확고하다고 생각해요. 이미 너무나도 많은 곳에서 사용하고 있거나 동적타입을 서포트하는 도구로서 특별한 대체제가 없거나 하는 수많은 이유들이 존재할거에요. 그래서 결국 TS를 사용해야 한다면 어떻게 잘 써야하는지에 대해서 인지하면 더 좋은 경험을 얻을수 있을거라는 생각에 이 글을 기고하게 되었어요. (실은 거창한 제목과는 달리 검소한 내용이라는 이야기에요.)

TypeScript가 불편해요

수많은 곳에서 TS를 찬양하고 적극적으로 사용하고 있지만 막상 실제로 사용했을때 편하다고 느껴지지 않았던 경험들이 있을거에요. 그리고 저는 이따금씩 TS를 사용하여 직접 작성한 코드들을 돌아보고 저는 정말 TS를 잘 활용하고 있고 좋은 개발경험을 얻어가고 있어요 라고 자신있게 말할 수 있는 개발자가 얼마나 있을지 생각해보곤 해요.

왜 TS를 쓰는데도 불편할까요? 아래의 항목들을 잘 사용하고 있는지 다시 한번 돌이켜봐요. (아마도 잘 사용하고 있겠지만 복습의 의미도 겸해보아요.)

strictNullChecks: false

이 옵션을 사용하지 않는다면 수많은곳에서 null이 허용될것이고 그 순간부터 코드는 이미 깨진유리창이 되어버리는거에요.
깨진유리창 이론) 깨진 유리창 하나를 방치해 두면, 그 지점을 중심으로 범죄가 확산되기 시작한다는 이론으로, 사소한 무질서를 방치하면 큰 문제로 이어질 가능성이 높다는 의미를 담고있어요.

간단하게 타입과 값을 선언해보았을 때 이 옵션을 사용하지 않는다면 아래의 값은 컴파일에러를 발생시키지 않아요. 1차원적으로 지금은 없을수도 있는 값일지라도 올바르게 검사하지 않는다면 이 값은 의도하던 의도하지 않았던 아마도.. 없을수도 있어 라는 의미를 지니게 되기 때문에 이를 사용하는 모든 곳에서 검증의 의무를 지녀야 할거에요.

// 1. (X)
type Count = number;
const count1: Count = 1;
const count2: Count = undefined;

const result: Count = count1 * count2;
console.log(`Ba${result}a`); // BaNaNa


// 2. (O)
type Count = number;
const count1: Count = 1;
const count2: Count | undefined = undefined;

// compile error!!
// const result: Count = count1 * count2; 

없는 값을 명시적으로 선언하는것이 정적타입에서 근본적으로 지녀야할 의무에요. 그렇지 않다면 1번의 코드처럼 골치아픈상황이 생길거에요.

Optional한 값이라면 명시적으로 선언하고 정면 돌파하세요!

noImplicitAny: false

하나하나 Type을 선언하는건 너무나도 복잡하고 귀찮아서 수많은 any를 쓰고있지 않나요? 그렇다면 지금 당장 터미널에서 다음의 커맨드를 실행하세요!
npm uninstall typescript && yarn remove typescript && pnpm remove typescript && bun remove typescirpt (진짜 하진 마세요!)

TS 컴파일러가 any로 타입을 추론하게되면 모든것을 검증해야 하는데 그렇다면 TS를 굳이 사용할 필요가 없어요! 이는 Optional한 값을 모든곳에서 사용하겠다는 의미와 같아요.

그래도 불편해요

이미 위의 것들을 잘 사용하고 있음에도 불편하다고 느낄수도 있어요. 그렇다면 이제 올바른 타입설계에 대해서 공부할 시간이 되신거에요.

결국엔 로직과 타입은 사용법이 어려운것이 아니라 실제로 이 철학대로 구현하는것이 더 어려워요. 지금 이 글에서 설명하지 않을것이지만 대수적 자료 타입이라는 키워드로 타입설계에 대해서 한번 찾아본다면 좋은 인사이트를 찾을 수 있을거에요. (무책임)

TypeScript는 만능이 아니지만

엄격하게 TS를 사용하고 있음에도 불편하다고 느끼는 이유는 무엇일까요? 그건 TS가 JS를 호환하는 슈퍼셋을 추구하기 때문에 보조적인 도구로서의 측면이 있는데 결국 정적타이핑을 통해 코드를 더 예측가능하고 안전하게 만드는데 있어서 많은 도움을 주고 있어요.

그럼 아래의 설명에서 TS가 대체적으로 어떤것을 도와주는지 설명해 볼게요.

Structural Typing

TS는 구조적 타이핑을 통해 멤버타입을 검사해요. 같은 타입을 가진 멤버를 가지면 동일한 타입이며 동일한 값으로 인식하기도 하죠.

이 코드는 TypeScript 공식 홈페이지의 구조적타이핑에 대한 코드 샘플을 그대로 가져왔어요.

type Ball = {
  diameter: number;
}
type Sphere = {
  diameter: number;
}

let ball: Ball = { diameter: 10 };
let sphere: Sphere = { diameter: 20 };

sphere = ball;
ball = sphere;

type Tube = {
  diameter: number;
  length: number;
}

let tube: Tube = { diameter: 12, length: 3 };

// tube = ball; // Compile Error!!
ball = tube;

같은 속성을 가진 값은 서로 할당할 수 있지만 더 큰 범위의 타입은 더 작은 타입이 될 수 없음을 나타내기도 해요. 이는 Type은 집합이라는 개념을 잘 설명하는 예제이기도 해요.
그리고 선언된 타입을 더 늘릴수는 있지만 줄일수는 없다는것은 작은 단위의 타입부터 잘 설계해서 사용해야 한다는 것을 의미하기도 하는데, 이는 우리가 타입을 억지로 덜어낼 수 없음과 같아서 나쁜 코드를 자연스럽게 사용할 수 없도록 해줘요. (any를 쓰면 안되는 이유와도 연결할수도 있겠네요!)

Nominal Typing

명시적 타이핑이라고도 하는데 이는 Tagged Union이라고도 Branded Typing 이라고도 할 수 있어요. 좀 더 올바르게 설명해보면 Tagged Union 또는 Branded Typing을 통해 Nominal Typing을 구현한다고 할 수 있어요. 그리고 이것은 동적타입의 세계에서 정말로 중요한 이야기에요!

위와 마찬가지로 이 코드도 TypeScript 공식 홈페이지에서 그대로 가져왔어요.

type ValidatedInputString = string & { __brand: "User Input Post Validation" };

const validateUserInput = (input: string) => {
  const simpleValidatedInput = input.replace(/\</g, "≤");
  return simpleValidatedInput as ValidatedInputString;
};

const printName = (name: ValidatedInputString) => {
  console.log(name);
};

const input = "alert('bobby tables')";
const validatedInput = validateUserInput(input);
printName(validatedInput);

// printName(input); // compiler error!!

위에서 선언된 ValidatedInputString 이라는 타입은 일반적인 string이 아닌 특별한 의미를 가진 string이에요. 그리고 더불어 validateUserInput 함수 의 return절에서 as가 가지는 강제성도 더불어 보여주고있어요. (정말로 타입이 추론이 되지 않는 경우를 제외하면 satisfies 키워드로 대체해보세요)

좀 더 간략하고 멋들어지게 설명해본다면 ValidatedInputString은 명시적으로 서명된 타입이에요. 일반적인 문자열 값이 아닌 의미를 가진, 구체화된 문자열값으로 구현해낸것이에요. 그리고 이것을 Nominal Typing이라고 해요. 이러한 테크닉은 비슷한 타입을 서명으로 더 구분짓기 편하게 하고 값을 다루는데 있어서 편의성을 제공해요. 다룰 값이 일반적인 값이 아닌 서명된 값만을 사용하는거에요.

printName 함수에 서명되지 않은(이 코드에서는 검증되지 않은 string을 이라는 의미겠지만요.) string을 넣을수 없는것처럼요!

Type Widening and Narrowing

타입은 넓히거나 좁힐수 있어요. 위의 두 개념을 아직 잘 이해하지 못했더라도 잘 따라올 수 있을거에요.

이 또한 TypeScript 공식 홈페이지의 코드를 가져왔어요.

// case 1
const welcomeString = "Hello There";
let replyString = "Hey";

replyString = "Hi :wave:";

// case 2
declare const quantumString: string | undefined;

// quantumString.length; // compile error!!

if (quantumString) {
  quantumString.length;
}

case 1의 const는 변경될 수 없음을 의미하기에 welcomeString의 추론된 타입은 "Hello There"가 되지만 let은 변경될 수 있음을 의미하기에 replyString의 추론된 타입은 string이 되어요.
이는 const는 타입을 좁히고, let은 타입을 넓히는것을 의미해요.

case 2의 quantumString은 명시적으로 undefined를 허용하기 때문에 주석된 코드는 undefined일때 length라는 속성을 가질 수 없어서 컴파일 에러가 발생해요.
하지만 if 조건문을 통해 undefined가 아님을 검사했을 때 해당 스코프 내에서의 quantumStringstring타입으로 좁혀지기 되기 때문에 length 속성을 가져요. 이는 일종의 타입가드를 통해 타입을 좁힐수 있는 테크닉이에요.

그리고 위에서 설명했던 구조적 타이핑과 명시적 타이핑을 이 예제에 덧붙여서 설명할수도 있어요.

// case 1
const welcomeString = "Hello There";
let replyString = "Hey";

replyString = "Hi :wave:";

// Structural Typing
replyString = welcomeString;


// case 2
type ValidatedInputStringLength = number & { __branded: 'validation string length' };

// 인자로 받는 value값이 quantumString의 타입이라고 가정해보세요.
const validateInput = (value: string | undefined): ValidatedInputStringLength => {
  return (value ? value.length : 0) as ValidatedInputStringLength;
}

case 1에서 구조적 타이핑을 통해 string 타입을 가진 값에 "Hello There"을 가진 값을 할당할 수 있기에 replyString = welcomeString이 될 수 있어요.

그리고 case 2에서 명시적 타이핑을 통해 검증되었다는 서명이 된 값을 돌려주는 함수를 구현할수도 있어요. 이는 타입을 좁히는것의 개념과도 조금은 비슷해요.(똑같지는 않아요)

그럼에도 불구하고

TS는 우리가 작성하거나 설계해야 하는 코드와 로직을 없을때와 비교하면 더 방대하게 만드는것은 부정할 수 없어요. 그런데 동적인 것을 정적으로 만드는 테크닉을 활용할 수 있게 도와주는 이 도구를 쓰지 않는게 더 손해이지 않을까요?

과거에 저는 TS를 정말 못쓰고 있었어요. 사실 이 글의 제목인 차라리 그럴거면 TypeScript를 왜쓰나요?라는 이야기는 과거의 저에게 하는 자조적인 이야기였으니깐요. 그런데 어느새 정적타입이 주는 매력에 대해서 이해하고 나니 TS를 넘어서 Type에 대해서 정말 모르고 있었다는 생각이 들기도 했지만 그보다도 정말 엉망진창으로 코드를 쓰고 있었구나. 사서 고생했구나. 몸이, 머리가 참으로 고생했구나 싶었어요.
물론 지금 당장 이러한 테크닉과 이론들을 처음접한다면 이해하기가 어렵더라도 언젠가는 이 글을 읽은 분들의 코드에 좋은 영향이 있기를, 너무 고생하지 않기를 바라요.

profile
BaNaNa never cramp

6개의 댓글

comment-user-thumbnail
2023년 11월 22일

TS 샴푸도 쓰시나요?

1개의 답글
comment-user-thumbnail
2023년 12월 2일

여전히 TSC(oding)를 하시나요?

1개의 답글