우아한 TS with React 도서 정리

우아한 TS with React 도서에서 학습한 내용을 정리했습니다.


1장 들어가며

1.1 웹 개발의 역사

Polyfill

  • 브라우저가 지원하지 않는 코드를 브라우저에서 사용할 수 있도록 변환한 코드 조각이나 플러그인
  • ex) core.js, polyfill.io

Transpile

  • 최신 버전의 코드를 예전 버전의 코드로 변환하는 과정
  • ex) Babel

CBD(Component Base Development)

  • 서비스에서 다루는 데이터를 구붆고 그에 맞는 UI를 표현할 수 있게 컴포넌트 단위로 개발하는 접근 방식
  • 재사용할 수 있는 컴포넌트를 개발 또는 조합해서 하나의 애플리케이션을 만드는 개발 방법론
  • 컴포넌트
    • 모듈과 유사하게 하나의 독립된 기능을 재사용하기 위한 코드 묶음
    • 모듈과 달리 런타임 환경에서 독립적으로 배포/실행 될 수 있는 단위(다른 컴포넌트와 의존성을 최소화하거나 없애야 함)

1.2 JS의 한계

동적 타이핑 시스템의 한계

const sumNumber = (a, b) => {
  return a + b;
};
 
sumNumber(100); // NaN
sumNumber('a', 'b'); // ab
  • 동적 타입 언어라는 특성 때문에 sumNumber 함수를 호출할 때 사용되는 인수 값에 따라 a와 b의 타입이 결정됨
  • 즉, 코드는 기계 입장에서는 정상적이지만 사람 입장에서는 정상적이지 않는 코드 발생

TS 등장(JS의 단점 극복)

  1. JS의 슈퍼셋

    • 기존 언어에 새로운 기능과 문법을 추가해서 보완하거나 향상하는 것
    • 기존 언어와 호환되며 일반적으로 컴파일러 등으로 기존 언어 코드로 변환되어 실행 됨
  2. 안정성 보장

    • 정적 타이핑 제공
    • 컴파일 단계에서 타입 검사하기때문에 런타임 에러를 사전에 방지하여 안정성 크게 향상
  3. 개발 생산성 향상

    • IDE에서 자동 타입 완성 기능 제공
    • 변수와 함수 타입을 추론 가능
    • React 사용할 때, props 타입 확인 가능
  4. 협업에 유리

    • interface, generic 등으로 코들르 더 쉽게 이해할 수 있도록 제공
  5. JS 점진적으로 적용 가능

    • TS는 JS의 슈퍼셋이기 때문에 일괄 전환이 아닌, 점진적 도입 가능

2장 타입

2.1 타입이란

자료형으로서의 타입

  • ECMAScript 표준 데이터 타입: undefined, null, Boolean, String, Symbol, Number, BigInt, Object
  • 데이터 타입은 여러 종류의 데이터를 식별하는 분류 체계로 컴파일러에 값의 형태를 알려줌
  • 메모리에 저장된 값을 데이터 타입으로 설명할 수 있으며 모든 데이터를 해석할 때 데이터 타입 체계가 사용됨
  • 메모리 관점에서의 데이터 타입은 프로그래밍 언어에서 일반적으로 타입으로 부르는 개념과 같음

컴파일 방식

  • 컴파일: 사람이 이해 할 수 있는 방식으로 작성한 코드를 컴퓨터가 이해할 수 있는 기계어로 바꿔주는 과정
  • TS 컴파일 결과는 JS 파일: JS의 컴파일 타임에 런타임 에러를 사전에 잡아내기 위함

2.2 TS 타입 시스템

타입 Annotation

  • 변수나 상수 혹은 함수의 인자와 반환 값에 타입을 명시적으로 선언해서 어떤 타입 값이 저장 될 것인지를 컴파일러에게 직접 알려주는 문법

    let isDone: boolean = false;
    let decimal: number = 6;
    let color: string = 'blue';
    let list: number[] = [1, 2, 3];
    let x: [string, number]; // tuple

구조적 타이핑

  • 타입을 사용하는 여러 프로그래밍 언어에서 값이나 객체는 하나의 구체적인 타입을 가지고 있음. 타입은 이름으로 구분되며 컴파일타임 이후에도 남아있음. 이것을 명목적으로 구체화한 타입 시스템이라고 부름

     class Animal {
        String name;
        int age;
     }
  • 서로 다른 클래스끼리 명확한 상속 관계나 공통으로 가지고 있는 인터페이스가 없다면 타입은 서로 호환되지 않음

    interface Developer {
      faceValue: number;
    }
     
    interface BankNote {
      faceValue: number;
    }
     
    let develop: Developer = { faceValue: 52 };
    let bankNote: BankNote = { faeValue: 1000 };
     
    develop = bankNote; // ok
    bankNote = develop; // ok
  • TS는 구조로 타입을 구분함(구조적 타이핑)

구조적 서브 타이핑

  • 객체가 가지고 있는 속성을 바탕으로 타입을 구분하는 것(이름이 다른 객체라도 가진 속성이 동일하다면 TS는 서로 호환이 가능한 동일한 타입으로 여김)

    interface Pet {
      name: string;
    }
     
    interface Cat {
      name: string;
      age: number;
    }
     
    let pet: Pet;
    let cat: Cat = { name: 'Zag', age: 2 };
     
    pet = cat; // ok
    interface Pet {
      name: string;
    }
     
    function greet(pet: Pet) {
      console.log('Hello, ' + pet.name);
    }
     
    greet(cat); // ok
    • 타입을 명시하지 않은 cat 객체를 greet()함수의 인자로 전달해도 코드는 정상적으로 동작
    • cat 객체는 Pet 인터페이스가 가지고 있는 name 속성을 가지고 있어 pet.name의 방식으로 name 속성에 접근할 수 있음
    • 이와 같은 타이핑 방식이 구조적 타이핑
  • TS의 서브타이핑, 즉 타입의 상속 역시 구조적 타이핑을 기반으로 하고 있음

    class Person {
      name: string;
      age: number;
     
      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
    }
     
    class Developer {
      name: string;
      age: number;
      sleepTime: number;
     
      constructor(name: string, age: number, sleepTime: number) {
        this.name = name;
        this.age = age;
        this.sleepTime = sleepTime;
      }
    }
     
    function greet(p: Person) {
      console.log(`Hello, I'm${p.name}`);
    }
     
    const developer = new Developer('zig', 20, 7);
     
    greet(developer); // Hello, I'm zig
    • Develop 클래스가 Person 클래스를 상속받지 않았는데도 greet(develop)는 정상적으로 동작함. DevelopPerson이 갖고 있는 속성을 가지고 있기 때문

JS를 닮은 TS

  • 덕 타이핑: 어떤 함수의 매개변숫값이 올바르게 주어진다면 그 값이 어떻게 만들어졌는지 신경쓰지 않고 사용한다는 개념
  • TS는 JS의 슈퍼셋이기 때문에 덕 타이핑 동작을 그대로 모델링 함 그래서 명시적인 이름을 가지고 타입을 구분하는 대신 객체나 함수가 가진 구조적 특징을 기반으로 타이핑하는 방식 택함
  • 덕 타이핑은 런타임에 타입을 검사하고 구조적 타이핑은 컴파일타임에 타입체거가 타입을 검사함
  • 덕 타이핑과 구조적 타이핑 모두 객체 변수, 메서드 같은 필드를 기반으로 타입을 검사한다는 점에서는 동일하지만, 타입을 검사하는 시점이 다름
  • 덕 타이핑은 주로 동적 타이핑에서, 구조적 타이핑은 정적 타이핑에서 사용됨

구조적 타이핑의 결과

  • TS의 구조적 타이핑의 특징 때문에 예기치 못한 결과가 나올수 있음

    interface Cube {
      width: number;
      height: number;
      depth: number;
    }
     
    function addLines(c: Cube) {
      let total = 0;
     
      for (const axis of Object.keys(c)) {
        // ‼️ Element implicitly has an 'any' type
        // because expression of type 'string' can't be used to index type 'Cube'
        // ‼️ No index signature with a parameter of type 'string'
        // was found on type 'Cube'
        const length = c[axis];
        total += length;
      }
    }
    • addLines() 함수의 매개변수인 cCube의 타입으로 선언되었고, Cube 인터페이스의 모든 필드는 number 타입을 가지기 때문에 c[axis]는 당연히 number 타입일 것이라고 예측함

    • 그러나 c에 들어온 객체는 Cubewidth, height, depth 외에도 어떤 속성이든 가질 수 있기 때문에 c[axis]의 타입이 string일 수도 있어 에러가 발생함

      const namedCube = {
        width: 6,
        height: 5,
        depth: 4,
        name: 'SweetCube', // string 타입의 추가 속성이 정의되었다.
      };
      • TS는 c[axis]가 어떤 속성을 지닐지 알 수 없으며 c[axis] 타입을 number라고 확정할 수 없어서 에러를 발생시킴
      • TS 구조적 타이핑의 특징으로 Cube 타입 값이 들어갈 곳에 name 같은 추가 속성을 가진 객체도 할당할 수 있기 때문에 발생하는 문제
      • 이러한 한계를 극복하고자 TS에는 명목적 타이핑 언어의 특징을 가미한 식별할 수 있는 유니온 같은 방법이 생겨남

TS의 점진적 타입 확인

  • 점진적 타입 검사: 컴파일 타임에 검사하면서 필요에 따라 타입 선언 생략을 허용하는 방식
  • 타입을 지정한 변수와 표현식은 정적으로 타입을 검사하지만 타입 선언이 생략되면 동적으로 검사를 수행
  • 타입 선언 생략하면 암시적 타입 변환 일어남

주의 사항

  • 위 특징때문에 TS의 타입시스템은 정적 타입의 정확성을 100% 보장하지 않음

  • 모든 변수와 표현식의 타입을 컴파일에 검사하지 않아도 되기 때문에 타입이 올바르게 정해지지 않으면 런타임에 에러가 발생하기 함

    const names = ['zig', 'colin'];
     
    // ‼️ TypeError: Cannot read property 'toUpperCase' of undefined
    console.log(names[2].toUpperCase());

값과 타입

  • 값: 프로그램이 처리하기 위해 메모리에 저장하는 모든 데이터
  • TS는 값과 타입이 함께 사용됨

값과 타입 공간에 동시에 존재하는 심볼

  1. Class

    class Developer {
      name: string;
      domain: string;
     
      constructor(name: string, domain: string) {
        this.name = name;
        this.domain = domain;
      }
    }
     
    const me: Developer = new Developer('kay', 'frontend');
  • 변수명 me 뒤에 등장하는 :Developer에서 Developer는 타입에 해당하지만, new 키워드 뒤의 Developer는 클래스의 생성자 함수인 값으로 동작함
  • TS에서 클래스는 타입 애너테이션으로 사용할 수 있지만, 런타임에서 객체로 변화되어 JS의 값으로 사용되는 특징을 가지고 있음
  1. enum
  • 런타임에 객체로 변환되는 값

  • 런타임에 실제 객체로 존재하며, 함수로 표현할 수도 있음

    enum Direction {
      Up, // 0
      Down, // 1
      Left, // 2
      Right, // 3
    }
    // Enum이 Type으로 사용하는 겨우
    enum WeekDays {
      MON = "Mon",
      TUES = "Tues",
      WEDNES = "Wednes",
      THURS = "Thurs',
      FRI = "Fri"
    }
     
    // 'MON' | 'TUES' | 'WEDNES' | 'THURS' | 'FRI'
    type WeekDaysKey = keyof typeof WeekDays;
    function printDay(key: WeekDaysKey, message: string) {
      const day = WeekDays[key];
     
      if (day <= WeekDays.WEDNES) {
        console.log(`It's still ${day}day, ${message}`)
      }
    }
     
    printDay("TUES", "wanna go home");
    // enum이 값 공간에서 사용된 경우
    enum MyColors {
      BLUE = '#0000FF',
      YELLOW = '#FFFF00',
      MINT = '#2AC1BC',
    }
     
    function whatMintColor(palette: { MINT: string }) {
      return palette.MINT;
    }
     
    whatMintColor(MyColors); // ok

enum과 객체 차이

  1. enum과 객체의 차이
  • enum
    • 타입 & 값 제공: 선언 시 타입과 런타임 값 둘 다 생성되어 하나의 네임스페이스처럼 사용할 수 있습니다.
    • 역매핑: 숫자 enum의 경우, 값과 이름 간에 양방향(정방향/역방향) 매핑이 자동으로 이루어집니다.
    • 런타임 객체: 실제 객체로 생성되므로, 트리 쉐이킹 측면에서는 다소 불리할 수 있습니다.
  • 객체 (예: as const 사용)
    • 불변 & 리터럴 타입: as const를 사용하면 각 속성이 고정된 리터럴 타입으로 취급되어 타입 안전성이 높아집니다.
    • 단순 값 집합: 값만 제공하며, 자동으로 타입(예: union 타입)이나 역매핑 기능은 지원하지 않습니다.
    • 최적화: 불필요한 코드 제거(트리 쉐이킹)에 유리합니다.
  1. 역매핑(Reverse Mapping)
  • 숫자 enum에서 각 멤버의 숫자 값으로 해당 멤버 이름을 찾을 수 있도록, enum 객체가 자동으로 양방향 매핑(값 → 이름, 이름 → 값)을 생성하는 기능입니다.

    enum Colors {
      Red, // 0
      Blue, // 1
      Green, // 2
    }
     
    // 내부적으로 아래와 같이 생성됨
    {
      0: "Red",
      1: "Blue",
      2: "Green",
      Red: 0,
      Blue: 1,
      Green: 2,
    }
  • 문자열 enum: 문자열 enum은 역매핑 기능을 지원하지 않습니다.

  • enum은 타입과 값의 네임스페이스를 제공하고, 숫자 enum에서는 역매핑 기능이 있어 값을 통해 이름을 찾을 수 있지만, 객체는 단순한 상수 집합으로 트리 쉐이킹 등에 유리한 장점이 있습니다.

우형 enum 어떻게 사용할까?

Q) 팀 내에서 enum과 유니온 타입을 사용하나요?

  1. A팀

    • 유니온 타입은 내가 어떤 타입을 가졌는지 전부 기억해야 하고, 변경이 필요하면 사용되는 곳을 모두 찾아서 바꿔야 할 때가 있음. 특히 string 타입의 유니온 타입은 리팩터링하기에 번거로운 점이 많은 것 같음. 또한 유니온 타입은 타입이니까 순회가 안되지만, enum은 값이기 때문에이터러브 해서 순회 가능한 장점도 있음. 그래서 enum사용
    • enum은 정의부를 바꾸면 알아서 사용하는 쪽에서도 변경되서 편함. 그래서 넓은 범위에 확장해서 써야 한다면 enum을 사용 중
    • 단, enum은 트리쉐이킹이 되지 않아 번들 사이즈에 영향을 줄 수 있지만, const enum을 사용하면 해결할 수 있음.(사실 enum을 사용한다고 해서 전체 파일의 번들 사이즈가 서비스에 영향을 미칠정도로 커지지 않아서 고민하고 있지 않음)
  2. B팀

    • TS에서 타입을 선언하는 용도로 enum이 있어야 하는지 잘 모르겠음.
    • enum이 들어갈 수 있는 값을 타입으로 강제해놓고 객체로 만들어야 맞지 않나 하는 생각을 많이 함.
    • enum은 타입을 위한 문법이라기보다 개발을 위한 문법 같음.
    • enum의 기능이 TS 컴파일러에 의해 동작하는 것이 이상하게 느껴짐. 예를 들어 enum의 리버스 매핑 기능은 컴파일러에서 처리되면 안되는 동작이라고 생각함. 그래서 사용하지 않음
  3. C팀

    • enum 사용하기에는 되게 편한데 JS 컴파일될 때 IIFF로 바뀌는게 크진 않지만, 성능에 영향을 줄 수 있다는 것을 알게되어서 사용하고 있지 않음.

Q) enum외에 const enum을 사용하나요?

  1. A팀

    • enumaration(열거)폴더를 만들어 사용. 이 폴더에 정의한 enum을 외부에서 전역적으로 참조할 때는 const enum을 사용
    • const enum은 빌드과정에서 참조 값만 남기기 때문에 트리쉐이킹이 된다는 장점있음
    • enum도 상수를 쓰기 위한 것으로 생각하기 때문에 const enum을 사용하는게 적절하다고 판단함
    • 물론 isolate 모드를 활성화하고 const enum을 쓰면 안된다는 의견도 있지만, 현재로서는 더 명확하고 정적인 값을 사용할 수 있다는 장점이 더 크다고 생각함
  2. B팀

    • const enum 사용하지 않음.
    • const enumenum과 다르게 직접적인 값으로 치환되기 때문에 전체 네임스페이스에 접근하지 못하고 순회할 수도 없다는 단점을 가지고 있음

트리쉐이킹(tree-shaking)

JS, TS에서 사용하지 않는 코드를 삭제 하는 방식. ES6 이후의 최신 APP 개발 환경에서 웹팩, 롤업 같은 도구로 번들링 작업을 수행할 때 사용하지 않는 코드는 자동으로 삭제 됨.

CommonJS는 트리쉐이킹을 지원하지 않지만, ES6 이후에는 파일 내 특정 모듈만 임포트하면 해당 모듈을 사용하지 않는 파일 코드는 삭제되어 더 작은 크기의 번들링 파일을 생성할 수 있게 되었음

타입을 확인 하는 방법

  • TS에서 typeof, instanceof` 그리고 타입 단언을 사용해 타입을 확인 할 수 있음

  • typeof는 연산하기 전에 피연산자의 데이터 타입을 나타내는 문자열을 반환한다. 반환하는 값은 JS의 7가지 기본 데이터 타입(Boolean, null, undefined, Number, BigInt, String, Symbol)과 Function(함수), 호스트 객체 그리고 object 객체가 될 수 있다.

    typeof 2022; // "number"
    typeof 'kay'; // "string"
    typeof true; // "boolean"
    typeof {}; // "object"
  • TS에는 값 공간과 타입 공간이 별도로 존재함.

  • TS에서 typeof 연산자도 값에서 쓰일 때와 타입에서 쓰일 때의 역할이 다름.

    interface Person {
      first: string;
      last: string;
    }
     
    const person: Person = { first: 'kay', last: 'ko' };
     
    function email(options: { person: Person; subject: string; body: string }) {}
  • 값에 사용된 typeof는 JS 런타임의 typeof 연산자가 됨

    const v1 = typeof person; // 'object'
    const v2 = typeof email; // 'string'
  • 반면 타입에 사용된 typeof는 값을 읽고 TS 타입을 반환함

    type T1 = typeof person; // 타입은 Person
    type T2 = typeof email; // 타입은 (options: { person: Person; subject: string; body: string; }) => void
  • JS Class 사용시 typeof 연산자 주의 사항

    class Developer {
      name: string;
      sleepingTime: number;
     
      constructor(name: string, sleepingTime: number) {
        this.name = name;
        this.sleepingTime = sleepingTime;
      }
     
      const d = typeof Developer;  // 값이 'function'
      type T = typeof Developer;   // 타입이 typeof Developer
    }
    • JS의 Class는 결국 함수이기 때문에 값 공간에서 typeof Developer의 값은 function이 됨
    • 타입 공간에서 typeof Developer의 반환 값은 조금 특이한데, type T에 할당된 Developer는 인스턴스의 타입이 아니라, new 키워드를 사용할 때 볼 수 있는 생성자 함수이기 때문
    const zid: Developer = new Developer('zig', 7);
    type ZigType = typeof zig; // Developer;
    • DeveloperDeveloper 타입의 인스턴스를 만드는 생성자 함수여서 typeof Developer 타입 그 자체인 typeof Developer

    • typeof Developer를 풀어서 설명하면 new (name: string, sleepingTime: number): Developer

    • JS에서 instanceof연산자를 사용하면 프로토타입 체이닝 어딘가에 생성자의 프로토타입 속성이 존재하는지 판단할 수 있음

    • typeof 연산자처럼 instanceof 연잔사의 필터링으로 타입이 보장된 상태에서 안전하게 값의 타입을 정제하여 사용할 수 있음

      let error = unknown;
       
      if (error instanceof Error) {
        shwAlertModal(error.message);
      } else {
        throw Error(error);
      }
  • TS에서 타입 단언을 통해 타입을 강제 할 수 있음(as 사용)

  • 컴파일 단계에서는 타입 단언이 형 변환을 강제할 수 있지만, 런타임에서는 효력을 발휘하지 못함

    const loaded_text: unknown; // 어딘가에서 unknown 타입 값을 전달받았다고 가정
     
    const validateInputText = (text: string) => {
      if (text.length < 10) return '최소 10글자 이상 입력해야 합니다.';
     
      return '정상 값 입니다.';
    };
     
    validateInputText(loaded_text as string); // as 키워드를 사용해서 string으로 강제 하지 않으면 TS 컴파일러 단계에서 에러 발생

2.3 원시 타입

Boolean

  • 오직 truefalse 값만 할당할 수 있는 boolean 타입

  • 아래 코드에서 errorAction.typeERROR_TEXT가 같은지 비교한 결괏값을 boolean 타입으로 변환하는 함수이며 비교식의 결과도 boolean타입을 갖음

  • JS에서는 boolean 원시 값은 아니지만 형 변환을 통해 true / false로 취급되는 Truthy / Falsy 값이 존재 함. 이 값은 boolean 원시 값이 아니므로 TS에서도 boolean 타입에 해당하지 않음

    const isEmpty: boolean = true;
    const isLoading: boolean = false;
     
    function isTextError(errorCode: ErrorCodeType): boolean {
      const errorAction = getErrorAction(errorCode);
      if (errorAction) {
        return errorAction.type === ERROR_TEXT;
      }
     
      return false;
    }

3장 고급 타입

4장 타입 확장하기 & 좁히기

5장 타입 활용하기

6장 TS 컴파일

7장 비동기 호출

8장 JSX에서 TSX로

9장 Hook

10장 상태 관리

11장 CSS-in-JS

12장 TS 프로젝트 관리

13장 TS TS와 객체 지향


참고