이펙티브 타입스크립트:8장 아이템58-61

이펙티브 타입스크립트 북스터디 8장 아이템58-61

타입스크립트로 마이그레이션 하기

8장에서는 큰 사이즈의 자바스크립트라고 할지라도 꾸준하게 타입스크립트로 마이그레이션 할수 있게 해주는 방법을 소개하고 있습니다. 한꺼번에 많은 코드를 타입스크립트로 전화할 수 없기 때문에, 대규모 프로젝트를 마이그레이션 할 때는 점진적으로 마이그레이션이 필요합니다.


아이템58. 모던 자바스크립트로 작성하기

  • 모던 자바스크립트란 ? -> 최신버전의 자바스크립트로, 현재는 ES2015(ES6) 버전부터 모던 자바스크립트라고 부르고 있다.

타입스크립트는 타입체크 기능도 있지만, 타입스크립트 코드를 특정 버전의 자바스크립트로 컴파일 하는 기능도 가지고 있다.

책에서는 어디서부터 마이그레이션을 시작해야할지 몰라 막막하다면 옛날 버전의 자바스크립트 코드를 최신 버전으로 바꾸는 작업부터 시작해보길 권하고 있다.

  • 타입스크립트를 도입할 때 가장 중요한 기능
    • ECMASCRIPT 모듈
    • ES2015 클래스

ECMASCRIPT 모듈 사용하기

  • CommonJS 모듈 시스템
//CommonJS
//a.js
const b = require('./b');
console.log(b.name);

//b.js 
const name = 'Module B';
module.exports = {name};
  • ES 모듈
//ECMASCript module
//a.ts
import * as b from './b';
console.log(b.name);

//b.ts
export const name = 'Module B';

프로토타입 대신 클래스 사용하기

  • 객체를 프로토타입으로 구현
function Person(first,last){
    this.first = first;
    this.last = last;
}

Person.prototype.getName = function(){
    return this.first + '' + this.last;
}

const marie = new Person('Marie', 'Curie');
const personName = marie.getName();
  • 프로토타입 기반 객체를 클래스 기반 객체로 변경
class Person {
  first: string
  last: string

  constructor(first: string, last: string) {
    this.first = first
    this.last = last
  }

  getName() {
    return this.first + ' ' + this.last
  }
}

const marie = new Person('Marie', 'Curie')
const personName = marie.getName()

var 대신 let/const 사용하기

for(;;) 대신 for-of 또는 배열 메서드 사용하기

for (var i = 0; i < array.length; i++) {
  const el = array[i]
  // ...
}
  • 모던 자바스크립트의 for-of 루프
    • for-of 루프는 코드가 짧고 인덱스 변수를 사용하지 않기 때문에 실수를 줄일 수 있다.
declare let array: number[]
for (const el of array) {
  // ...
}
  • index 변수가 필요한 경우엔 forEach 메서드를 사용한다.
declare let array: number[]
array.forEach((el, i) => {
  // ...
})

함수표현식보다 화살표 함수 사용하기

단축 객체 표현과 구조분해 할당 사용하기

  • 단축객체표현
    • pt 객체를 생성하는 코드
    • 변수와 객체 속성의 이름이 같다. 따라서 두번째 코드와 같이 간단하게 작성할 수 있다.
const x = 1,
  y = 2,
  z = 3
const pt = {
  x: x,
  y: y,
  z: z,
}
const x = 1,
  y = 2,
  z = 3
const pt = { x, y, z }

함수매개변수 기본값 사용하기

자바스크립트에서 함수의 모든 매개변수는 선택적이며, 매개변수를 지정하지 않으면 undefined 로 간주된다.

function log2(a, b) {
  console.log(a, b)
}
log2()
// undefined, undefined
  • 옛날 자바스크립트에서 매개변수 기본값 지정
function parseNum(str, base) {
  base = base || 10
  return parseInt(str, base)
}
  • 모던 자바스크립트에서 매개변수에 기본값 지정
    • 코드가 간결해진다
    • base 가 선택적 매개변수라는 것을 명확하게 나타내는 효과를 줄 수 있다.
    • 매개변수에 타입 구문을 쓰지 않아도 된다
function parseNum(str, base = 10) {
  return parseInt(str, base)
}

프로미스나 콜백 대신 async/await 사용하기

요약

  • 타입스크립트의 개발환경은 모던 자바스크립트도 실행할 수 있으므로 모던 자바스크립트의 최신 기능들을 적극적으로 사용해야한다.

아이템59. 타입스크립트 도입 전에 @ts-check 와 JSDoc 으로 시험해보기

@ts-check 지시자를 사용하면 타입스크립트 전환시에 어떤 문제가 발생하는지 미리 볼 수 있다. @ts-check 지시자는 느슨한 수준으로 타입체크를 수행한다 (noImpllicitAny 설정을 해제한 것보다 헐거운 체크를 수행함)

  • noImplicitAny 는 컴파일러 옵션 중 하나로, 암시적 any를 허용하지 않도록 설정하는 옵션이다.

ts-check 지시자 사용하기

  • 아래는 @ts-check 지시자를 사용해서 자바스크립트에도 불가하고 타입 체크가 동작한다.
const person = { first: 'Grace', last: 'Hopper' }
2 * person.first
// ~~~~~~~~~~~~ 

person.first의 타입은 string 으로 추론됨. 따라서 2 * person.first 에는 타입 불일치 오류가 발생한다.

선언되지 않은 지역변수

  • 어딘가에 숨어있는 변수라면, 변수를 제대로 인식할 수 있게 별도로 타입선언 파일을 만들어야 한다.
// @ts-check
console.log(user.firstName)
// ~~~~ Cannot find name 'user'
  • user 을 찾을 수 없어 오류가 발생하고 있다. 아래에서 user 을 선언하여 오류를 해결한다.
interface UserData {
  firstName: string
  lastName: string
}
declare let user: UserData

알 수 없는 라이브러리

서드파티 라이브러리 (예 : jquery) 를 사용하는 경우, 서드파티 라이브러리의 타입 정보를 알아야 한다.

// @ts-check
$('#graph').style({ width: '100px', height: '100px' })
// ~~~~~ Cannot find name '$'

jquery 타입선언을 설치해 오류를 제거한다.

npm install --save-dev @types/jquery

이제 오류가 제이쿼리의 사양과 관련된 내용으로 변경된 것을 확인할 수 있다.

// @ts-check
$('#graph').style({ width: '100px', height: '100px' })
// ~~~~~ Property 'style' does not exist on type 'JQuery<HTMLElement>'

부정확한 JSDoc

프로젝트에 이미 JSDoc 스타일의 주석을 사용중이었다면 @ts-check 지시자를 설정하는 순간부터 기존 주석에 타입 체크가 동작하게 되고 수많은 오류가 발생한다.

// @ts-check
/**
 * Gets the size (in pixels) of an element.
 * @param {Node} el The element
 * @return {{w: number, h: number}} The size
 */
function getSize(el) {
  const bounds = el.getBoundingClientRect()
  // ~~~~~~~~~~~~~~~~~~~~~ Property 'getBoundingClientRect'
  //                       does not exist on type 'Node'
  return { width: bounds.width, height: bounds.height }
  // ~~~~~~~~~~~~~~~~~~~ Type '{ width: any; height: any; }' is not
  //                     assignable to type '{ w: number; h: number; }'
}
  • 첫번째 오류 : DOM 타입 불일치
    • getBoundingClientRect() 는 Node 가 아니라 Element 에 정의되어있기 때문
  • 두번째 오류 : return 타입 불일치
    • @return 타입에 명시된 타입과 실제 반환 타입이 맞지 않는다.

타입스크립트 언어 서비스는 타입을 추론해서 JSDoc 을 자동으로 생성해준다. 자동생성기능은 타입 정보를 빠르게 추가할 수 있기 때문에 유용하지만 실제로 잘 동작하지 않는 경우도 있다. 주석이 코드 분량을 늘려서 로직을 해석하는데 방해가 될 수 있기 때문에 @ts-check 지시자와 JSDoc 을 너무 장기간 사용하는 것은 좋지 않다. js -> ts 마이그레이션의 궁극적인 목표는 자바스크립트에 JSDoc 주석이 있는 형태가 아니라 모든 코드가 타입스크립트 기반으로 전환되는 것이다.

요약

  • 파일 상단에 // @ts-check 를 추가하면 자바스크립트에서도 타입 체크를 수행할 수 있다.
  • 전역 선언과 서드파티 라이브러리의 타입 선언을 추가하는 방법을 익힌다
  • JSDoc 은 주석은 중간 단계이기 때문에 너무 공들일 필요가 없다. 최종목표는 ts 로 된 타입스크립트 코드이다.

아이템60. allowJS 로 타입스크립트와 자바스크립트 같이 사용하기

대규모 프로젝트는 한꺼번에 작업하는 것이 불가능하므로 점진적으로 전환할 수 있어야 하고, 그러러면 마이그레이션 기간중에 자바스크립트와 타입스크립트가 동시에 동작할 수 있도록 해야 한다.

allowJS 컴파일러 옵션

  • 타입스크립트와 자바스크립트가 공존하는 방법의 핵심
  • 타입스크립트 파일과 자바스크립트 파일을 서로 임포트 할 수 있게 해준다.
  • 기존 빌드 과정에 타입스크립트 컴파일러를 추가하기 위해서는 allowJS 옵션이 필요하다

아이템61. 의존성 관계에 따라 모듈 단위로 전환하기

프로젝트 내에 존재하는 모듈은 서드파티 라이브러리에 의존하지만 서드파티 라이브러리는 해당 모듈에 의존하지 않기 때문에 먼저 서드파티 라이브러리 타입 정보를 해결해야한다. 일반적으로 @types 모듈을 설치한다

  • ex ) lodash : @types/lodash - lodash 의 타입 정보를 담고 있다.

madge

madge 라는 도구를 통해 의존성 관계도를 얻을 수 있다. 모듈단위 마이그레이션을 시작하기 전에, 모듈 간의 의존성 관계를 시각화 하는데 도움을 준다.

선언되지 않은 클래스 멤버

  • 자바스크립트 : 클래스에서 멤버변수를 선언할 필요가 없다
  • 타입스크립트 : 명시적으로 멤버변수를 선언해야한다.

멤버변수를 선언하지 않은 클래스가 있는 js 파일을 ts 로 바꾸면 오류가 발생한다.

class Greeting {
  constructor(name) {
    this.greeting = 'Hello'
    // ~~~~~~~~ Property 'greeting' does not exist on type 'Greeting'
    this.name = name
    // ~~~~ Property 'name' does not exist on type 'Greeting'
  }
  greet() {
    return this.greeting + ' ' + this.name
    // ~~~~~~~~              ~~~~ Property ... does not exist
  }
}

quick fix 1 quick fix 2 위와 같이 quick fix 기능으로 해결할 수 있다.

class Greeting {
  greeting: string
  name: any
  constructor(name) {
    this.greeting = 'Hello'
    this.name = name
  }
  greet() {
    return this.greeting + ' ' + this.name
  }
}

greetring 타입은 string 으로 정확히 추론되었지만 name 타입의 경우 any 로 추론되었다. quick fix 를 적용한 후에는 다시 살펴보고 any 로 추론된 부분은 직접 수정이 필요하다.


요약

  • 마이그레이션의 첫 단계는 서드파티 모듈과 외부 API 호출에 대한 @types 를 추가하는 것이다.

아이템62. 마이그레이션의 완성을 위해 noImplicitAny 설정하기

js 프로젝트를 ts 로 전환하는데 마지막 단계는 noImplicitAny 를 설정하는 것이다.

Chart 클래스에서 quick fix 를 통해 모든 멤버변수 추가 기능을 사용했고, 문맥의 정보가 부족해 any 타입으로 추론되었다.

class Chart {
  indices: any 
  // ...
}

indices는 index 의 복수형으로, 숫자의 배열인 것처럼 보이니 number[] 타입으로 수정

class Chart {
  indices: number[]

  // ...
}

사실 Chart 클래스는 아래와 같이 생겼다

class Chart {
  indices: number[]
  // END
  getRanges() {
    for (const r of this.indices) {
      const low = r[0] // Type is any
      const high = r[1] // Type is any
      // ...
    }
  }
  // HIDE
}

indices 의 타입은 이중배열 number[][] 또는 [number, number[]] 가 되어야 정확하다. 이렇게 noImplicitAny 설정을 하지 않으면 타입 체크는 매우 허술해진다.

noImplicitAny 설정하면 다음과 같은 오류가 발생한다

class Chart {
  indices: number[]
  // END
  getRanges() {
    for (const r of this.indices) {
      const low = r[0]
      // ~~~~ 'Number' 형식에 인덱스 시그니척 없으므로 요소에 암시적으로 'any' 형식이 있습니다.
      const high = r[1]
      // ~~~~ 'Number' 형식에 인덱스 시그니처가 없으므로 요소에 암시적으로 'any' 형식이 있습니다.
    }
  }
  // HIDE
}
// END

noImplicitAny는 상당히 엄격한 설정이며, strictNullChecks 같은 설정을 하지 않더라도 대부분의 타입 체크를 적용한 것으로 볼 수 있다. 그러나 최종적으로 가장 강력한 설정은 ‘strict: true’ 이다.

요약

  • noImplicitAny 설정을 활성화하여 마이그레이션의 마지막 단계를 진행해야 합니다. noImplicitAny 설정이 없다면 타입 선언과 관련된 실제 오류가 드러나지 않는다.

디지엠유닛원 주식회사

  • 대표이사 권혁태
  • 개인정보보호책임자 정경영
  • 사업자등록번호 252-86-01619
  • 주소
    서울특별시 금천구 가산디지털1로 83, 6층 601호(가산동, 파트너스타워)
  • 이메일 unit1@dgmit.com