Node.js 단일 스레드 특성 때문에 트래픽이 몰리면 Express 서버는 금방 병목을 드러낸다. 응답 크기를 줄이고, 중복 연산을 캐시하고, CPU 코어를 전부 쓰는 세 가지 축만 잡아도 처리량이 수 배 뛴다.
compression 미들웨어는 HTTP 응답을 gzip / Brotli로 압축해 전송 바이트를 줄인다. 설정 한 줄로 JSON·HTML 응답을 평균 60~80% 압축할 수 있다.
import express from "express";
import compression from "compression";
const app = express();
app.use(
compression({
level: 6, // 1(빠름)~9(고압축), 기본 6
threshold: 1024, // 1KB 미만은 압축 생략
})
);
gzip — DEFLATE 알고리즘 기반 압축 포맷. 대부분의 브라우저와 HTTP 클라이언트가 Accept-Encoding: gzip을 보내므로 별도 협상 없이 동작한다.
프로덕션에서는 Express 앞단의 Reverse Proxy(Nginx)에서 압축을 처리하는 편이 CPU 부하를 분리할 수 있어 더 효율적이다.
Client ──► Nginx (gzip) ──► Express ──► DB
↑
압축은 여기서
정적 파일은 Cache-Control 헤더로 브라우저와 CDN이 캐시하게 만든다.
app.use(
express.static("public", {
maxAge: "7d", // 브라우저 캐시 7일
etag: true, // 조건부 요청용 ETag 자동 생성
lastModified: true,
})
);
API 응답처럼 동적 데이터는 ETag 기반 조건부 요청으로 304를 돌려줘 바디 전송을 생략한다.
app.get("/api/products", (req, res) => {
const data = getProducts();
const etag = generateETag(data);
if (req.headers["if-none-match"] === etag) {
return res.status(304).end();
}
res.setHeader("ETag", etag);
res.setHeader("Cache-Control", "public, max-age=60");
res.json(data);
});
ETag — 리소스 버전을 나타내는 불투명 토큰. 서버가 값을 바꾸지 않으면 클라이언트는 캐시된 데이터를 재사용한다.
| 헤더 | 용도 | 예시 값 |
|---|---|---|
Cache-Control | 캐시 정책 선언 | public, max-age=3600 |
ETag | 리소스 버전 식별 | "abc123" |
Last-Modified | 수정 시각 기준 캐시 | Wed, 23 Aug 2026 00:00:00 GMT |
Node.js는 기본적으로 단일 코어만 쓴다. cluster 모듈로 Worker 프로세스를 CPU 수만큼 띄우면 병렬 처리가 가능하다.
import cluster from "cluster";
import os from "os";
if (cluster.isPrimary) {
const cpus = os.cpus().length;
for (let i = 0; i < cpus; i++) cluster.fork();
cluster.on("exit", () => cluster.fork()); // 재시작 보장
} else {
// 각 Worker가 독립적으로 Express 실행
const { app } = await import("./app.js");
app.listen(3000);
}
PM2를 쓰면 코드 수정 없이 같은 효과를 얻는다.
# ecosystem.config.yaml
apps:
- name: api
script: dist/server.js
instances: max # CPU 수만큼 자동
exec_mode: cluster
watch: false
env:
NODE_ENV: production
pm2 start ecosystem.config.yaml
pm2 monit # 실시간 CPU/메모리 확인
PM2 — Node.js 프로세스 매니저. Cluster 모드, 무중단 재시작(graceful reload), 로그 수집을 한 번에 제공한다.
최적화 전후를 수치로 확인해야 방향이 보인다. autocannon으로 로컬에서 빠르게 부하 테스트를 돌린다.
npx autocannon -c 100 -d 10 http://localhost:3000/api/products
# -c: 동시 연결 수, -d: 테스트 시간(초)
압축 → 캐시 → 클러스터 순으로 하나씩 적용하고 RPS(Requests Per Second)와 레이턴시 p99를 기록하면 각 기법의 실제 기여도를 분리해서 볼 수 있다.