반응형

require를 쓸까, import를 쓸까?
Node.js는 CommonJS(CJS)ECMAScript Modules(ESM) 두 방식을 모두 지원합니다. 이 글은 두 방식의 차이, 선택 기준, 상호 운용(interop), 마이그레이션 팁을 실무 중심으로 정리했습니다.


1) 빠른 요약: 언제 CJS, 언제 ESM?

  • CJS(CommonJS) — require, module.exports
    • 장점: 레거시/도구 호환 최고, 시작이 빠름
    • 언제: 기존 라이브러리/도구(CJS 중심)와의 호환이 최우선일 때
  • ESM(ECMAScript Modules) — import, export
    • 장점: 브라우저·번들러·TypeScript 생태계와 일관, 정적 분석/트리 셰이킹, Top-Level await
    • 언제: 신규 프로젝트 기본값으로 추천

실무 제안: 새 프로젝트는 ESM 통일 → 기존 CJS 패키지는 동적 import() 또는 createRequire로 연결.


2) 문법 & 실행 규칙 한눈에

A. CommonJS(CJS)

  • 파일 조건
    • 확장자 .cjs, 또는
    • package.json에 "type": "commonjs"(혹은 type 미지정)인 .js
  • 문법 예시
// module.cjs (또는 type: commonjs 환경의 module.js)
exports.abs = (n) => (n < 0 ? -n : n);
module.exports.util = () => 'ok';

// main.cjs
const mod = require('./module');    // .js 확장자 생략 가능
console.log(mod.abs(-3));
  • require 해석 규칙(요약)
    • require('./x') → ./x → ./x.js → ./x.json → ./x.node
    • 대상이 디렉터리면 package.json의 main/exports 우선 → 없으면 index.js(→.json→.node)
    • 확장자/디렉터리 자동 보정 O

B. ES Modules(ESM)

  • 파일 조건
    • 확장자 .mjs, 또는
    • package.json에 "type": "module"인 .js
  • 문법 예시
// module.mjs (또는 type: module 환경의 module.js)
export const abs = (n) => (n < 0 ? -n : n);

// main.mjs
import { abs } from './module.js';  // ESM은 확장자 명시가 원칙
console.log(abs(-3));
  • import 해석 규칙(요약)
    • 확장자/디렉터리 자동 보정 X → 경로에 확장자 필수
    • 패키지 import 시 package.json의 exports(서브패스 포함) 규칙을 엄격히 따름

3) package.json으로 프로젝트 기준 정하기

ESM 프로젝트(권장)

{
  "type": "module"
}

CJS 프로젝트(기본값과 동일)

{
  "type": "commonjs"
}

혼용 전략

  • 한 저장소에서 CJS와 ESM을 섞는 경우:
    • CJS 파일은 .cjs, ESM 파일은 .mjs 로 확장자 분리
    • 패키지 배포 시 exports에 조건부 내보내기(CJS/ESM 동시 제공) 구성

4) 상호 운용(Interop) — CJS ↔ ESM 함께 쓰기

CJS → ESM 불러오기 (동적 import)

// main.cjs
(async () => {
  const { abs } = await import('./module.mjs');
  console.log(abs(-3));
})();

 

ESM → CJS 불러오기 (default 형태로 옴)

// main.mjs
import legacy from './module.cjs';
const { abs } = legacy;

 

ESM에서 require 사용(특수 케이스)

// main.mjs
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const cjs = require('./legacy.cjs');
 

5) 자주 겪는 함정과 해결

exports vs module.exports (CJS)

  • exports.foo = ... (같은 객체를 가리킴)
  • exports = fn  (참조 교체 → 내보내기 끊김) → 객체 전체 교체는 module.exports = ...로

변수 이름 충돌

  • CJS의 내장 변수 module을 가리는 코드 X
// 나쁨
const module = require('./module');
// 권장
const myModule = require('./module');

경로/파일 상수 차이

  • CJS: __filename, __dirname 바로 사용 가능
  • ESM: import.meta.url 기반으로 계산
 
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname  = dirname(__filename);

 

 

확장자/디렉터리 보정

  • CJS(require): 자동 보정 O
  • ESM(import): 확장자 명시 필수, 디렉터리 import 금지

Top-Level await

  • ESM만 지원
// ESM에서만 가능
const data = await fetch(url).then(r => r.json());

 

6) 선택 가이드 & 마이그레이션 체크리스트

신규 프로젝트

  • ESM 통일("type": "module")
  • 브라우저/번들러/TS와 규칙 일치, 정적 분석/트리 셰이킹, Top-Level await

기존 CJS 유지

  • 도구/레거시 호환 최우선
  • ESM 패키지 연동: 동적 import(), createRequire 브리지 고려

점진 전환 체크리스트

  • package.json에 "type": "module" 추가, CJS 파일은 .cjs로 분리
  • 확장자 명시(ESM 규칙) 및 디렉터리 import 제거
  • __dirname/__filename → import.meta.url 기반 대체
  • 외부 패키지의 exports/서브패스 호환 확인
  • 빌드/테스트/번들러 설정(ESM 대응) 점검

7) 유틸 스니펫

CJS: 실제 로드 경로 확인

console.log(require.resolve('./module'));

ESM: JSON 가져오기(Node 20+)

import data from './data.json' with { type: 'json' };
 

ESM: CommonJS 패키지 가져오기 패턴

import cjsDefault from 'legacy-pkg';
import * as cjsAll from 'legacy-pkg';

8) 결론

  • Node.js는 CJS와 ESM을 모두 지원합니다.
  • 새 프로젝트는 ESM로 통일, 레거시/도구 호환이 중요하면 CJS 유지도 합리적입니다.
  • 혼용 시 .cjs/.mjs 분리, 동적 import(), **createRequire**로 안전하게 연결하세요.
 
728x90
반응형

+ Recent posts