CJS의 ESM 적용과 동작원리에 기반한 트리쉐이킹 효율성 이해하기

pengooseDev·2023년 9월 1일
5
post-thumbnail

ESM을 사용하기 위해, package.json의 module을 사용하려다 NodeJS의 버젼이 낮은 경우도 호환하고싶어서 babel의존성 모듈을 추가하게 되었다.

더욱 많은 방법들이 있다는 피드백을 확인했다.
마침 궁금했던 내용들이라 기회가 왔을때 학습하도록 한다.


ESM? CJS?

우선 ESM과 CJS를 이해해보도록 하자.

CJS(CommonJS ES)

CommonJS ES (require)

// add.js
module.exports.add = (x, y) => x + y;

// main.js
const { add } = require('./add');

add(1, 2);

ESM(ECMA Script Module) - v13.2.0 이상

ES 모듈 (import / export)

// add.js
export function add(x, y) {
  return x + y
}

// main.js
import { add } from './add.js';

add(1, 2);

동작원리와 특성

아래에 많다면 많은 내용들이 나오게되지만, 여기서 서술할 본질을 관통하는 하나만 내용만을 인지하면 CJS와 ESM에 대한 모든 내용들이 자연스럽게 납득이 될 것이다. 이해하기 쉽도록 작성하였으니 아래의 설명을 먼저 이해하도록 하자.

CJS와 ESM의 차이점은 대부분 모듈을 분석하는 시점이 다른 것에 기인한다.

우리 살아가며 미래의 불상사를 예측할 수 없어, 여러가지 보험을 든다.
만약, 인류가 인생을 살기 전, 인생을 미리 살펴볼 수 있는 기회가 생긴다면, "보험"이라는 시장이 가장 먼저 사라지지 않을까 조심스레 예측해본다.

CJS와 ESM의 모듈의 분석 시점은 이것과 같다.


분석시점

CJS는 실행이 되고나서야 모듈의 내용을 확인할 수 있고,
ESM은 실행이 되기 전인, 컴파일 단계에서 모듈을 내용을 미리 확인할 수 있다.

"컴파일 단계? 실행 단계? 그게 뭔가요?"

모듈이 분석되고 시행되는 생명주기는 아래와 같다.

  1. 파싱 => 2. 분석 => 3. 번들링 => 4. 컴파일 => 5. 실행(런타임)

현재 내용을 이해하려면 분석과 실행 단계만 확인하면 된다.
이제 아래의 예시만 읽으면 바로 이해 될 것이다.

평범한 인류. CJS

CJS는 평범한 인간이다.
모듈의 내용을 실행(런타임)이 되고나서야 확인(분석)할 수 있다. 즉, 우리의 삶처럼 코드가 동작되고 나서야 모듈의 내용을 알 수 있다는 것이다.

삶을 미리 살펴보는 인류. EMS

EMS는 모듈을 컴파일 시점에 분석한다. 즉, 인생을 살기 전(런타임)에 본인의 미래를 한 눈에 살펴보고 분석한다는 뜻이다. 내 삶에서 필요한 것과 필요하지 않은 것들을 미리 확인하고, 필요없는 것들을 사전에 배제할 수 있어 효율적이다.

이러한 ESM과 CJS의 특성(분석시점)은 어떤 차이점을 발생시킬까?


공통점과 차이점

항목ESM (ECMAScript Modules)CJS (CommonJS)
정의ECMAScript (ES6 이상)에서 정의된 모듈 시스템Node.js에서 사용하기 위해 만들어진 모듈 시스템
로딩 방식정적(Static)동적 (Dynamic)
분석 시점컴파일 단계 (코드가 번들링 되는 시점)런타임 (코드가 실행되는 시점)
문법import / exportrequire() / module.exports
부분 로딩쉬움 (특정 모듈에서 파일단위로 default export된 일부 모듈만 가져오거나 하나의 모듈 내에서도 named export로 모듈을 부분로딩 할 수 있음)어려움 (모듈 전체를 가져와야 함)
트리 쉐이킹효율적 (사용하지 않는 코드 제거 가능)비효율적 (동적 특성 때문에 어떤것을 사용하지 않는지 파악하기 어려워 제거 불가능)

이처럼 ESM은 각 모듈에서 사용하는 것과 사용하지 않는 것. 즉, 모듈간의 의존성을 미리 파악할 있는 것을 의미한다. 이는 시사하는 바가 크다.

예를들어, ESM은 번들링 단계에서 미리 모듈의 의존성을 파악할 수 있기 때문에, 불필요한 모듈들이 무엇인지 명확히 확인할 수 있다. 따라서 번들링 단계에서 의존성이 없는 모듈들을 효과적으로 제거하여 번들링 크기를 크게 줄일 수 있다. (번들링 크기가 작으면 페이지의 초기 렌더링 속도가 단축된다)


로딩 방식과 분석 시점

여태 내용을 로딩 방식을 연관지어 간단히 정리해보자.

ESM (ECMAScript Modules)

  • 로딩 방식: ESM은 정적 로딩 방식을 채택한다. 이는 importexport 구문이 소스 코드의 상단에 미리 정의되어 있기 때문이다.

  • 분석 시점: 코드가 번들링되는 시점에 ESM의 importexport 구문을 분석한다. 번들러는 이 정보를 사용하여 모듈 간의 의존성을 파악하고 번들링 단계에서 트리쉐이킹에 활용한다.

CJS (CommonJS)

  • 로딩 방식: CJS는 동적 로딩 방식을 채택한다. require() 함수를 통해 필요한 시점에 모듈을 로드해야 하기 때문이다.

  • 분석 시점: 실행단계(런타임)에 CJS의 require()는 모듈을 로드하기 때문에, 의존성을 미리 파악하기 어렵기 때문에 트리 쉐이킹이 비효율적이다.


ESM 적용하기

1. 파일 확장자 변경(v8.5.0)

파일의 확장자가 .mjs인 경우, 이 확장자를 사용하는 파일은 자동으로 ESM 모듈로 취급된다.

ESM 지원 자체는 v8.5.0부터 지원하지만, 안정성을 위해 v12 LTS이상이 권장된다고 한다.

// myModule.mjs
import fs from 'fs';

2. Dynamic import(v9.7.0)

CommonJS 모듈 내에서도 동적 import() 문법을 사용하여 ESM 모듈을 가져올 수 있다. 해당 로직은 비동기적으로 동작하며 Promise를 반환한다.
(Dynamic import를 잘 사용하면 ReactJS에 named export한 컴포넌트를 동적으로 default export하여 CodeSplitting을 적용할 수 있다.)

// commonJSFile.js
import('./gooseModule.mjs').then(module => {
  // code...
});

3. package.json 설정 변경(v12.0.0)

package.json 파일에 type 필드를 module로 설정한다. 이렇게 설정하면 프로젝트의 .js 파일들이 ESM 모듈로 취급된다.

{
  "type": "module"
}

4. --input-type (v12.12.0)

Node.js를 실행할 때, --input-type=module 플래그를 사용하면 코드 스니펫을 ESM으로 간주하게 할 수 있다.

node --input-type=module -e "import fs from 'fs'; console.log(fs);"

0개의 댓글