"프로파일링은 느릴 것 같은 코드를 찾는 일이 아니야. 실제 워크로드에서 시간이 어디에 쓰이고 무엇이 메모리에 남는지 측정하는 일이야."
먼저 어떤 자원을 볼지 정해
- CPU: JavaScript 실행 시간이 어느 함수에 모이는지, 긴 동기 작업이 이벤트 루프를 막는지 본다.
- 메모리: 무엇이 할당되고 계속 참조되는지, 할당 속도와 가비지 컬렉션이 지연에 영향을 주는지 본다.
- 이벤트 루프와 I/O: 루프 지연, 외부 서비스 대기, 파일·DNS·암호화 작업의 동시성처럼 CPU 프로파일만으로 설명되지 않는 시간을 본다.
도구를 켜기 전에 재현할 요청과 비교할 지표를 정해. 평균만 보지 말고 p95나 p99 지연, 처리량, 오류율도 함께 기록해야 해. 변경 뒤에 같은 조건으로 다시 측정할 수 있어야 최적화가 실제로 효과가 있었는지 판단할 수 있어.
Inspector와 내장 프로파일 플래그
node --inspect server.mjs로 Inspector를 열면 Chrome DevTools 같은 호환 클라이언트에서 프로세스에 연결할 수 있어. CPU 프로파일을 녹화하고 힙 스냅샷을 비교하며 실행 중인 코드를 디버깅할 수 있지. Inspector 포트는 코드 실행 권한으로 이어질 수 있으므로 외부 네트워크에 공개하면 안 돼.
대화형 도구가 필요 없다면 --cpu-prof로 샘플링 CPU 프로파일을 파일에 남길 수 있어. --heap-prof는 할당을 샘플링한 힙 프로파일을 만들며, 특정 시점의 모든 객체를 담는 힙 스냅샷과는 목적이 달라. 메모리 누수를 찾을 때는 시간 간격을 둔 스냅샷이나 할당 프로파일을 증상에 맞춰 골라.
이벤트 루프 지연도 함께 측정해
node:perf_hooks의 monitorEventLoopDelay()는 이벤트 루프가 예정된 시점보다 얼마나 늦게 돌아왔는지 분포로 보여 줘. CPU 사용률이 높지 않아도 긴 동기 작업이나 과도한 콜백이 있으면 지연이 커질 수 있어. 한편 DB나 외부 API를 기다리는 시간은 이벤트 루프 지연이 아니라 트레이스와 외부 지표에서 드러날 수 있어. 한 신호만으로 원인을 단정하지 마.도구가 알려 주는 것과 알려 주지 않는 것
Clinic.js 같은 진단 도구는 CPU, 이벤트 루프, 비동기 흐름, 메모리 할당을 보고서로 묶어 탐색을 도와줘. 편리하지만 보고서가 원인을 자동으로 확정해 주는 건 아니야. 사용 중인 Node 버전과 도구 버전이 맞는지 확인하고, 같은 증상을 내장 프로파일과 서비스 지표에서도 교차 확인해.
Flame graph의 넓은 프레임은 샘플이 많이 모인 실행 경로를 뜻해. 그것이 곧 잘못된 코드라는 뜻은 아니야. 서비스의 핵심 작업이라 원래 시간을 많이 쓸 수도 있어. 줄였을 때 처리량이나 꼬리 지연이 개선되는지 재측정해야 해.
자주 만나는 병목
- 메인 스레드의 긴 동기 작업: 큰 JSON 파싱, 동기 암호화, 복잡한 정규식, 긴 반복문은 다른 요청 처리를 늦춘다.
- 외부 시스템 왕복: N+1 쿼리와 직렬 네트워크 호출은 CPU 프로파일보다 트레이스와 DB 지표에서 더 잘 보인다.
- libuv 스레드 풀 경합: 일부 파일·DNS·암호화 작업이 기본 스레드 풀을 공유한다. 크기를 키우기 전에 작업 수와 병렬성부터 측정해야 한다.
- 계속 자라는 참조: 제한 없는 캐시나 해제되지 않은 리스너는 힙 사용량을 올린다. 무엇이 객체를 붙잡는지 스냅샷에서 확인한다.
- 높은 할당률: 짧게 사는 객체를 대량으로 만들면 가비지 컬렉션 비용이 커질 수 있다. 할당 프로파일로 먼저 확인한다.
운영 신호에서 재현 가능한 실험으로
운영 환경의 메트릭과 트레이스는 어느 경로와 조건에서 문제가 생기는지 알려 줘. 그 단서를 바탕으로 민감한 데이터를 제거한 재현 워크로드를 만들고, 로컬이나 격리된 환경에서 프로파일을 수집해. 운영 프로파일링이 꼭 필요하다면 짧은 시간과 제한된 대상에 샘플링 방식으로 적용하고, 오버헤드와 출력 파일의 민감성을 함께 관리해야 해.