Node.js 앱이 시작이 느립니다. 어떤 모듈을 탓해야 할지 모릅니다.

발행: (2026년 4월 2일 AM 03:56 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

30초 안에 보는 문제

Node.js는 시작이 왜 느린지 이유를 알려주지 않습니다. 총 부팅 시간이라는 하나의 숫자만 제공하고, 세부적인 분석은 전혀 없습니다.

그 사이에:

  • 단일 require('sequelize')만으로도 400 ms가 조용히 추가될 수 있습니다.
  • 전이 의존성이 쌓이면서 — 하나를 require하면 Node가 300개의 모듈을 로드합니다.
  • 모듈 스코프에서 동기 작업(파일 읽기, 템플릿 컴파일, DB 연결 등)이 이벤트 루프를 차단해 애플리케이션이 시작되기 전까지 지연을 발생시킵니다.
  • 캐시된 모듈이라도 의존성 그래프에 엣지를 추가해 실제 병목을 가립니다.

이 문제는 그 어느 때보다 중요합니다. Lambda에서 실행 중이라면(콜드 스타트가 이제 청구됩니다), 서버리스 플랫폼이나 제로에서 스케일링되는 컨테이너에서도 — 시작 시간은 첫 번째 요청에서 사용자가 체감하는 지연이 됩니다.

Source:

coldstart가 실제로 하는 일

다음 명령을 어떤 Node 앱에든 실행해 보세요:

npx @yetanotheraryan/coldstart server.js

그러면 다음과 같은 결과가 나타납니다:

coldstart — 847ms total startup

  ┌─ express          234ms  ████████████░░░░░░░░
  │  ├─ body-parser    89ms  █████░░░░░░░░░░░░░░░
  │  ├─ qs             12ms  █░░░░░░░░░░░░░░░░░░░
  │  └─ path-to-regex   8ms  ░░░░░░░░░░░░░░░░░░░░
  ├─ sequelize        401ms  █████████████████████  ⚠ slow
  │  ├─ pg            203ms  ███████████░░░░░░░░░
  │  └─ lodash         98ms  █████░░░░░░░░░░░░░░░
  └─ dotenv             4ms  ░░░░░░░░░░░░░░░░░░░░

event loop max 42ms, p99 17ms, mean 4.3ms
modules 312 total, 59 cached
time split 286ms first‑party, 503ms node_modules

트리는 부모 → 자식 로드 관계를 포함 타이밍(전체 서브트리가 걸린 시간)과 심각도에 따라 색칠된 막대 차트로 보여줍니다. 한눈에 다음을 알 수 있습니다:

  • sequelize가 문제입니다.
  • sequelize 내부에서 pglodash가 대부분의 작업을 수행하고 있습니다.

내부 작동 방식

핵심 기법은 간단합니다 — coldstartModule._load를 몽키패치합니다 (Node가 모든 require() 호출 시 내부적으로 사용하는 함수).

  1. 원본 _load가 실행되기 전에 performance.now()와 부모 모듈을 기록합니다.
  2. Node가 작업을 수행하도록 둡니다 — 모듈을 해석하고, 컴파일하고, 실행합니다.
  3. _load가 반환된 후 종료 시간을 기록합니다.

원시 이벤트는 다음과 같습니다:

{
  "request": "...",
  "resolvedPath": "...",
  "parentPath": "...",
  "startMs": 123.45,
  "endMs": 124.67,
  "cached": false
}

ESM의 경우, Node의 module.register() 로더 훅( Node 18.19+에서 사용 가능)을 이용해 resolveload 이벤트를 포착하고, 타이밍 데이터를 메시지 채널을 통해 메인 트레이서로 전달합니다.

앱이 시작을 마치면 트레이서는 다음을 구축합니다:

  • 트리 – 런타임에 로드된 실제 부모 → 자식 의존성 그래프.
  • 포함 시간 – 모듈 그 모듈이 가져온 모든 하위 모듈의 전체 실시간(벽시계) 시간.
  • 배제 시간 – 자식 모듈을 제외한, 해당 모듈 자체 초기화 비용만.
  • 이벤트‑루프 통계 – 시작 중 최대, 평균, p99 차단 시간(perf_hooks 사용).
  • 분할 – 순수 코드와 node_modules가 차지한 시간 비율.

포함 시간과 배제 시간의 구분이 핵심입니다. 포함 시간은 높지만 배제 시간이 낮은 모듈은 단순히 관문 역할을 하는 경우가 많습니다 — 무거운 자식 모듈을 불러오지만 자체적으로는 느리지 않습니다. 배제 시간이 높다는 것은 그 특정 모듈이 로드 시점에 비용이 많이 드는 작업을 수행하고 있다는 뜻입니다.

사용 방법 3가지

1. CLI (가장 쉬움 – 모든 앱을 프로파일링)

coldstart server.js
coldstart --json server.js          # 기계가 읽을 수 있는 출력
coldstart -- node --inspect app.js  # Node 플래그를 그대로 전달

2. 프로그래밍 API (자신의 도구에 임베드)

import { monitor, renderTextReport } from '@yetanotheraryan/coldstart'

const done = monitor()
require('./bootstrap')
require('./server')

console.log(renderTextReport(done()))

3. 프리로드 모드 (코드 변경 없이)

node --require @yetanotheraryan/coldstart/register server.js
# 또는 ESM인 경우:
node --import @yetanotheraryan/coldstart/register server.mjs

또한 renderFlamegraphHtml() 내보내기를 통해 브라우저에서 열 수 있는 독립형 HTML 플레임그래프를 생성할 수 있습니다 — 팀과 공유하거나 PR 설명에 삽입할 때 유용합니다.

실제로 작업에서 발견한 내용

Running coldstart를 우리 서비스에 적용했을 때 1초도 안 되어 원인이 명확해졌습니다: 세 단계 깊은 전이 의존성이 모듈 스코프에서 동기 파일 I/O를 수행해 설정 파일을 읽고 있었습니다. 의존성 버전 업데이트가 초기화 경로를 바꿨습니다.

해결책은 한 줄짜리 지연 require()를 사용해 로드를 중요한 시작 경로 밖으로 옮기는 것이었습니다. 부팅 시간이 ~320 ms로 돌아왔습니다.

완전한 투명성

저는 이를 구축하는 동안 Claude를 많이 사용했습니다 — ESM 로더 훅을 스캐폴딩하고, flamegraph HTML 템플릿을 생성하며, 트리‑렌더링 로직을 반복해서 다듬는 데 활용했습니다. 핵심 아이디어(performance.now() 책갈피로 Module._load를 패치하는)와 전체 아키텍처는 제가 직접 만든 것이지만, AI가 구현을 가속화했습니다. 지금은 많은 솔로 오픈‑소스가 이렇게 만들어지고 있다고 생각하며, 저는 이를 솔직히 밝히고 싶습니다.


--cpu-prof만 사용하지 않나요?

--cpu-prof어떤 코드가 실행되고 있는지 이해하는 데는 훌륭하지만 어떤 모듈 로드가 느린지 혹은 우리를 여기까지 이끈 의존성 체인을 알려주지는 못합니다.
V8 내부와 함수 호출에 대한 플레임그래프는 얻을 수 있지만, 타이밍이 포함된 require() 트리 맵은 얻을 수 없습니다.

coldstart는 의도적으로 더 높은 수준에서 동작합니다. 그것은 “어떤 npm 패키지가 내 시작을 느리게 만들고 있나요?” 라는 질문에 답하고, “어떤 V8 내장 함수가 뜨거운가?” 라는 질문에는 답하지 않습니다.

두 도구는 상호 보완적입니다:

  • coldstart를 사용해 느린 모듈을 찾습니다.
  • 그 모듈이 왜 느린지 이해하려면 --cpu-prof를 사용합니다.

현재 상황 및 부족한 점

현재 구현된 기능

  • CommonJS 프로파일링
  • ESM 프로파일링 (Node 18.19+)
  • CLI, 프로그래밍 API, 프리로드 모드
  • 텍스트 보고서, JSON 보고서, HTML 플레임그래프

아직 구현되지 않음

  • 동적 import() 추적
  • 시작 최적화를 위한 워치 모드
  • CI 통합 (시작 시간이 임계값을 초과하면 실패)

아직 초기 단계입니다. API는 일상적인 사용에 충분히 안정적이지만, 출력 형식을 계속 다듬고 있으며 실제로 필요한 몇 가지 기능을 고려하고 있습니다.

사용해 보기

npm install @yetanotheraryan/coldstart

또는 npx 로 한 번 실행해 보세요:

npx @yetanotheraryan/coldstart your-app.js

GitHub:

If this is useful to you, a star on the repo genuinely helps with discoverability. And if you run it on your app and find something interesting — I’d love to hear about it in the comments. What was your slowest module?

저는 Aryan입니다 — 부업으로 Node.js용 오픈‑소스 도구를 만들고 있습니다. 다른 프로젝트는 GitHub에서 확인하실 수 있습니다.

0 조회
Back to Blog

관련 글

더 보기 »