module.exports는 Node.js 파일이 require()로 자신을 불러온 다른 파일에 넘겨주는 값입니다. 처음에는 빈 객체이고, 여러 개를 내보내려면 속성을 추가하고, 하나(함수나 클래스)를 내보내려면 새 값을 대입합니다. exports는 같은 객체를 가리키는 줄임 변수라서 exports.add = ...는 동작하지만, exports = ...는 연결을 끊어 아무것도 내보내지 않습니다.
5 20 3.14159, 이어서 [ 'add', 'multiply', 'PI' ]가 출력되었습니다. math.js의 세 줄은 module.exports를 거치든 exports를 거치든 같은 객체에 속성을 추가했고, require('./math')는 그 객체를 반환했습니다.
함수, 클래스, 객체 내보내기
모듈에 핵심이 하나뿐이라면 그것을 module.exports에 바로 대입합니다. 그러면 호출하는 쪽은 객체에 감싼 것이 아니라 함수 자체를 받습니다.
Hello, Ada와 Grace (user)가 출력되었습니다. TypeError: greet is not a function이 보이면 호출하는 쪽이 모양을 잘못 받은 것입니다. 객체 속성이면 const { greet } = require(...), 대입된 값이면 const greet = require(...)로 씁니다.
module.exports vs exports: 재대입 함정
파일이 실행되기 전에 Node.js는 exports = module.exports를 설정합니다. 두 이름 모두 한 객체를 가리키고, require가 반환하는 것은 module.exports입니다. exports에 새 값을 대입하면 지역 변수만 바뀝니다.
broken.js는 {}를 내보냈습니다. exports가 아무도 반환하지 않는 새 객체를 가리키게 되었기 때문입니다. working.js는 { version: 2 }를 내보냈습니다.
| 이렇게 쓰면 | 내보내지나요? |
|---|---|
module.exports.x = 1 | 예 |
exports.x = 1 | 예, 같은 객체 |
module.exports = { x: 1 } | 예, 객체를 교체 |
exports = { x: 1 } | 아니요, 값이 사라짐 |
가장 간단한 규칙은 항상 module.exports라고 쓰는 것입니다. 모든 경우에 동작하고, 코드가 하는 일이 그대로 읽힙니다.
require는 캐시된 같은 객체를 반환합니다
Node.js는 모듈 코드를 처음 require될 때 한 번만 실행하고 module.exports를 캐시합니다. 같은 파일을 나중에 require하면 매번 그 객체가 돌아오므로, 모듈 안의 상태는 그 모듈을 불러오는 모든 파일이 공유합니다.
counter.js is running은 한 번만 나왔고, b.next()는 3을 반환했으며, a === b는 true입니다. 캐시 키가 해석된 파일 경로이고, ./counter와 ./counter.js는 같은 파일로 해석되기 때문입니다. 한 모듈에서 만든 데이터베이스 연결이나 설정 객체가 앱 전체에서 공유되는 이유가 이것입니다.
module.exports와 ES 모듈
ES 모듈은 대신 export와 import를 씁니다. 각 형태는 이렇게 대응합니다.
// CommonJS // ES modules
module.exports = greet; export default greet;
module.exports.add = add; export { add };
const greet = require('./greet'); import greet from './greet.js';
const { add } = require('./math'); import { add } from './math.js';
ES 모듈은 CommonJS 파일을 import할 수 있고, 그 파일의 module.exports는 기본 import로 들어옵니다. Node.js 22.12부터는 최상위 await가 없는 ES 모듈이라면 require()로도 불러올 수 있습니다. 어느 쪽을 고를지는 CommonJS vs ES 모듈을 참고하세요.
흔한 실수
한 파일에서 exports.x와 module.exports = ...를 섞는 것. 대입이 객체를 교체하므로 그 앞에 쓴 exports.x는 모두 사라집니다. 파일마다 한 가지 방식만 쓰세요.
나중에 바뀌는 값을 내보내는 것. module.exports = { count }는 불러올 때의 숫자를 복사합니다. 모듈이 count를 바꿔도 호출하는 쪽에는 여전히 옛 값이 보입니다. 위의 counter.js처럼 값을 반환하는 함수를 내보내세요.
./를 빼먹는 것. require('math')는 내 파일 옆의 math.js가 아니라 node_modules의 패키지를 찾습니다. 로컬 파일은 항상 ./나 ../로 시작합니다.
자주 묻는 질문
한 파일에서 module.exports와 export를 같이 쓸 수 있나요?
없습니다. 파일은 CommonJS(require, module.exports)이거나 ES 모듈(import, export) 중 하나입니다. Node.js는 확장자(.cjs나 .mjs) 또는 package.json의 "type"으로 판단합니다.
require가 빈 객체를 반환하는 이유는 뭔가요?
보통은 모듈이 module.exports = ... 대신 exports = ...로 대입했거나, 두 모듈이 서로를 require하고 있는 경우입니다. 순환 require에서는 한 파일이 실행을 마치기 전에 다른 쪽이 그 파일의 module.exports를 받아 갑니다.
require에 .js 확장자를 써야 하나요?
아니요. require('./math')는 ./math, 다음으로 ./math.js, ./math.json, ./math.node, 마지막으로 index.js가 있는 ./math 폴더 순서로 찾습니다. ES 모듈의 import에서는 확장자가 필요합니다.
require가 이미 캐시한 모듈을 다시 불러오려면?
delete require.cache[require.resolve('./math')]로 항목을 지운 뒤 다시 require합니다. 주로 테스트와 개발 도구에서 쓸모가 있고, 애플리케이션 코드가 여기에 기대서는 안 됩니다.