프로그램이 커지면 코드를 여러 파일로 나누고, 남이 만든 라이브러리를 가져다 써야 합니다. JavaScript에는 두 가지 모듈 방식이 있습니다. 표준인 ES 모듈(ESM)과 Node.js가 오래 써 온 CommonJS입니다. 이 장에서는 두 방식의 차이, npm과 package.json, 스크립트, 그리고 번들링의 기초를 다룹니다.
ES 모듈
ES 모듈은 언어 표준에 정의된 모듈 방식으로, 브라우저와 Node.js 모두에서 동작합니다. export로 내보내고 import로 가져옵니다. 이름 붙은 내보내기(named export)는 여러 개 둘 수 있고, 기본 내보내기(default export)는 파일당 하나입니다.
// math.js
export const PI = 3.14159;
export function circleArea(r) {
return PI * r * r;
}
export default function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
// main.js
import sum, { circleArea, PI as pi } from "./math.js";
import * as math from "./math.js";
console.log(sum(1, 2, 3)); // 6
console.log(circleArea(2)); // 12.56636
console.log(pi, math.PI); // 3.14159 3.14159
// 필요할 때 불러오는 동적 import(Promise를 돌려줍니다)
const { readFile } = await import("node:fs/promises");
ES 모듈의 특징은 다음과 같습니다.
- 항상 엄격 모드(strict mode)로 실행됩니다.
import 선언은 파일 맨 위에서 정적으로 분석되므로, 도구가 쓰지 않는 코드를 걸러낼 수 있습니다(tree shaking).- 모듈 최상위에서
await를 쓸 수 있습니다. - 브라우저에서는
<script type="module" src="main.js">로 불러오고, 상대 경로에 확장자(.js)까지 적어야 합니다.
CommonJS
CommonJS는 ES 모듈이 표준이 되기 전부터 Node.js가 써 온 방식입니다. require로 가져오고 module.exports로 내보냅니다. 지금도 많은 패키지와 오래된 프로젝트에서 볼 수 있습니다.
// greet.cjs
function greet(name) {
return `안녕하세요, ${name}님`;
}
module.exports = { greet };
// app.cjs
const { greet } = require("./greet.cjs");
const path = require("node:path");
console.log(greet("유나"));
console.log(path.join("logs", "today.txt"));
Node.js는 파일이 어느 방식인지 다음 규칙으로 정합니다.
새 프로젝트라면 package.json에 "type": "module"을 넣고 ES 모듈로 시작하는 것을 권합니다. ES 모듈에서는 CommonJS 패키지를 import로 가져올 수 있습니다. 반대로 CommonJS에서 ES 모듈을 쓸 때는 동적 import()를 쓰는 것이 가장 확실합니다. Node.js 내장 모듈은 node:fs, node:path처럼 node: 접두사를 붙이면 npm 패키지와 헷갈리지 않습니다.
npm과 package.json
npm은 Node.js와 함께 설치되는 패키지 관리자입니다. 패키지를 설치하면 node_modules에 내려받고, package.json에 이름과 허용 버전 범위를 기록합니다.
npm install zod # 실행에 필요한 패키지(dependencies)
npm install -D vitest # 개발할 때만 필요한 패키지(devDependencies)
npm uninstall zod # 제거
npm outdated # 새 버전이 있는 패키지 확인
npm ci # lock 파일 그대로 깨끗하게 설치(CI 서버용)
{
"name": "todo-api",
"version": "0.1.0",
"type": "module",
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js",
"test": "node --test"
},
"dependencies": {
"zod": "^3.23.0"
},
"devDependencies": {
"vitest": "^2.0.0"
}
}
버전 앞의 ^는 "주 버전이 같은 범위에서 더 새로운 버전 허용"을, ~는 "부 버전까지 같고 패치 버전만 허용"을 뜻합니다. 실제로 설치된 정확한 버전은 package-lock.json에 기록되므로, 이 파일을 커밋하면 팀원과 서버가 모두 같은 버전을 쓰게 됩니다.
스크립트 실행하기
scripts에 적은 명령은 npm run 이름으로 실행합니다. start와 test는 run 없이 npm start, npm test로도 실행할 수 있습니다. 스크립트 안에서는 node_modules/.bin에 설치된 도구를 경로 없이 부를 수 있습니다.
npm run dev
npm test
npm start
# 설치하지 않은 도구를 한 번만 실행
npx prettier --write src
번들링 기초
브라우저용 앱은 파일이 수십, 수백 개로 나뉘고 npm 패키지도 많이 씁니다. 번들러는 이 파일들을 따라가며 하나 또는 몇 개의 파일로 묶고, 공백 제거(minify), 쓰지 않는 코드 제거(tree shaking), 코드 분할 같은 최적화를 함께 처리합니다.
# Vite로 새 프로젝트 만들기
npm create vite@latest my-web
# esbuild로 한 파일로 묶고 압축하기
npx esbuild src/main.js --bundle --minify --outfile=dist/app.js
Node.js에서만 실행하는 서버 코드는 보통 번들링 없이 그대로 실행해도 충분합니다. 번들러는 주로 브라우저에 보낼 코드를 다룰 때 필요합니다.
정리
- 새 코드는 ES 모듈(
import/export)로 쓰고, package.json에 "type": "module"을 지정합니다. - CommonJS(
require/module.exports)는 오래된 코드와 패키지에서 여전히 자주 만납니다. dependencies와 devDependencies를 구분하고, package-lock.json은 커밋합니다.- 반복하는 명령은
scripts에 등록해 npm run으로 실행합니다. - 브라우저용 코드는 Vite나 esbuild 같은 번들러로 묶고 최적화합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.