학습 출처: [Typescript Handbook - Modules]
typescript를 사용하다보면 tsconfig.json
에서 관련 설정들을 조정해주어야 할 때가 잦았다. 각 옵션이 무엇을 의미하는지, 또 어떻게 typescript는 동작을 하는지 조금 늦게나마 학습하고 기록으로 정리하는 중이다.
그 중, 자바스크립트에서의 Module
개념은 반드시 알아야 하는 녀석이라 생각하여 우선적으로 정리한다.
In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module.
타입스트립트에서 모듈의 정의이다. 즉, 파일에 import, export문이 없으면 모듈이 아니다. 모듈은 각자의 스코프가 존재한다. 반면 모듈이 아닌 파일은 script
라 부르며, 스크립트는 전역 스코프
에서 접근이 가능하다.
우리는 개발 과정에서 거의 필연적으로 모듈을 호출하고, 모듈로 코드를 분리해 코드를 작성한다. typescript는 컴파일 과정을 통해 javascript로 변환된다. 타입스크립트의 tsc(typescript compiler)가 어떤 형태의 자바스크립트 코드를 읽어들여서 어떤 형태의 자바스크립트 코드로 변환할 지를 tsconfig.json
파일을 통해 지시받는다. 그래서 tsc가 모듈의 경로를 해석하여 적절한 파일로 연결해주고, 잘 컴파일 할 수 있도록 설정해주는 것이 중요하다.
그래서 tsconfig.json의 옵션들 중에서 모듈과 관련된 옵션들을 몇 개 추려서 공부해보았다.
만약 어떤 파일(가령 .js 파일)이 ESM으로 결정되었으면, Node.js는 require와 CommonJS module을 평가 과정에 주입하지 않는다. 만약 해당 파일에서 require, CommonJS module을 사용하고 있다면 충돌이 발생한다.
반면, 어떤 파일이 CJS로 결정되었으면, 파일 내에서의 import
, export
키워드의 사용은 충돌을 발생시킬 것이다.
참고 - What the heck are CJS, AMD, UMD, and ESM in Javascript?
스택오버플로우 Why we need "nodenext" typescript compiler option when we have "esnext"?
이 질문글의 답변에는 typescript의 module 옵션에 대한 설명이 꽤나 친절하게 적혀있었다. 그런데도 나는 이해를 잘 하지 못했다.
그래서 공식 문서를 뒤져봤고, The module output format 섹션을 읽어본 결과,
module 옵션은 컴파일러가 어떤 형태로 모듈을 내보낼지 결정하는 (emit)옵션이었다.
import sayHello from "greetings";
sayHello("world");
가령 위 코드는 tsconfig.json의 module 옵션에 따라 컴파일 결과가 달라질 수 있다는 것!
Module resolution is the process of taking a string from the import or require statement, and determining what file that string refers to.
TypeScript includes two resolution strategies: Classic and Node. Classic, the default when the compiler option module is not commonjs, is included for backwards compatibility. The Node strategy replicates how Node.js works in CommonJS mode, with additional checks for .ts and .d.ts.
위 인용구는 Module의 import/require 구문의 문자열이 해당 문자열이 의미하는 파일로 결정되는지에 대한 과정인 Module resolution
에 대한 설명이다.
타입스크립트는 Classic, Node 이렇게 두 resolution 전략을 가지고 있다. Classic은 타입스크립트 1.6버전 이전에 사용된 옵션이라 모던 타입스크립트에서는 사용하지 않는 옵션이란다(tsconfig/moduleResolution). Node 전략은 CommonJS mode에서 Node.js가 동작하는 방식을 그대로 따르는 것으로, .ts
와 .d.ts
파일을 통한 추가적인 체크를 하는 방식으로 이루어진단다.
내가 지금 진행하고 있는 electron
프로젝트의 tsconfig.json
를 예시로 한 번 살펴보자.
{
"compilerOptions": {
"incremental": true,
"target": "es2022",
"module": "commonjs",
"lib": ["dom", "es2022"],
"jsx": "react-jsx",
"strict": true,
"sourceMap": true,
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"allowJs": true,
"outDir": ".erb/dll"
},
"exclude": ["test", "release/build", "release/app/dist", ".erb/dll"]
}
moduleResolution이 "node"이다. 이 옵션에 대한 공식 문서의 설정을 살펴보면 아래와 같다.
'node16' or 'nodenext' for modern versions of Node.js. Node.js v12 and later supports both ECMAScript imports and CommonJS require, which resolve using different algorithms. These moduleResolution values, when combined with the corresponding module values, picks the right algorithm for each resolution based on whether Node.js will see an import or require in the output JavaScript code.
'node10' (previously called 'node') for Node.js versions older than v10, which only support CommonJS require. You probably won’t need to use node10 in modern code.
조금 더 자세한 설명을 찾아볼까?
node10: Formerly known as node, this is the unfortunate default when module is set to commonjs. It’s a pretty good model of Node.js versions older than v12, and sometimes it’s a passable approximation of how most bundlers do module resolution. It supports looking up packages from node_modules, loading directory index.js files, and omitting .js extensions in relative module specifiers. Because Node.js v12 introduced different module resolution rules for ES modules, though, it’s a very bad model of modern versions of Node.js. It should not be used for new projects.
아하, 지금 module옵션에 commonjs가 설정되어 있어서 moduleResolution이 node로 설정되어 있구나.
그런데 현재 나는 electron ^26.2.1 버전을 사용중이고, 이 경우에 Node.js 버전은 18이다. 그러니, module옵션과 moduleResolution 옵션을 수정해도 될 것 같다.
에러가 났던 이유는, 내가 사용하는 보일러플레이트 프로젝트에서 webpack ts-loader의 compilerOption이 "esnext"로 되어있어 Node16옵션과 맞지 않았기 때문이다.
그런데 ts-loader? 이 녀석은 webpack과 관련된 주제라 오늘은 여기까지...후우...