CommonJs

Node.js에서 시작된 JavaScript 모듈 시스템으로, require()로 모듈을 불러오고 module.exports로 내보내는 방식입니다.

왜 필요했는가

[ 초기 JavaScript — 모듈 시스템 없음 ]

<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>

a.js: var name = "Kim";
b.js: var name = "Lee";     // a.js의 name을 덮어씀
c.js: console.log(name);    // "Lee" — 의도한 건 "Kim"이었는데?

→ 모든 변수가 전역 스코프에 노출
→ 파일 간 이름 충돌
→ 의존 관계 불명확
→ 대규모 프로젝트 불가능
[ CommonJS 등장 (2009) ]

// a.js
const name = "Kim";
module.exports = name;       // 명시적으로 내보내기

// b.js
const name = require("./a"); // 명시적으로 가져오기
console.log(name);           // "Kim"

→ 파일마다 독립된 스코프
→ 의존 관계 명확
→ Node.js의 기본 모듈 시스템으로 채택

기본 문법

내보내기 (exports)

// ── 방법 1: module.exports 직접 할당 ──

// 단일 값 내보내기
module.exports = function add(a, b) {
    return a + b;
};

// 객체로 여러 값 내보내기
module.exports = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
};


// ── 방법 2: exports에 속성 추가 ──

exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;

가져오기 (require)

// 단일 값
const add = require("./math");
add(1, 2);  // 3

// 객체에서 구조 분해
const { add, subtract } = require("./math");
add(1, 2);       // 3
subtract(5, 3);  // 2

// Node.js 내장 모듈
const fs = require("fs");
const path = require("path");

// npm 패키지
const express = require("express");

module.exports vs exports 차이

// exports는 module.exports를 가리키는 참조(별칭)

// 내부적으로 이렇게 초기화됨
var module = { exports: {} };
var exports = module.exports;   // 같은 객체를 참조
// O 정상 — 속성 추가는 같은 객체에 반영
exports.name = "Kim";
exports.age = 25;
// module.exports === { name: "Kim", age: 25 }

// X 문제 — exports에 새 객체를 할당하면 참조가 끊김
exports = { name: "Kim" };
// module.exports는 여전히 {} (빈 객체)
// require 결과: {} ← 의도와 다름

// O 새 객체를 할당하려면 module.exports 사용
module.exports = { name: "Kim" };
메모리 구조:

[ 초기 상태 ]
module.exports ──→ { }
exports ──────────→ { }  (같은 객체)

[ exports = { name: "Kim" } 이후 ]
module.exports ──→ { }           ← require가 반환하는 것
exports ──────────→ { name: "Kim" }  ← 참조가 끊김, 무의미

require()의 동작 원리

require("./math")
     │
     ▼
① 경로 해석 (Resolution)
   "./math" → /project/math.js
     │
     ▼
② 캐시 확인
   이미 로드한 적 있으면 캐시에서 반환
     │
     ├── 캐시 있음 → 캐시된 module.exports 반환 (끝)
     │
     └── 캐시 없음 → 계속
     │
     ▼
③ 파일 읽기
   math.js 파일 내용을 읽음
     │
     ▼
④ 래핑 (Wrapping)
   파일 내용을 함수로 감싸서 독립 스코프 생성
     │
     ▼
⑤ 실행
   래핑된 함수를 실행하여 module.exports 생성
     │
     ▼
⑥ 캐싱
   결과를 캐시에 저장
     │
     ▼
⑦ 반환
   module.exports를 호출자에게 반환

④ 래핑 상세

// 개발자가 작성한 코드
const name = "Kim";
module.exports = name;

// Node.js가 내부적으로 래핑한 코드
(function(exports, require, module, __filename, __dirname) {
    const name = "Kim";
    module.exports = name;
});

// → 이 덕분에:
// - const name이 전역을 오염시키지 않음 (함수 스코프)
// - require, module, exports를 사용할 수 있음
// - __filename, __dirname에 접근 가능

② 캐싱 — 한 번만 실행

// counter.js
let count = 0;
count++;
console.log("counter 로드됨");
module.exports = { getCount: () => count };

// main.js
const a = require("./counter");  // "counter 로드됨" 출력
const b = require("./counter");  // 출력 없음 (캐시에서 반환)
const c = require("./counter");  // 출력 없음

console.log(a === b);  // true — 같은 객체
console.log(b === c);  // true
첫 번째 require:  파일 읽기 → 실행 → 캐시 저장 → 반환
두 번째 require:  캐시 확인 → 즉시 반환 (실행 안 함)

→ 모듈은 싱글톤처럼 동작

모듈 경로 해석 규칙

require("./math")       // 상대 경로 → 현재 디렉토리의 math.js
require("../utils")     // 상대 경로 → 상위 디렉토리의 utils.js
require("express")      // 패키지명 → node_modules에서 탐색
require("fs")           // 내장 모듈 → Node.js 코어 모듈

파일 확장자 탐색 순서

require("./math")

1. ./math.js          있으면 반환
2. ./math.json         있으면 반환
3. ./math.node         있으면 반환 (C++ 애드온)
4. ./math/index.js     디렉토리인 경우
5. ./math/package.json의 main 필드

node_modules 탐색 순서

require("express")

/project/src/node_modules/express      ← 현재 디렉토리부터
/project/node_modules/express          ← 상위로 올라가며 탐색
/node_modules/express                  ← 루트까지

CommonJS의 핵심 특징

1. 동기적 로딩

// require는 파일을 동기적으로 읽고 실행
const a = require("./a");  // a.js 로드 완료될 때까지 블로킹
const b = require("./b");  // a 완료 후 b 로드
console.log("시작");       // a, b 모두 로드된 후 실행
실행 순서:
a.js 로드 ████████
b.js 로드          ████████
"시작"                      ██

→ 서버(Node.js)에서는 시작 시 1회만 로드하므로 문제 없음
→ 브라우저에서는 네트워크 I/O를 블로킹하므로 부적합

2. 값의 복사본 반환

// counter.js
let count = 0;
module.exports = { count };

// main.js
const counter = require("./counter");
console.log(counter.count);  // 0

// counter.js에서 count를 변경해도
// main.js의 counter.count는 변하지 않음 (복사본)

3. 런타임 로딩

// 조건부 로딩 가능 — 런타임에 결정
if (process.env.NODE_ENV === "production") {
    const logger = require("./prod-logger");
} else {
    const logger = require("./dev-logger");
}

// 동적 경로도 가능
const moduleName = "math";
const mod = require(`./${moduleName}`);

CommonJS vs ES Modules (ESM)

항목CommonJSES Modules
문법require / module.exportsimport / export
로딩 방식동기 (블로킹)비동기
실행 시점런타임파싱 타임 (정적 분석 가능)
값 전달복사본참조 (live binding)
조건부 로딩가능기본 불가 (dynamic import로 가능)
Tree Shaking불가가능
환경Node.js브라우저 + Node.js
파일 확장자.js (기본).mjs 또는 package.json type:"module"

문법 비교

// ── CommonJS ──
const { readFile } = require("fs");
const add = (a, b) => a + b;
module.exports = { add };

// ── ES Modules ──
import { readFile } from "fs";
export const add = (a, b) => a + b;

Tree Shaking 차이

// CommonJS — 번들러가 어떤 것을 쓰는지 정적 분석 불가
const utils = require("./utils");   // 전체를 가져옴
utils.add(1, 2);                    // add만 쓰지만 전부 포함

// ES Modules — 정적 분석으로 사용하지 않는 코드 제거 가능
import { add } from "./utils";      // add만 가져옴
add(1, 2);                          // subtract 등은 번들에서 제거
번들 크기:
CommonJS:  utils.js 전체 포함 (100KB)
ESM:       add 함수만 포함 (2KB)  ← Tree Shaking

현재 상태와 흐름

2009        2015         2020~         현재
─────      ──────       ──────        ──────
CommonJS    ES Modules   Node.js ESM   ESM이 표준
등장        표준 제정     정식 지원      CommonJS는 레거시

브라우저:   X            O             O
Node.js:   O (기본)     O (지원)       O (권장)
번들러:     O            O (선호)       O (기본)
상황권장
신규 프로젝트ESM
기존 Node.js 프로젝트CommonJS 유지 또는 점진적 ESM 전환
브라우저ESM (또는 번들러 사용)
npm 라이브러리 배포둘 다 지원 (dual package)

핵심 정리

CommonJS는 Node.js의 기본 모듈 시스템으로, require()로 동기적으로 모듈을 로드하고 module.exports로 내보냅니다. 파일마다 독립 스코프를 제공하고, 한 번 로드된 모듈은 캐싱되어 싱글톤처럼 동작합니다. 그러나 동기 로딩, Tree Shaking 불가, 브라우저 미지원이라는 한계로 인해 현재는 ES Modules(import/export)이 표준으로 자리 잡았고, CommonJS는 기존 Node.js 생태계에서 레거시로 유지되고 있습니다.

CSLOG에서 보기