Published on

코드를 작성하고 실행하기 ~ 타입스크립트로 마이그레이션하기 - Item 57 ~ 59

코드를 작성하고 실행하기 ~ 타입스크립트로 마이그레이션하기 - Item 57 ~ 59

Item 57) 소스맵을 사용하여 타입스크립트 디버깅하기

  • 타입스크립트 코드는 독립적으로 실행할 수 없고 엄밀히 따지면 타입스크립트 컴파일러가 생성한 자바스크립트 코드를 실행하는 것

    • 컴파일러뿐 아니라 압축기나 전처리기처럼 기존 코들르 다른 형태의 코드로 변환하는 도구들도 모두 해당됨
  • 디버거는 런타임에 동작하며, 현재 동작하는 코드가 어떤 과정을 거쳐 만들어진 것인지 알지 못하기때문에 디버깅 문제를 해결하기 위해 브라우저 제조사들은 서로 협력하여 소스맵(source map)이라는 해결책을 내놓음

  • 대부분의 브라우저와 많은 IDE는 소스맵을 지원함

  • 오래된 브라우저에서 asyncawait를 지원하기 위해, 타입스크립트는 이벤트 핸들러를 상태 머신으로 재작성하하므로 원본 코드와 동일하게 동작하지만 코드의 형태는 매우 다른 모습을 띠게 됨

  • 이와 같이 변환된 코드를 읽기위해서는 소스맵이 필요한데, 이 설정은 tsconfig.json에서 sourceMap 옵션으로 제어할 수 있음

    {
      "compilerOptions": {
        "sourceMap": true
      }
    }
    
    • 해당 옵션을 활성화한 후 컴파일을 실행하면 각 .ts파일에 대해서 .js.js.map 두개의 파일을 생성함
    • 소스맵이 .js 파일과 함께 있으면 브라우저의 디버거에서 새로운 index.ts 파일이 나타나고 원하는 대로 브레이크 포인트를 설정해서 변수를 확인할 수 있음

소스맵에 대해 알아야할 사항

  • 타입스크립트와 함께 번들러나 압축기를 사용하고 있다면, 각자의 소스맵을 생성하게 됨
    • 이상적인 디버거 환경을 위해 원본 타입스크립트 소스로 매핑되도록해야함
  • 상용 환경(production)에 소스맵이 유출되고 있는지 꼭 확인해야 함
    • 소스맵에 원본 코드 인라인 복사본이 포함되어 있다면 공개해서는 안 될 내용이 들어 있을 수 있음
  • 타입 체커가 코드를 실행하기 전에 많은 오류는 잡을 수 있지만, 디버거를 대체할 수 없으므로 제대로 된 타입스크립트 디버깅 환경을 구축하는 것이 좋음

Item58) 모던 자바스크립트로 작성하기

  • 타입스크립트는 타입 체크 기능 외에, 자바스크립트로 컴파일하는 기능도 가지고 있음
    • 즉, 타입스크립트 컴파일러를 자바스크립트 트랜스 파일러로 사용할 수 있는데 타입스크립트는 자바스크립트의 상위 집합이므로 최신 버전의 자바스크립트 코드를 옛날 버전의 자바스크립트로 변환할 수 있음
    • 따라서 마이그레이션 작업 시, 자바스크립트 프로젝트를 타입스크립트로 바로 바꾸기보다는 옛날의 자바스크립트를 모던 자바스크립트로 작성한 후 도입하는 것을 권장

ECMAScript 모듈 사용하기

  • ES2015 이전에는 코드를 개별 모듈로 분할하는 표준 방법이 없지만, 지금은 개별 모듈로 분할하는 방법이 많아짐
    • 여러 개의 <script> 태그를 사용하기, 직접 갖다 붙이기, Makefile 기법, require 구문, define 콜백, 타입스크립트의 모듈시스템 등등
  • ES2015부터는 importexport 키워드를 사용하는 것이 표준이 되어 있음
  • 마이그레이션 대상인 자바스크립트 코드가 단일 파일이거나 비표준 모듈 시스템이라면 ES 모듈로 전환하는 것이 좋음
    • ES 모듈 시스템을 사용하기 위해 프로젝트에 따라 webpack이나 ts-node 같은 도구가 필요한 경우도 있음

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

  • 과거에는 프로토타입 기반의 객체 모델을 사용했으나 견고하게 설계된 클래스 기반 모델을 선호했기때문에 결국 ES2015에 class 키워드를 사용하는 클래스 기반 모델이 도입됨
  • 프로토타입으로 구현한 객체보다 클래스로 구현한 객체가 문법이 더 간결하고 직관적이므로 클래스 문법에 익숙하지 않더라도, 타입스크립트 언어 서비스를 활용하면 클래스를 간단히 작성할 수 있음

var 대신 let/const 사용하기

  • 자바스크립트 var 키워드의 스코프 규칙에 문제가 있으므로 letconst를 사용해 스코프 문제를 피할 수 있음
  • var 키워드 대신 let, const를 사용해 제대로된 블록 스코프 규칙을 가지고, 일반적으로 인지 가능한 방식으로 구현할 수 있음

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

  • 과거에는 자바스크립트에서 배열을 순회할 때 C 스타일의 for 루프를 사용했지만 모던 자바스크립트에서는 for-of 루프를 사용하여 실수를 줄일 수 있으며, 인덱스 변수가 필요한 경우엔 forEach 메서드를 사용

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

  • this 키워드는 일반적인 변수들과는 다른 스코프 규칙을 가지므로, 화살표 함수를 사용해 상위 스코프의 this를 참조할 수 있게 의미를 통일할 수 있으며 인라인 또는 콜백함수에서는 화살표 함수가 더 직관적인 장점이 있음
  • 컴파일러 옵션에 noImplicitThis(또는 strict)를 설정하면 this 바인딩 관련된 오류를 표시해 주므로 설정하는 것이 좋음

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

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 }
    
  • 앞의 두 예제 중 후자의 코드가 더 간결하고 중복된 이름을 생략하기 때문에 가독성이 좋고 실수가 적음

  • 화살표 함수 내에서 객체를 반환할 때는 소괄호로 감싸야 함

    ;['A', 'B', 'C'].map((char, idx) => ({ char, idx }))
    // [ { char: 'A', idx: 0 }, { char: 'B', idx: 1 }, { char: 'C', idx: 2 } ]
    
  • 객체의 속성 중 함수를 축약해서 표현하는 방법은 다음과 같음

    const obj = {
      onClickLong: function (e) {
        // ...
      },
      onClickCompact(e) {
        // ...
      },
    }
    
    • 객체 구조 분해를 사용해 아래와 같이 작성할 수 있음

      // const props = obj.props
      // const a = props.a
      // const b = props.b
      
      // 줄여서 작성
      // const { props } = obj
      // const { a, b } = props
      
      // 극단적인 예시
      const {
        props: { a, b },
      } = obj
      
    • 구조 분해 문법을 통해 기본값 설정

      const { a = 'default' } = obj.props
      
      • 배열에서도 구조 분해 문법을 사용해 튜플처럼 사용할 수 있음

        const point = [1, 2, 3]
        const [x, y, z] = point
        const [, a, b] = point // 첫번째 요소 무시
        
    • 함수 매개변수에서도 구조 분해 문법을 사용할 수 있음

      const points = [
        [1, 2, 3],
        [4, 5, 6],
      ]
      points.forEach(([x, y, z]) => console.log(x + y + z))
      // 6, 15을 출력함
      

함수 매개변수 기본값 사용

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

  • 모던 자바스크립트에서는 매개변수에 기본값을 직접 지정할 수 있음

    function parseNum(str, base = 10) {
      return parseInt(str, base)
    }
    
    • 매개변수에 기본값을 지정하면 base가 선택적 매개변수라는 것을 명확히 나타낼 수 있으며 기본값을 기반으로 타입 추론이 가능하므로 타입스크립트로 마이그레이션할 때 매개변수에 타입 구문을 쓰지 않아도 됨(⭐)

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

  • async와 await를 사용하면 코드가 간결해져서 버그나 실수를 방지할 수 있으며 비동기 코드에 타입 정보가 전달되어 타입 추론을 가능하게 함

    async function getJSON(url: string) {
      const response = await fetch(url)
      return response.json()
    }
    

타입스크립트에 use strict 넣지 않기

  • ES5에서는 버그가 될 수 있는 코드 패턴에 오류를 표시해주는 엄격 모드가 도입됐지만 타입스크립트에서 수행되는 안전성 검사가 엄격 모드보다 훨씬 더 엄격한 체크를 하므로 타입스크립트에서서 use strict는 무의미 함
    • 타입스크립트 코드에 use strict를 쓰지 않고, 대신 alwaysStrict 설정을 사용해야 함

자바스크립트 표준 단체 TC39는 매년 새로운 기능을 발표하고, 타입스크립트 개발팀은 자바스크립트 표준화 4단계 중 3단계 이상의 기능들을 타입스크립트 내에 구현하고 있으므로 표준화 완성 여부에 상관없이 표준화 3단계 이상의 기능들은 타입스크립트 내에서 사용할 수 있음

Item59) 타입스크립트 도입 전에 @ts-check와 JSDoc으로 시험해 보기

  • @ts-check 지시자를 사용하면 타입스크립트 전환시에 어떤 문제가 발생하는지 미리 시험해 볼 수 있음
    • 타입 체커가 파일을 분석하고, 발견된 오류를 보고하도록 지시하지만 매우 느슨한 수준의 타입 체커이므로 컴파일러 설정 옵션에서 noImplicitAny 설정을 해제한 것보다 헐거운 체크를 함
// @ts-check
const person = { first: 'Grace', last: 'Hopper' }
2 * person.first
// 산술 연산 오른쪽은 'any', 'number', 'bigint'
// 또는 열거형 형식이어야 합니다.
  • person.first의 타입은 string으로 추론되었고, 2 * person.first는 타입 불일치 오류가 되었음
  • 지시자 덕분에 자바스크립트임에도 불구하고 타입 체크가 동작함

선언되지 않은 전역 변수

  • 변수를 선언할 때 보통 let이나 const를 사용하지만 어딘가에 숨어있는 변수라면, 변수를 제대로 인식할 수 있게 별도로 타입 선언 파일을 만들어야 함

    // @ts-check
    // console.log(user.firstName);
    // 'user' 이름을 찾을 수 없습니다.
    
  • user 선언을 위해 types.d.ts 파일을 만듦

    interface UserData {
      firstName: string
      lastName: string
    }
    declare let user: UserData
    
    • 타입 선언 파일을 만들면 오류가 해결됨

    • 선언 파일을 찾지 못하는 경우는 트리플 슬래시 참조를 사용하여 명시적으로 임포트 할 수 있음

      // @ts-check
      /// <reference path="./types.d.ts" />
      console.log(user.firstName) // 정상
      

DOM 문제

  • 웹브라우저에서 동작하는 코드라면, 타입스크립트는 DOM 엘리먼트 관련된 부분에 수많은 오류를 표시하게 됨

    // @ts-check
    const ageEl = document.getElementById('age')
    ageEl.value = '12'
    // 'HTMLElement' 유형에 'value' 속성이 없습니다.
    
    • HTMLInputElement 타입에는 value 속성이 있지만, document.getElementById는 더 상위 개념인 HTMLElement 타입을 반환하는 것이 오류의 원인이므로 확실한 엘리먼트에 대해서 타입 단언문으로 명시해야 함

      // @ts-check
      const ageEl = /** @type {HTMLInputElemnt} */ document.getElementById('age')
      ageEl.value = '12' // 정상
      
      • JSDoc@type 구문을 사용할 때는 타입을 감싸는 중괄호가 필요함

부정확한 JSDoc

  • 자바스크립트 환경에서도 @ts-check 지시자와 JSDoc 주석이라면 타입스크립트와 비슷한 경험을 작업이 가능함
  • 특별한 작업이 필요없으므로 점진적 마이그레이션 과정 중에는 유용하지만, 장기간 사용하지 않는 것이 좋음
    • 주석이 코드 분량을 늘려서 로지글 해석하는 데 방해가 될 수 있음
  • 타입스크립트는 ts파일에서 가장 잘 동작하며, 마이그레이션의 궁극적인 목표는 자바스크립트에 JSDoc 주석이 있는 형태가 아니라 모든 코드가 타입스크립트 기반으로 전환되는 것
    • 다만, 이미 JSDoc 주석으로 타입 정보가 많이 담겨 있는 프로젝트라면 @ts-check 지시자만 간단히 추가해서 기존 코드에 타입 체크를 실해하고 초기 오류를 빨리 잡아낼 수 있다는 점은 기억해야 함

Referenced

  • 댄 밴더캄, 『이펙티브 타입스크립트』, 인사이트(2021.11.4), 281 ~ 306p