Quick Flow
import { createServer } from "node:http";
const server = createServer((req, res) => res.end("ok"));
server.listen(3000);
let shuttingDown = false;
function closeServer() {
return new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
server.closeIdleConnections();
});
}
async function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
console.log(`received ${signal}; draining connections`);
const forceClose = setTimeout(() => server.closeAllConnections(), 10_000);
forceClose.unref();
try {
await closeServer();
process.exitCode = 0;
} catch (error) {
console.error(error);
process.exitCode = 1;
} finally {
clearTimeout(forceClose);
}
}
process.on("SIGTERM", () => void shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));SIGTERM과 SIGINT는 같은 종료 흐름으로 모읍니다. 먼저 새 연결을 닫고 idle 연결을 정리한 뒤, 진행 중인 요청을 기다립니다. deadline을 넘긴 연결만 강제 종료하고 process.exitCode로 결과를 남깁니다. process.exit()는 로그와 I/O를 끊을 수 있으므로 복구 불가능한 마지막 수단입니다.
종료 흐름
먼저 구분해 둘 기본형은 아래입니다.
- 운영 종료 신호:
SIGTERM - 로컬 중단 신호:
SIGINT - 정리 후 자연 종료:
process.exitCode = 1 - 정말 마지막 즉시 종료:
process.exit(1) - 종료 중복 방지:
if (shuttingDown) return
SIGTERM과 SIGINT
SIGTERM: 운영 환경 종료 요청SIGINT: 보통 Ctrl+C
실전에서는 둘 다 graceful shutdown 경로로 묶는 경우가 많습니다.
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));process.exit()를 바로 치지 않는 이유
서버, DB 연결, 열린 파일, 로그 flush 같은 정리 작업이 남아 있을 수 있기 때문입니다.
즉 종료 카드는 사실상 "정리 순서" 카드입니다.
- 새 요청 받기 중단
- 연결 정리
- 로그/큐 flush
- 마지막에 종료 코드 확정
process.exitCode와 직접 종료
즉시 종료가 아니라 "정리 후 종료 코드만 예약"하고 싶을 때는 process.exitCode가 더 자연스럽습니다.
반면 정말 마지막에 프로세스를 끝낼 타이밍이면 process.exit()를 씁니다.
process.exitCode = 1; // 정리 후 종료
// ...
process.exit(1); // 즉시 종료종료 로직은 한 번만 타게 만든다
SIGTERM과 SIGINT를 둘 다 받거나, 종료 중 예외가 다시 올라오면 shutdown 함수가 중복 실행되기 쉽습니다.
실전에서는 shuttingDown 플래그를 두고 한 번만 정리 흐름에 들어가게 만드는 편이 안전합니다.
'exit' 이벤트는 정리 시작점이 아니라 마지막 동기 훅이다
process.on("exit") 안에서는 비동기 정리를 기대하면 안 됩니다.
서버 닫기, DB flush, 큐 drain 같은 작업은 signal handler나 명시적 shutdown 함수에서 먼저 처리해야 합니다.
언제 직접 종료할까
체크포인트
- 운영 종료는
SIGTERM - 로컬 중단은
SIGINT - 먼저 정리, 나중에 종료
- 종료 코드는 0/비0 의미를 분명히 둔다
- shutdown 함수는 중복 실행되지 않게 막는다
주의할 점
시그널 핸들러 안에서 바로 process.exit(0)을 호출하면 "깔끔하게 종료"가 아니라 "정리 생략 후 즉시 종료"가 될 수 있습니다. 종료 신호를 받았을 때 더 중요한 건 exit 호출 자체보다, 남은 자원과 연결을 어떤 순서로 닫을지입니다.
참고 링크
1 sources