[이펙티브 타입스크립트] 7장 코드를 작성하고 실행하기

JIY00N·2023년 9월 7일
0

TypeScript

목록 보기
8/9
post-thumbnail
post-custom-banner

7장 코드를 작성하고 실행하기

이번 장은..

타입과는 관계 없지만, 코드를 작성하고 실행하면서 실제로 겪을 수 있는 문제들에 대해서 다룬다.

아이템 53 타입스크립트 기능보다는 ECMAScript 기능을 사용하기


  • 2010년으로 거슬러 올라가 자바스크립트가 그리 발전하지 않았을 때, 타입스크립트는 자바스크립트에 없는 기능을 지원했었다.
  • 자바스크립트가 충분히 발전한 지금은, 타입스크립트와 자바스크립트의 경계를 모호하게 만들지 않기 위해서 타입스크립트만의 독자적인 기능은 사용하지 않는 것이 좋다.

타입스크립트와 자바스크립트간의 호환성문제로 유의해야 하는 것들

  1. 열거형(enum)
  • 문제점
  1. 숫자 열거형에 0,1,2 이외의 다른 숫자가 할당되면 매우 위험하다.
  2. 상수 열거형은 보통 열거형과 달리 런타임에 완전히 제거된다.
  3. preserveConstEnums 플래그를 설정한 상태의 상수 열거형은 보통의 열거형처럼 런타임 코드에 상수 열거형 정보를 유지한다.
  4. 타입스크립트에서 문자열 열거형은 명목적 타이핑을 사용한다.
// 숫자 열거형
enum Flavor {
  VANILLA = 0,
  CHOCOLATE = 1,
  STRAWBERRY = 2,
}

// 상수 열거형
cosnt enum Flavor {
  VANILLA = 0,
  CHOCOLATE = 1,
  STRAWBERRY = 2,
}

// 문자열 열거형
const enum Flavor {
  VANILLA = 'vanilla',
  CHOCOLATE = 'chocolate',
  STRAWBERRY = 'strawberry'
}

// Flavor를 매개변수로 받는 함수 가정
function scoop(flavor: Flavor){ //... }

// Flavor는 런타임 시점에는 문자열이기 때문에, 자바스크립트에서는 정상
scoop('vanilla');

// 타입스크립트에서는 에러 'vanilla' 형식은 'Flavor' 형식의 매개변수에 할당될 수 없습니다.
scoop('vanilla');

// 따라서, 타입스크립트에서는 열거형을 임포트하고 문자열 대신 사용해야 한다.
import { Flavor } from 'ice-cream';
scoop(Flavor.VANILLA); // 정상
  • 자바스크립트와 타입스크립트에서 동작이 다르기 때문에 문자열 열거형을 사용하지 않는 것이 좋다.
  • 그 대신, 리터럴 타입의 유니온 사용
  1. 열거형만큼 안전하고, 자바스크립트와 호환 가능하다.
  2. 편집기에서 자동완성 기능을 사용할 수 있다.
type Flavor = 'vanilla' | 'chocolate' | 'strawberry';

let flavor: Flavor = 'chocolate'; //정상

function scoop(flavor: Flavor) {
    if (flavor === 'v
    // 자동완성이 'vanilla'를 추천합니다.
    }
  1. 매개변수 속성
  • 문제점
  1. TS 컴파일은 타입제거가 이루어져서 코드가 줄지만, 매개변수 속성으로 코드가 증가
  2. 매개변수 속성은 런타임에는 사용되지만, TS 관점에서는 사용되지 않은 것 처럼 보임
  3. 매개변수 속성과 일반 속성을 섞어서 사용하면 클래스 설계가 혼란스러움
// 클래스를 초기화할 때 속성을 할당하기 위해 생성자의 매개변수를 사용
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// 매개변수 속성(public name) 사용
class Person {
  constructor(public name: string) {}
}

// 설계의 혼란
class Person {
  first: string;
  last: string;
  constructor(public name: string) {
    [this.first, this.last] = name.split(' ');
  }
}
  • 해결책 - 한 가지 속성만 사용(일반속성 추천)
  1. 네임스페이스와 트리플 슬래시 임포트
  • 타입스크립트 초창기 시절 모듈 시스템을 임포트 하기 위한 것
  • ECMAScript 2015가 공식적으로 모듈 시스템을 도입한 이후, 충돌을 피하기 위해 module과 같은 기능을 하는 namespace 키워드를 추가함.
namespace foo {
  function bar() {}
}
/// <reference path="other.ts"/>
foo.bar();

→ 이제는 ECMAScript 2015 스타일의 모듈(import와 export)을 사용하자.

  1. 데코레이터
  • 클래스, 메서드, 속성에 애너테이션을 붙이거나 기능을 추가하는데 사용할 수 있다.
  • 데코레이터는 처음에 앵귤러 프레임워크를 지원하기 위해 추가되었으며 tsconfig.jsonexperimentalDecorators 속성을 설정하고 사용해야 한다.
  • 데코레이터가 표준이 되기 전에는 사용하지 말자.
// tsConfig: {"experimentalDecorators":true}

// 예시: 클래스의 메서드가 호출될 때마다 로그를 남기려면 logged 애너테이션을 정의할 수 있다.
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  @logged
  greet() {
    return 'Hello, ' + this.greeting;
  }
}

function logged(target: any, name: string, descriptor: PropertyDescriptor) {
  const fn = target[name];
  descriptor.value = function () {
    console.log(`Calling ${name}`);
    return fn.apply(this, arguments);
  };
}

console.log(new Greeter('Dave').greet());
// Logs:
// Calling greet
// Hello, Dave

export default {};

🎯 요약

타입스크립트에서 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터를 사용하지 말자

아이템 54 객체를 순회하는 노하우


  • 객체를 순회할 때 겪는 오류..
const obj = {
  one: 'uno',
  two: 'dos',
  three: 'tres',
};

for (const k in obj) {
  const v = obj[k]; // - obj 에 인덱스 시그니처가 없기 때문에 엘리먼트는 암시적으로 'any' 타입입니다.
}
// k의 타입은 string인 반면, obj 객체에는 'one', 'two', 'three' 세 개의 키만 존재한다.
// k와 obj 객체의 키 타입이 서로 다르게 추론되어 오류가 발생함
  • 해결책
interface ABC {
  a: string;
  b: string;
  c: number;
}

// 1. keyof 선언 사용 -> 상수이거나 추가적인 키 없이 정확한 타입을 원할 때
function foo(abc: ABC) {
  let k: keyof ABC;
  for (k in abc) {
    // let k: "a" | "b" | "c"
    const v = abc[k]; // Type is string | number
  }
}

// 2. Object.entries -> 일반적인 경우지만 키와 값의 타입을 다루기 까다롭다.
function foo(abc: ABC) {
  for (const [k, v] of Object.entries(abc)) {
    k; // Type is string
    v; // Type is any
  }
}

🎯 요약

객체를 순회하며 키와 값을 얻으려면, keyof 선언이나 Object.entries 를 사용하자.

아이템 55 DOM 계층 구조 이해하기


  • 아래 코드는 타입스크립트에서 수많은 오류가 발생함
function handleDrag(eDown: Event) {
    const targetEl = eDown.currentTarget;
    ta rgetEl.classList.add(‘dragging1);
    const dragStart = [eDown-clientX, eDown.clientY];
    const handleUp = (ellp: Event) => {
        targetEl.classList.remove('dragging');
        targetEl.removeEventListener('mouseup', handleUp);
        const dragEnd = [ellp.clientX, eUp-dientY];
    console.log('dx, dy =', [0, l].map(i => dragEnd[i] - dragStart[i]));
}
	targetEl.addEventListener('mouseup', handleUp);
}
const div = document.getElementByld('surface');
div.addEventListener('mousedown', handleDrag);
  • TS에서는 DOM 엘리먼트의 계층 구조를 파악하기 용이하다.
  • 계층은 EventTarget > Node > Element > HTMLElement > HTMLButtonElement 와 같다.
타입예시
EventTargetwindow, XMLHttpRequest
Nodedocument, Text, Comment
ElementHTMLElement, SVGElement
HTMLElement<i>, <b>
HTMLButtonElement<button>
function addDragHandler(el: HTMLElement) {
  // mousedown 이벤트 핸들러를 인라인 함수로 만들어 TS에게 문맥 정보 제공
  el.addEventListener('mousedown', (eDown) => {
    const dragStart = [eDown.clientX, eDown.clientY];
    // Event 대신 MouseEvent으로 선언
    const handleUp = (eUp: MouseEvent) => {
      el.classList.remove('dragging');
      el.removeEventListener('mouseup', handleUp);
      const dragEnd = [eUp.clientX, eUp.clientY];
      console.log(
        'dx, dy = ',
        [0, 1].map((i) => dragEnd[i] - dragStart[i])
      );
    };
    el.addEventListener('mouseup', handleUp);
  });
}

const div = document.getElementById('surface');
// #surface 엘리먼트가 없는 경우 체크
if (div) {
  addDragHandler(div);
}

🎯 요약

DOM 에는 타입 계층이 존재한다.
DOM 엘리먼트와 이벤트에는 충분히 구체적인 타입 정보를 사용하거나 TS가 추론할 수 있도록 문맥 정보를 활용해야 한다.

아이템 56 정보를 감추는 목적으로 private 사용하지 않기


  • 타입스크립트에서 private, protected, public과 같은 키워드를 붙이더라도 컴파일 후에는 TS 키워드가 모두 사라진다.
  • 즉, 정보를 감추기 위해 privat 을 사용하면 안된다.
  • 대신 클로저를 사용하자
declare function hash(text: string): number;
class Passwordchecker {
  checkPassword: (password: string) => boolean;

  constructor(passwordHash: number) {
    this.checkPassword = (password: string) => {
      return hash(password) === passwordHash;
    };
  }
}

const checker = new PasswordChecker(hash('s3cret'));
checker.checkPassword('s3cret'); // 결과는 true
  • 장점 - Passwordchecker 생성자 외부에서 passwordHash 변수에 접근할 수 없다 -> 정보 은닉 성공

  • 단점 - passwordHash에 접근하는 메서드 역시 내부에 작성되어야하고, 인스턴스가 생성될때마다 메서드의 복사본이 생성됨 -> 메모리 낭비 발생

  • 비공개 필드 기능 (#)

→ 접두사로 #를 붙여서 타입 체크런타임 모두에서 비공개로 만드는 역할을 한다.

class Passwordchecker {
  #passwordHash: number;
  constructor(passwordHash: number) {
    this.#passwordHash = passwordHash;
  }
  checkPassword(password: string) {
    return hash(password) === this.#passwordHash;
  }
}
const checker = new Passwordchecker(hash('s3cret1'));
checker.checkPassword('secret'); // 결과는 false
checker.checkPassword('s3cret'); // 결과는 true
  • #passwordHash 속성은 클래스 외부에서 접근할 수 없다.

🎯 요약

public, protected, private 접근 제어자는 타입스크립트에서만 강제되고, 런타임에 소용이 없으므로 데이터를 감추고 싶다면, 클로저를 사용하자

아이템 57 소스맵을 사용하여 타입스크립트 디버깅하기


  • 타입스크립트 코드를 실행한다 -> 타입스크립트 컴파일러가 생성한 자바스크립트 코드를 실행한다는 의미
    변환된 자바스크립트코드는 복잡해서 디버깅하기 어려움 -> 이를 해결하기위해 Source Map 이 만들어짐
  • 소스맵(Source Map)
  1. 개발자가 작성한 '원본 코드'에서의 위치와 각종 전처리, 컴파일 과정을 거쳐 자바스크립트 엔진이 실행하는 '변환된 코드'를 서로 매핑하는데 사용되는 map이다.
  2. 원본코드가 그대로 유출되지 않도록 주의해야한다.
  • 타입 체커가 코드를 실행하기 전에 많은 오류를 잡을 수 있지만, 디버거를 대체할 수는 없다.
  • 따라서, 소스맵을 사용해서 제대로 된 타입스크립트 디버깅 환경을 구축해야 한다.

🎯 요약

소스맵을 사용해서 런타임에 타입스크립트 코드를 디버깅하자.

profile
블로그 이전 했습니다. https://yoon-log.vercel.app/
post-custom-banner

0개의 댓글