JavaScript 번들러의 이해 — (2) TypeScript 모듈

권세규
네이버 플레이스 개발 블로그
6 min readJun 17, 2022

--

이 글은 JavaScript 번들러의 이해 — (1) JavaScript 모듈에 이어지는 글로, TypeScript에서 모듈을 어떻게 처리하고 있는지를 알아봅니다. 만약 TypeScript를 학습하시지 않은 분들은 (3) 번들러 개론으로 넘어가셔도 괜찮습니다.

모던 웹 개발에 빠지지 않는 TypeScript는 2014년 1.0 버전이 출시되었습니다. ES6이 나오기 전이었지요. 때문에 CommonJS 등의 규격을 지원합니다.

TypeScript는 모듈 기술 문법으로 ESM 형식을 사용하지만 CommonJS로 작성된 모듈을 import할 수 있으며, CommonJS나 AMD, UMD 형식으로 소스를 출력할 수도 있습니다. 이 모든 일들은 TypeScript에서 제공하는 트랜스파일러인 tsc에서 해줍니다.

또한 tsc는 모듈을 인식하고 처리할 수 있지만, 합쳐주지는 않습니다. tsc는 ts 파일 (혹은 tsx) 을 js 파일로 바꿔줄 뿐입니다.

TypeScript 모듈에 관련해서 반드시 숙지해야 하는 사항은 아래의 2가지가 있습니다. 이외에는 JavaScript 모듈을 가져다 쓰는 것과 동일합니다.

타입 선언 파일 .d.ts

.d.ts 파일은 아마도 초심자들을 가장 애먹게 하는 것이 아닐까 싶은 존재입니다. TypeScript로 React 개발을 시도했다면, 한 번 쯤 이런 에러를 본 적이 있으실 겁니다.

Could not find a declaration file for module ‘react’.

당연한 이야기지만 JavaScript 모듈은 JavaScript 파일입니다. 즉, TypeScript 문법이 아닙니다. 따라서 타입 정보가 기술되어 있지 않습니다. tsc는 이것을 암묵적인(implicit) any 타입으로 간주합니다.

tsconfig.json에 별도 지정이 없는 경우, tsc는 이런 상황에서 에러를 냅니다. noImplicitAnyfalse로 설정하면 에러야 안나겠지만, 타입이 하나도 없을테니 개발은 여전히 불편할 것입니다.

TypeScript로 작성한 모듈은 타입 정보를 tsc가 유추할 수 있습니다. 그러나 JavaScript로 작성한 모듈은 그렇지 않기 때문에, 추가적인 타입 정보가 필요합니다. .d.ts 파일은 타입을 기술하는, “구현 없는 타입 선언(Type Declaration)” 파일입니다. C언어의 헤더 파일과 비슷한 역할을 한다고 볼 수 있습니다.

.d.ts파일은 컴파일러 옵션에서 declarationtrue로 설정하면, 트랜스파일 할 때 같이 생성할 수 있습니다. ts 소스와 트랜스파일 된 js 파일을 보면서 비교해보세요.

이렇게 작성된 .d.ts파일은 tsc의 Resolution Rule에 의해 읽혀집니다. 예컨데 import from ‘./vector'를 만나면, tsc는 처음에는 vector.ts 를 찾고, 없으면 vector.d.ts 를 찾습니다.

요즘은 JavaScript 라이브러리도 TypeScript로 개발하는 추세지만, 모두가 그렇지는 않습니다. 옛날에 개발된 도구(ex: React)들은 개발이 중단되었거나, TypeScript로 전환이 어려워서 타입 선언을 제공하지 않는 경우가 많습니다. 이런 도구들은 “@types/library-name” 이라는 컨벤션으로 npm에서 타입 선언만 따로 배포합니다.

라이브러리에 타입 선언 파일이 포함된 경우 TS 마크가, 별도 타입 선언 패키지가 있는 경우 DT 마크가 보입니다.
라이브러리에 타입 선언 파일이 포함된 경우 TS 마크가, 별도 타입 선언 패키지가 있는 경우 DT 마크가 보입니다.

esModuleInterop 옵션

우여곡절 끝에 “@types/react”를 설치하여 .d.ts 파일까지 준비를 한 초심자는, 또다른 에러를 만나고 절망합니다.

Module can only be default-imported using the allowSyntheticDefaultImports flag error.

1편에서 설명한 ESM과 CommonJS의 차이점이 혹시 기억나시나요?

export/import를 통해 객체(Object)만을 내보낼 수 있으며, 호출할 수 없다는 특징이 있습니다.

결론부터 말씀드리면 tsconfig.json의 esModuleInterop 옵션은 이 차이 때문에 생긴 옵션입니다. TypeScript는 ESM 이전에 개발되었기 때문에, 아래와 같은 논리로 모듈 import를 처리합니다.

  1. import * as name from 'path'const name = require('path') 와 동일
  2. import name from 'path'const name = require('path').default 와 동일

그런데 CommonJS의 경우 1번에서 반환한 것이 호출 가능한 함수여도 문제가 없는 반면, ESM에서는 그것이 허용되지 않습니다. 그러나 무조건 스펙대로만 하기엔 못쓰는 모듈(ex: express.js, react.js)이 너무 많았고, 이 괴리감을 메꾸기 위해 esModuleInterop 옵션이 TypeScript 2.7에 추가되었습니다.

이 플래그를 활성화할 경우, 트랜스파일 과정에서 아래와 같이 ESM 여부를 분기하여 모듈을 감싸주게 됩니다.

에러에 있는 allowSyntheticDefaultImportsesModuleInterop 플래그가 활성화 될 경우 같이 설정이 되는데요, CommonJS 모듈에서 default 값을 정하지 않은 경우, module.exports.defaultmodule.exports 와 동일값을 갖도록 자동으로 추가해주는 옵션입니다.

사용하는 것을 넘어서서 TypeScript로 모듈이나 라이브러리를 개발할 경우, 공식문서에 지침이 자세히 나와있기 때문에 참고해주시면 되겠습니다.

다음 글에서는 번들러의 일반적인 성질과 기능에 대해 알아보겠습니다.

이전 편 읽기

(1) JavaScript 모듈

다음 편 읽기

(3) 번들러 개론
(4) Webpack 및 다른 번들러들

--

--