큰 꿈은 파편이 크다!!⚡️

이펙티브 타입스크립트 🦅 1장 본문

Web FE

이펙티브 타입스크립트 🦅 1장

wood.forest 2022. 8. 15. 18:04

들어가며

타입스크립트는 지금 회사에서도 사용하고 있지만 굉장히 얕은 지식과 스킬들로 연명하고 있는 느낌을 지속적으로 받았다. 사실 학습해야할 게 많다보니 우선순위에서 밀리기도 하던 중에.. 이펙티브 타입스크립트 스터디에 참여하게 되었다. 확실히 오프라인 스터디를 하면 책임감이 생겨 더 열심히 하는 것 같다 ^^

스터디를 계기로 학습 내용을 기록하고자 한다.

 

⚠️

- 이펙티브 시리즈는 초급자를 대상으로 하는 책이 아니다. 책의 서문에도 나와있듯이, 초급자/중급자가 전문가로 발전할 수 있을 정도의 난이도를 제공한다.

- 책의 모든 내용을 요약하는 것이 아닌, 읽어보며 정리하고 싶은 내용+스터디 내용+내가 궁금해서 찾아본 내용 등, 책을 기반으로 재구성한 혼합체..에 가깝다.

- 이하 작성의 편의를 위해 타입스크립트 -> TS, 자바스크립트 -> JS로 표기한다 ㅎㅎ

 

밈을 넣어보고싶었다..

 

 

1. TS와 JS의 관계

TS는 JS의 상위 집합superset이다

- JS로 작성된 프로그램에 문법 오류가 없다면 유효한 TS 프로그램이다

- 각각 다른 확장자(.js, .ts)를 사용하지만 .js.ts로 사용해도 동일하다 (마이그레이션의 이점)

- 즉, 모든 JS프로그램은 TS이다

 

 

TS는 정적 타입 시스템이다

- TS는 JS 런타임 동작을 모델링하는 타입 시스템을 가지고 있으므로, 런타임에 오류를 발생시킬 코드를 미리 찾는다 (모두 찾는것은 아님)

- ex. state에 없는 capitol 속성을 사용하는 문제에 대해 오류를 나타낸다

// HIDE
export const foo = true;
const states = [
  {name: 'Alabama', capital: 'Montgomery'},
  {name: 'Alaska',  capital: 'Juneau'},
  {name: 'Arizona', capital: 'Phoenix'},
  // ...
];
// END

for (const state of states) {
  console.log(state.capitol);
                 // ~~~~~~~ Property 'capitol' does not exist on type
                 //         '{ name: string; capital: string; }'.
                 //         Did you mean 'capital'?
}

 

 

타입 체커 영역

- ex. 하지만 states에서부터 오타를 낸다면 어떨까? 아래와 같이 interface를 통해 객체 타입을 명시하여 오류를 찾을 수 있다. 이는 '타입 체커를 통과한 TS'영역이며, 우리가 평소 작성하는 TS코드이다.

interface State {
  name: string;
  capital: string;
}
const states: State[] = [
  {name: 'Alabama', capitol: 'Montgomery'},
                 // ~~~~~~~~~~~~~~~~~~~~~
  {name: 'Alaska',  capitol: 'Juneau'},
                 // ~~~~~~~~~~~~~~~~~
  {name: 'Arizona', capitol: 'Phoenix'},
                 // ~~~~~~~~~~~~~~~~~~ Object literal may only specify known
                 //         properties, but 'capitol' does not exist in type
                 //         'State'.  Did you mean to write 'capital'?
  // ...
];
for (const state of states) {
  console.log(state.capital);
}

 

- TS는 JS의 상위 집합이다, 라는 말이 어색하게 느껴지는 이유는 타입 체크된 TS영역 때문이다. 프로그램에 오류가 발생하지 않더라도 타입 체커가 오류를 표시한다.

const a = null + 7;  // Evaluates to 7 in JS
       // ~~~~ Operator '+' cannot be applied to types ...

 

 

 

 

2. TS 설정 이해하기

- tsc --init을 통해 tsconfig.json 설정 파일을 생성하여 동료들, 도구들과 공유하자

- 설정을 제대로 사용하기 위해 noImplicitAnystrictNullChecks를 이해하자

 

- NoImplicitAny: 변수들이 미리 정의된 타입을 가져야 하는지 여부를 제어

    - ex1. 아래의 경우 TS는 a, b를 any 타입으로 추론한다

// tsConfig: {"noImplicitAny":false}

function add(a, b) {
  return a + b;
}

    - ex2. noImplicitAny를 설정하면 오류를 나타낸다. 명시적으로 any 타입을 선언하거나 더 분명한 타입을 선언하여 해결할 수 있다

// tsConfig: {"noImplicitAny":true}

function add(a, b) {
          // ~    Parameter 'a' implicitly has an 'any' type
          //    ~ Parameter 'b' implicitly has an 'any' type
  return a + b;
}

  - TS는 타입 정보를 가질 때 가장 효과적이므로 되도록 noImplcitAny를 설정하자!

  - noImplicitAny 설정을 해제하는 순간은 기존 JS코드들을 TS로 마이그레이션하는 과정에만 사용하자!

 

strictNullChecks: null, undefined가 모든 타입에서 허용되는지 확인하는 설정

    - ex. strictNullChecks 설정을 사용하는 경우, 명시적으로 null타입이 지정된 변수에만 null을 할당할 수 있다. undefined도 마찬가지.

    - 사용하려면 noImplicitAny 설정이 선행되어야 한다

// tsConfig: {"noImplicitAny":true,"strictNullChecks":true}

const x: number = null;
//    ~ Type 'null' is not assignable to type 'number'

const properX: number | null = null;

 

- 이 두 가지 모두를 설정하여 엄격한 체크를 원한다면 strict 설정을 고려하자

 

 

 

3. 코드 생성과 타입은 관계없다

TS 컴파일러는 두 가지 역할을 독립적으로 수행한다

    1) 최신 TS/JS가 브라우저에서 동작하도록 구버전의 JS로 트랜스파일transpile한다

        💡 트랜스파일: translate+compile. 소스코드를 동일한 동작을 하는 다른 형태의 소스코드(다른 버전, 언어 등)로 변환하는 행위

    2) 코드의 타입 오류를 체크한다

 

- 컴파일은 타입 체크와 독립적이므로, 타입 오류가 있어도 컴파일이 가능하다

    - 어떤 부분에 오류가 있더라도 다른 부분을 테스트할 수 있게 해주는 장점이 있다

- 오류가 있을 때 컴파일하지 않으려면 tsconfig.jsonnoEmitOnError를 설정한다

 

 

 

런타임에는 타입 체크가 불가능하다

- 자바스크립트로 컴파일되는 과정에서 모든 인터페이스, 타입, 타입 구문은 제거된다

- ex. shape instanceof Rectangle 의 형식으로 체크할 때, Rectangle은 컴파일을 거치면 타입이 아닌 '값'이 된다

interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
                    // ~~~~~~~~~ 'Rectangle' only refers to a type,
                    //           but is being used as a value here
    return shape.width * shape.height;
                    //         ~~~~~~ Property 'height' does not exist
                    //                on type 'Shape'
  } else {
    return shape.width * shape.width;
  }
}

- 런타임에도 타입 정보를 유지하려면 추가 작업이 필요하다

    - ex. 위 코드에서 해당 속성이 존재하는지 체크하는 방식으로 변경한다. 속성 체크('height' in shape)는 런타임에 일어나지만, shape의 타입이 Rectangle임을 타입 체커가 보정하므로 오류가 사라지고 shape.width에 정상적으로 접근할 수 있다.

function calculateArea(shape: Shape) {
  if ('height' in shape) {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;
  }
}

 

 

타입 연산은 런타임에 영향을 주지 않는다

- ex. TS에서는 리턴 값에 타입을 줬다고 생각할 수 있지만(asNumber1) 실제 JS(asNumber2)에서는 아무것도 하지 않는다. 값을 정제하기 위해서는 런타임의 타입을 체크하고, JS연산을 통해 변화시켜야 한다. (asNumber3)

function asNumber1(val: number | string): number {
  return val as number;
}

//in js
function asNumber2(val) {
  return val;
}

//👍
function asNumber3(val: number | string): number {
  return typeof(val) === 'string' ? Number(val) : val;
}

- 타입과 런타임 동작이 무관하다는 점을 잊지말자!

    - 타입은 JS 변환 시점에 제거되므로 런타임 성능에 영향을 주지 않는다

    - 대신 TS 컴파일 오버헤드가 발생할 수 있다

 

 

 

 

4. 구조적 타이핑(Structural typing)에 익숙해지기

JS는 덕 타이핑 기반이며, TS는 이를 모델링하기 위해 구조적 타이핑을 사용한다

💡 duck typing: 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는 방식 (ex. 만약 어떤 새가 오리처럼 걷고 꽥꽥거린다면 그 새를 오리로 간주한다)

- 함수를 작성할 때 매개변수 속성들이 매개변수 타입에 선언된 속성만을 가질것이라 생각하지만 TS 타입시스템에서는 표현할 수 없음

    - ex. 매개변수 타입은 Vector2D이지만 동일한 속성을 가진 NamedVector를 매개변수로 사용해도 동작함

interface Vector2D {
  x: number;
  y: number;
}
function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}
interface NamedVector {
  name: string;
  x: number;
  y: number;
}
const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calculateLength(v);  // OK, result is 5

 

 

 

 

5. any 타입 지양하기

TS의 타입 시스템은 점진적gradual이고 선택적optional이며, 이 특성들의 핵심은 any타입이다.

- 점진적: 코드에 타입을 조금씩 추가할 수 있다 (JS를 TS로 마이그레이션하는 경우)

- 선택적: 언제든지 타입 체커를 해제할 수 있다

 

 

any타입에는 타입 안전성이 없다

- ex. string 타입은 number 타입이 필요한 곳에서 오류없이 실행될 수 있기때문에 문제를 일으킨다

let age: number;
age = '12';
//    ~~~ Type '"12"' is not assignable to type 'number'
age = '12' as any;  // OK
age += 1;  // OK; at runtime, age is now "121"

 

any는 함수 시그니처를 무시한다

- 함수 작성 시 호출하는 쪽은 약속된 타입의 입력을, 함수는 약속된 타입의 출력을 반환하지만 any 타입을 사용하면 이 약속을 어길 수 있다

 

 

any 타입은 언어 서비스가 적용되지 않는다

- any 타입인 심벌은 자동완성, 도움말, 이름 변경 등의 언어 서비스를 사용할 수 없다

    - 이는 생산성을 떨어뜨린다

 

 

any 타입은 버그와 타입 설계를 감춘다

- 매개 변수를 잘못 넣어도 타입 체커를 통과해서 버그를 낳는다

- 타입 설계는 중요하기 때문에, 설계가 명확히 보이도록 타입을 일일이 작성해야 한다

- any로 인해 개발 경험이 나빠지면 타입 시스템의 신뢰도가 떨어진다

 

 

 

 

 

1장 마무리

TS를 통해 원활한 협업이 이루어지면 생산성이 향상된다. 개인적으로 TS를 사용하면서 가장 편리했던 점은 자동완성과 함수를 사용할 때 어떤 타입의 변수를 넣는지 알려주는 도움말이었다 ㅎㅎ

책이 확실히 난이도가 있긴 한건지 가장 쉬울 1장을 읽는데도 꽤 시간이 걸렸다..🫠

TS설정파일에는 소홀했는데 noImplicitAny가 생각보다 훨씬 중요한 역할을 하고 있어서 기억에 남는다.

타입과 런타임이 상상 이상으로 무관하다는 점도 인상깊다. TS의 원리를 조금 더 이해한 느낌이다.

 

이번 장에서 배운.. 면접질문으로 나올수있는것들!

  • 왜 TS를 사용하는가
  • Transpile vs Compile
반응형