Node.js 모듈 시스템 완벽 정리: CJS vs. ESM 파헤쳐보기

node.js nest.js 의 환경에는 commonJs, ESM이라는 모듈 참조방식 2가지가 있습니다.

이는 Node.js 환경에서 개발을 하려면 알아야하는 방식인데 해당 내역들을 자세히 알아보겠습니다.

CommonJS

CommonJS는 JavaScript를 서버 사이드(Node.js)나 데스크톱 환경 등 브라우저 외의 환경에서 사용할 수 있도록 모듈 시스템을 표준화하려는 프로젝트 및 사양입니다. CJS의 핵심은 모듈을 동기적(Synchronous)으로 로드하여, 파일에서 필요한 모듈을 즉시 가져와 사용할 수 있도록 하는 것입니다.

등장 배경 및 시기

  • 등장 시기: 2009년경 (원래 이름은 ServerJS였음)
  • 목표: 당시 웹 브라우저 외의 환경(서버, 명령줄 도구)에서 JavaScript를 사용할 때 표준화된 모듈 방식이 없었기 때문에, 파일 시스템, I/O, 모듈 로딩 등을 위한 공통의 API와 사양을 만들기 위해 시작되었습니다.
  • 주요 채택: Node.js가 이 CommonJS 사양을 채택하고 구현함으로써, Node.js 생태계의 모듈 시스템으로 자리 잡았습니다. 따라서 Node.js에서 사용하는 require()와 module.exports가 바로 CJS의 구현체입니다.

장단점

  • 장점
    • Node.js 기본 모듈 시스템이기 때문에 호환성 좋음
    • 대부분의 Nest.js 예제, 라이브러리, 타사 패키지가 CommonJS 기반
    • 설정이 단순하고 기존 Node.js 프로젝트와 통합 쉬움
  • 단점
    • Tree-shaking 불가 → CJS의 require() 문은 코드 실행 시점에 동적으로 모듈을 로드합니다. 번들러가 코드 실행 전에 require가 무엇을 가져올지 정확히 예측할 수 없기 때문에, 코드를 안전하게 제거하기 어렵습니다.
    • ESM 전용 라이브러리 사용 시 import로 불러오기 어려움

트리 쉐이킹(Tree Shaking)은 JavaScript 생태계에서 사용되는 코드 최적화 기술 중 하나로, 실제로 사용되지 않는(Dead Code) 코드를 최종 빌드 결과물에서 제거하여 애플리케이션의 크기를 줄이는 기법


CommonJS 의 내보내기 및 가져오기

1. 내보내기 (Export)

CommonJS 모듈(CJS)은 본질적으로 Named ExportDefault Export를 구분하는 ESM의 개념이 없습니다. CJS는 단순히 하나의 객체를 module.exports로 내보냅니다.

문법 역할

module.exports = value; 모듈의 대표 값을 통째로 내보냅니다. (Default Export와 유사)
module.exports.name = value; 내보낼 객체에 속성을 추가하여 여러 값을 내보냅니다. (Named Export와 유사)

코드

// module.js (CJS)
const MyFunc = () => 'Hello CJS';
const MyData = 100;

module.exports = {
MyFunc,
MyData,
};

2. 가져오기 (Import)

require() 함수를 사용하여 module.exports 객체 전체를 동기적으로 가져옵니다.

문법 역할

const module = require('./file.js'); 내보낸 객체 전체를 가져와 변수에 할당합니다.
const { name } = require('./file.js'); 가져온 객체에서 원하는 속성만 구조 분해 할당(Destructuring)을 통해 가져옵니다.

코드

// app.js
const myModule = require('./file');
console.log(myModule.VERSION); // '1.0'

// 원하는 값만 가져올 경우
const { add } = require('./file');
console.log(add(1, 2)); // 3

ECMAScript Module (ESM)

ECMAScript Module은 JavaScript 언어의 공식 표준을 정의하는 ECMAScript(ES) 사양에 내장된 모듈 시스템입니다. CJS와 달리 비동기적(Asynchronous) 로드 방식을 지원하며, 정적 구조를 가지는 것이 특징입니다.

등장 배경 및 시기

  • 등장 시기: ECMAScript 2015 (ES6) 사양에 공식적으로 포함되어 표준화되었습니다.
  • 목표: JavaScript가 언어 자체적으로 표준화된 모듈 시스템을 가지지 못했던 문제와, 브라우저 환경에서 느린 네트워크 속도로 인해 모듈을 비동기적으로 효율적으로 로드해야 할 필요성 때문에 개발되었습니다.
  • 주요 채택:
    • 모든 현대 웹 브라우저에서 지원됩니다.
    • Node.js도 최신 버전에서 이 ESM을 공식 모듈 시스템으로 지원하고 있으며, CJS와 함께 사용할 수 있습니다.
반응형

장단점

  • 장점
    • 최신 JavaScript 표준 문법 사용 가능
    • Tree-shaking 지원 → ESM의 정적 특성: ESM의 import 및 export 문법은 코드 실행 전에 모듈 간의 의존성 관계를 미리 분석할 수 있습니다. 즉, 어떤 파일에서 무엇을 가져오는지 코드 실행 없이도 알 수 있습니다.
    • 향후 Node.js와 프론트엔드 통합, Vite/Next.js 같은 ESM 친화적 환경과 호환
  • 단점
    • CommonJS 기반 라이브러리 import가 까다로움
    • __dirname, __filename 등 Node 전역 변수 사용 시 추가 설정 필요
    • Nest.js 공식 문서 예제는 대부분 CommonJS 기준 → 초기 설정 복잡

ESM의 내보내기 및 가져오기

1. 내보내기 (Export)

ESM은 import와 export 키워드를 사용하며, Default ExportNamed Export를 명확히 구분합니다.

문법 역할

export default value; 모듈의 대표 값을 내보냅니다. (모듈당 하나만 가능)
export const name = value; 이름이 지정된 값을 내보냅니다. (여러 개 가능)

코드

// file.js
export const VERSION = '1.0'; // Named Export
export const subtract = (a, b) => a - b; // Named Export

export default function multiply(a, b) { // Default Export (하나만 가능)
    return a * b;

2. 가져오기 (Import)

import 문법을 사용하여 모듈을 가져옵니다.

문법 역할

import name from './file.js'; Default Export 값을 가져옵니다. (원하는 이름으로 지정 가능)
import { name } from './file.js'; Named Export 값을 가져옵니다. (이름을 정확히 일치시켜야 함)
import * as name from './file.js'; 모든 내보내기를 객체 하나로 묶어(Namespace) 가져옵니다.
(사용 지양), 반드시 필요할 때만 사용.  

코드

// app.js
import multiplyFunc from './file'; // Default Export 가져오기
import { VERSION, subtract } from './file'; // Named Export 가져오기

console.log(multiplyFunc(2, 3)); // 6
console.log(VERSION); // '1.0'

CJS와 ESM의 차이

구분 CommonJS (CJS) ECMAScript Module (ESM)

주요 문법 require() 및 module.exports import 및 export
로드 방식 동기적(Synchronous) 비동기적(Asynchronous)
적용 환경 Node.js의 전통적인 방식 브라우저(표준), Node.js (최신 방식)
실행 시점 런타임 (코드 실행 중 require() 호출 시) 컴파일/파싱 시점 (코드 실행 전 모듈 구조 확정)
특징 require()는 조건문 내에서 사용 가능 (동적 로드) import는 파일 최상단에서만 사용 가능 (정적 구조)
Default Export 원래 개념 없음. module.exports 객체가 대체 공식적으로 존재 (export default)

모듈 방식 코드 번들러의 인식 트리 쉐이킹 가능 여부

ESM import { a } from './lib'; 'a'만 사용됨을 정확히 파악 가능
CJS const lib = require('./lib'); 'lib' 전체가 사용됨을 추정 불가능 또는 제한적
 

 

반응형