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**로 안전하게 연결하세요.
'개발라이프 > 자바스크립트' 카테고리의 다른 글
| 자바스크립트 fallback 패턴 (1) | 2025.07.03 |
|---|---|
| JavaScript Math.ceil() 함수 사용법과 예시 (0) | 2025.07.03 |
| dayjs에서 .tz()와 dayjs.tz()의 차이, 쉽게 이해해보기 (1) | 2025.05.27 |
| TypeScript 전역 설치 vs 로컬 설치 – 무엇을 선택해야 할까? (0) | 2025.04.02 |
| Node.js에서 ?? (null 병합 연산자) 완전 정리 (0) | 2025.04.01 |