SHIN
Node.js 실전 팁 20선
20편"추측하지 말고 측정하라." 최적화 전에 프로파일러로 실제 병목을 찾아야 합니다.
# 프로파일링하며 실행
node --prof app.js
# 부하 테스트 (wrk, autocannon 등)
npx autocannon -c 100 -d 10 http://localhost:3000/api
# isolate-*.log 파일 생성됨
node --prof-process isolate-*.log > profile.txtnpm install -g clinic
# Flame Graph (CPU 병목 시각화)
clinic flame -- node app.js
# Bubbleprof (비동기 병목 시각화)
clinic bubbleprof -- node app.js
# Doctor (종합 진단)
clinic doctor -- node app.js// 간단한 코드 경로 측정
console.time('db-query');
const users = await db.user.findMany({ include: { posts: true } });
console.timeEnd('db-query'); // db-query: 42.3ms
// 더 정밀한 측정
const start = performance.now();
await heavyOperation();
const elapsed = performance.now() - start;
console.log(`소요 시간: ${elapsed.toFixed(2)}ms`);import { AsyncLocalStorage } from 'async_hooks';
const requestContext = new AsyncLocalStorage();
// 미들웨어: 요청별 컨텍스트 설정
app.use((req, res, next) => {
requestContext.run({ requestId: crypto.randomUUID(), start: Date.now() }, next);
});
// 어디서나 요청 ID 접근
function logWithContext(message) {
const ctx = requestContext.getStore();
console.log(`[${ctx?.requestId}] ${message}`);
}// ❌ N+1 쿼리 문제
const users = await db.user.findMany();
for (const user of users) {
user.posts = await db.post.findMany({ where: { userId: user.id } });
}
// ✅ 한 번에 조인
const users = await db.user.findMany({
include: { posts: true }
});
// ❌ 큰 JSON 동기 파싱 (이벤트 루프 블로킹)
const huge = JSON.parse(fs.readFileSync('huge.json', 'utf-8'));
// ✅ 스트림 파싱 (stream-json 라이브러리)
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray.js';// 간단한 벤치마크 유틸
async function benchmark(name, fn, iterations = 1000) {
const times = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
await fn();
times.push(performance.now() - start);
}
times.sort((a, b) => a - b);
console.log(`[${name}] avg: ${(times.reduce((s, t) => s + t, 0) / iterations).toFixed(2)}ms, p99: ${times[Math.floor(iterations * 0.99)].toFixed(2)}ms`);
}
await benchmark('user-query', () => db.user.findMany());