CommonJS & ES Modules (ESM)

CH.dev·2025년 7월 31일
post-thumbnail

JavaScript 프로젝트의 규모가 커지면서 코드의 분리, 재사용, 유지보수는 개발의 핵심 과제가 되었음.
이 문제를 해결하기 위해 등장한 것이 바로 '모듈(Module)' 시스템.
모듈은 독립적인 파일 스코프를 가진 코드 조각으로, 전역 스코프 오염을 막고 코드의 의존성을 명확하게 관리할 수 있도록 도와줌.

현재 JavaScript 생태계를 이끄는 두 가지 대표적인 모듈 시스템은 CommonJS(CJS)ES Modules(ESM)임. 이 둘의 작동 방식과 철학은 명확히 다르며, 이를 이해하는 것은 현대 JavaScript 개발의 기본기라 할 수 있음.

💡 CommonJS (CJS): 서버를 위한 동기식 모듈

CommonJS는 Node.js의 기본 모듈 시스템으로, 서버 사이드 환경에 맞춰 설계되었음. 서버에서는 모든 파일이 로컬 디스크에 존재하므로, 파일을 동기적(Synchronous)으로 읽어와도 성능 저하가 크지 않다는 점에 착안함.

  • 핵심 문법: require로 모듈을 불러오고, module.exports 또는 exports로 내보냄.
  • 동기적 로딩: require()가 호출되면 해당 모듈 파일이 로딩되고 실행될 때까지 다음 코드의 실행이 멈춤.
  • 런타임 평가: 모듈의 의존성 분석과 로딩이 코드 실행 시점(런타임)에 이루어짐. 이 덕분에 아래와 같이 조건부로 모듈을 불러오는 동적 로딩이 가능함.
  • 값의 복사: require로 가져온 값이 객체나 배열이 아닌 원시 타입(primitive type)일 경우, 모듈의 값을 복사해서 가져옴. 내보낸 쪽의 값이 바뀌어도 가져온 값은 변하지 않음 (단, 객체는 참조를 공유).

CJS 코드 예시

// /cjs/math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

// module.exports에 객체를 할당하여 내보냄
module.exports = {
  add,
  subtract,
};

// /cjs/index.js
// 동적 로딩: 특정 조건에서만 'fs' 모듈을 불러옴(불러오기만 해봄)
let fs;
if (process.platform === 'linux') {
  fs = require('fs');
  console.log('리눅스 환경에서 fs 모듈을 로드함.');
}

// math 모듈 불러오기
const { add } = require('./math.js');
console.log(`10 + 20 = ${add(10, 20)}`);

💡 ES Modules (ESM): 브라우저를 품은 표준 비동기식 모듈

ESM은 ECMA-262 명세에 포함된 JavaScript의 공식 표준 모듈 시스템임. 네트워크를 통해 파일을 가져와야 하는 브라우저 환경을 고려하여 비동기적(Asynchronous)으로 동작하도록 설계됨.

  • 핵심 문법: import로 모듈을 불러오고, export로 내보냄.
  • 비동기적 로딩: 모듈 로딩이 코드 실행을 차단하지 않음. 브라우저가 HTML을 파싱하며 필요한 모듈을 병렬로 다운로드하고, 모든 모듈이 준비되면 스크립트를 실행함.
  • 정적 분석: import/export 구문은 반드시 코드 최상단에 위치해야 함. 이 정적인 구조 덕분에 코드를 실행하기 전, 빌드 시점에 모듈 의존성을 완벽하게 파악할 수 있음. 이는 트리 셰이킹(Tree Shaking) 같은 최적화를 가능하게 하는 핵심적인 특징임.
  • 라이브 바인딩(Live Bindings): ESM은 모듈에서 내보낸 값에 대한 실시간 참조를 유지함. 즉, 내보낸 모듈 내부에서 변수 값이 변경되면, import한 다른 모듈에서도 변경된 값을 즉시 확인할 수 있음.

ESM 코드 예시 1: 기본 문법

// /esm/math.js

// 이름 있는 내보내기 (Named Export)
export const add = (a, b) => a + b;

// 기본 내보내기 (Default Export): 파일 당 하나만 가능
const PI = 3.14159;
export default PI;

// /esm/index.js
import PI, { add } from './math.js'; // default와 named를 함께 import

console.log(`10 + 20 = ${add(10, 20)}`);
console.log(`PI는 약 ${PI}입니다.`);

ESM 코드 예시 2: 라이브 바인딩 (Live Bindings) 증명

// /esm/counter.js
export let count = 0;

export function increment() {
  count++;
}

// /esm/main.js
import { count, increment } from './counter.js';

console.log(`초기 값: ${count}`); // 출력: 초기 값: 0

increment();
console.log(`증가 후 값: ${count}`); // 출력: 증가 후 값: 1
// counter.js의 count 변수가 직접 변경된 것처럼 동작함. 이것이 라이브 바인딩임.

CommonJS였다면 count는 0인 채로 복사되어 와서 increment 함수를 호출해도 main.jscount 값은 변하지 않았을 것임.

⚖️ CommonJS vs ES Modules: 한눈에 비교하기

특징CommonJS (CJS)ES Modules (ESM)
주요 환경Node.js (서버)브라우저, 최신 Node.js
로딩 방식동기적 (Synchronous)비동기적 (Asynchronous)
문법require() / module.exportsimport / export
구조 분석런타임 (동적)빌드 타임 (정적)
바인딩값 복사 (Primitives)라이브 바인딩 (실시간 참조)
최적화제한적트리 셰이킹에 매우 유리

🧩 번들러의 역할: 두 세계의 통합

"그렇다면 브라우저에서 CommonJS 코드를, 혹은 Node.js에서 ESM 코드를 어떻게 사용할까?"

이 질문에 대한 답이 바로 웹팩(Webpack), 롤업(Rollup), Vite와 같은 모듈 번들러(Module Bundler)임. 번들러는 여러 개로 나뉜 모듈 파일들을 분석하여 하나의 (또는 여러 개의) 파일로 합쳐주는 도구임.

  • 의존성 해결: importrequire를 따라가며 모듈 간의 의존성 그래프를 생성함.
  • 변환(Transpiling): 최신 ESM 문법이나 CommonJS 문법을 구형 브라우저도 이해할 수 있는 코드(주로 즉시 실행 함수 표현식, IIFE)로 변환함.
  • 최적화: 위에서 언급한 트리 셰이킹으로 사용하지 않는 코드를 제거하고, 코드를 압축(Minify)하여 최종 파일 크기를 줄임.

현대 프론트엔드 개발에서는 ESM 문법으로 코드를 작성하고, 개발 서버(Vite)나 빌드 과정(Webpack)에서 번들러가 이를 처리하는 방식이 표준으로 자리 잡았음.

🔍 더 깊이 찾아보기

  1. 트리 셰이킹 (Tree Shaking): ESM의 정적 구조 덕분에 "죽은 코드(Dead Code)", 즉 사용되지 않는 export를 빌드 시점에 파악하고 최종 번들에서 제거하는 기술. 애플리케이션의 로딩 속도를 높이는 데 결정적인 역할을 함.

  2. 동적 import(): ESM에서도 필요할 때 모듈을 불러오는 동적 로딩이 가능함. import() 함수는 Promise를 반환하며, 코드 스플리팅(Code Splitting)과 결합하여 초기 로딩 성능을 최적화하는 데 사용됨.

    const loginButton = document.getElementById('login-btn');
    loginButton.addEventListener('click', () => {
      import('./auth/login.js')
        .then(module => {
          module.showLoginModal();
        })
        .catch(err => console.error('모듈 로딩 실패:', err));
    });
  3. Node.js의 ESM 지원: 최신 버전의 Node.js는 ESM을 공식적으로 지원함. package.json"type": "module"을 추가하거나, 파일 확장자를 .mjs로 사용하면 됨. 이로 인해 Node.js 생태계도 점차 ESM 중심으로 이동하고 있음.

profile
더 이상 미룰 수 없다 나의 공부 나의 성장

0개의 댓글