목차
- Node 26 업그레이드 변경사항의 배경과 릴리스 일정
- 변경 항목을 비교하는 기준
- 제거된 API: Node 26 업그레이드 변경사항 중 호환성을 깨는 항목
- 런타임 deprecation으로 격상된 항목과 대응
- 새 기능과 엔진 변경: Temporal, V8 14.6, 애드온 ABI
- 수치와 조건으로 보는 24 LTS 유지 vs Node.js 24에서 26 마이그레이션
- 자주 묻는 질문
- 업그레이드 전 점검 순서와 이어지는 주제
Node.js 22나 24에서 26으로 올린 뒤 서버가 첫 요청을 처리하다 TypeError: res.writeHeader is not a function을 던지며 응답하지 못하는 경우가 있다. Node 26 업그레이드 변경사항 중 v26.0.0에서 writeHeader()와 _stream_* 레거시 모듈이 완전히 제거된 것이 원인이다. v24 이하에서는 동작하던 코드가 v26부터는 실행되지 않는다.
런타임 에러만 문제가 되는 것도 아니다. v26의 NODE_MODULE_VERSION은 147이다. Node-API를 쓰지 않고 V8·NAN에 직접 의존해 이전 버전 기준으로 컴파일된 네이티브 애드온은 버전 불일치로 로드에 실패한다. Node-API만 사용하는 애드온은 재컴파일 없이 로드될 수 있다. 설치 단계에서 빌드 도구 요구사항이 걸리는 경우도 있다.
이 글은 2026-09 기준 Node.js 26.0.0 릴리스 노트와 Node.js Deprecated APIs 문서를 근거로 변경 항목을 영향 수준별로 나누고, NestJS 프로젝트에서 어디를 점검해야 하는지 비교한다.
Node 26 업그레이드 변경사항의 배경과 릴리스 일정
Node.js 26.0.0은 2026년 5월 5일에 Current 버전으로 출시됐다. 짝수 메이저라 LTS로 올라갈 예정이다. Node.js 릴리스 일정 페이지에 따르면 현재 상태는 다음과 같다.
| 버전 | 코드네임 | 상태 (2026-09 기준) | 업그레이드 관점 |
|---|---|---|---|
| v26 | — | Current | 2026년 10월 LTS 전환 예정 |
| v24 | Krypton | LTS | 현재 운영 기준선으로 적합 |
| v22 | Jod | LTS | 유지는 가능하나 이전 계획 필요 |
| v20 | Iron | EOL | 보안 패치 없음, 즉시 이전 대상 |
Node.js 릴리스 일정 페이지에는 v26의 정확한 LTS 전환일이 나와 있지 않다. 페이지에는 “메이저 버전은 6개월 동안 Current 단계를 거친다”고만 적혀 있다. 출시일인 5월 5일에서 6개월을 더하면 11월 초가 되므로, 이 문장만으로는 정확한 전환일을 정할 수 없다. 현재 파악된 전환 예정 시점은 2026년 10월이지만, 공식 날짜는 아직 발표되지 않았다. 배포 일정을 특정 날짜에 맞춰 잡지 말고, 전환 직전에 릴리스 일정 페이지를 다시 확인하는 편이 안전하다.
Node.js 27부터 바뀌는 연간 릴리스 주기
버전 선택에 영향을 주는 또 다른 변화는 릴리스 주기다. Node.js 27부터는 메이저 릴리스가 1년에 한 번으로 줄어든다. 모든 메이저 버전이 6개월 Current 단계를 거쳐 LTS가 되고, Current 앞에는 6개월 Alpha 단계가 추가된다.
flowchart LR
A[Alpha 6개월] --> B[Current 6개월]
B --> C[LTS 전환]
C --> D[유지보수 후 EOL]
이 구조에서는 홀수 버전이 “LTS가 되지 않는 실험 버전”이라는 기존 공식이 더 이상 성립하지 않는다. v26은 짝수 버전만 LTS가 되는 기존 방식이 적용되는 마지막 버전이다. 24 LTS에서 26 LTS로 넘어갈지, 27 이후 연간 주기를 기다릴지가 운영 계획의 쟁점이 된다.
Current 단계에서는 semver-minor 변경이 계속 들어오고, LTS로 전환된 뒤에는 안정성 위주의 패치만 반영된다. 프로덕션 기준선을 잡을 때 Current 버전을 곧바로 채택하면 마이너 릴리스마다 동작 변화를 추적해야 한다.
Node 26 업그레이드 변경사항은 항목 수가 많지만, 대응 순서를 정할 때는 “언제, 어떤 형태로 드러나는가”를 기준으로 나누는 것이 실용적이다. 같은 변경이라도 빌드 단계에서 드러나면 배포 전에 걸러지고, 특정 요청 경로에서만 드러나면 운영 중에 발견된다.
| 분류 | 대표 항목 | 드러나는 시점 | 증상 | 대응 우선순위 |
|---|---|---|---|---|
| 제거(End-of-Life) | writeHeader(), _stream_*, DEP0182 | 해당 코드 실행 시 | TypeError, 모듈 로드 실패 | 최우선 |
| 플래그 제거 | --experimental-transform-types | 프로세스 시작 시 | 기동 실패 | 최우선 |
| 런타임 deprecation | DEP0205, DEP0203, DEP0204, DEP0201 | 해당 API 첫 사용 시 | DeprecationWarning 1회 출력 | 다음 메이저 전까지 |
| ABI 변경 | NODE_MODULE_VERSION 147 | 설치·로드 시 | Node-API 미사용 네이티브 애드온 로드 실패 | 최우선 |
| 빌드 요구사항 | GCC 13.2, Python 3.9 지원 종료 | 소스 빌드 시 | 컴파일 실패 | 소스 빌드 환경만 해당 |
| 신규 API | Temporal, getOrInsert() | 도입 시점에 선택 | 없음 | 선택 사항 |
가장 위험한 것은 첫 번째 행이다. 에러 처리 경로나 스트리밍 응답처럼 테스트 커버리지가 낮은 곳에 제거된 API가 남아 있으면, 정상 요청은 통과하고 예외 상황에서만 서버가 실패한다.
제거된 API: Node 26 업그레이드 변경사항 중 호환성을 깨는 항목
v26.0.0의 semver-major 제거 항목은 네 가지로 요약된다. ServerResponse.prototype.writeHeader()의 완전 제거, 레거시 스트림 모듈 여섯 개의 제거, crypto 관련 DEP0182의 end-of-life 전환, --experimental-transform-types 플래그 제거다.
writeHeader()에서 writeHead()로
Deprecated APIs 문서의 DEP0063에 따르면 ServerResponse.prototype.writeHeader()는 v26.0.0에서 End-of-Life로 제거됐다. 대체 메서드는 writeHead()이며 인자 형태가 같아 메서드 이름만 바꾸면 된다.
// Deprecated
response.writeHeader(200, { 'Content-Type': 'text/plain' });
// Correct
response.writeHead(200, { 'Content-Type': 'text/plain' });
같은 응답 처리를 여러 곳에서 반복한다면 writeHead() 호출을 helper로 모아두면 이후 변경 지점이 한 곳으로 줄어든다.
function sendText(response, status, body) {
response.writeHead(status, { 'Content-Type': 'text/plain' });
response.end(body);
}
module.exports = { sendText };
NestJS 애플리케이션은 기본적으로 HTTP 어댑터가 응답을 대신 작성하므로, 프레임워크 추상화만 쓰는 컨트롤러에는 영향이 없는 편이다. 문제는 데코레이터로 플랫폼의 원본 응답 객체를 주입받아 직접 헤더를 쓰는 컨트롤러, 전역 예외 필터, 헬스 체크 엔드포인트처럼 저수준 응답을 다루는 코드다. 이런 코드는 호출 경로가 예외 상황에 한정되는 경우가 많아 단위 테스트에서 누락되기 쉽다.
stream* 레거시 모듈에서 node:stream으로
v26.0.0에서 _stream_wrap, _stream_readable, _stream_writable, _stream_duplex, _stream_transform, _stream_passthrough가 모두 제거됐다. DEP0193은 node:_stream_* 내부 모듈 대신 공개 모듈 node:stream에서 클래스를 가져오도록 안내한다.
// Deprecated
const Readable = require('node:_stream_readable');
// Correct
const { Readable } = require('node:stream');
ESM이나 TypeScript로 빌드하는 NestJS 프로젝트라면 import 구문도 같은 원칙으로 바꾼다. 여러 클래스를 한 번에 가져오면 파일마다 흩어진 require를 정리하기도 수월하다.
import { Readable, Writable, Duplex, Transform, PassThrough } from 'node:stream';
export function toReadable(chunks) {
return Readable.from(chunks);
}
직접 작성한 코드보다 의존성 쪽이 더 자주 문제가 된다. 오래된 스트림 유틸리티 패키지가 _stream_readable을 직접 require하는 경우가 있어, 애플리케이션 코드에 해당 문자열이 없어도 node_modules 안에서 로드 실패가 발생할 때가 있다.
출처에 따라 두 항목의 현재 Type이 다르게 나온다. Deprecated APIs 문서에는 Runtime으로 적혀 있지만, 26.0.0 릴리스 노트는 DEP0182를 end-of-life로, `_stream_*` 모듈을 완전 제거로 기록하고 있다. 이 글은 릴리스 노트 기준으로 서술한다.
crypto 관련 DEP0182는 v26에서 end-of-life로 바뀌었다. 이 항목의 대상은 일반적인 인증·서명 호출 전체가 아니라, AES-GCM 복호화에서 authTagLength를 명시하지 않은 채 짧은 인증 태그를 받아들이는 코드다. createDecipheriv()로 GCM 복호화를 하면서 태그 길이를 지정하지 않는 곳이 있는지 확인하고, 짧은 태그를 써야 한다면 authTagLength를 명시적으로 넘기도록 바꿔야 한다.
--experimental-transform-types 플래그도 제거됐다. tsc로 JavaScript를 먼저 만든 뒤 실행하는 일반적인 NestJS 빌드에는 영향이 없다. 반면 이 플래그로 TypeScript를 Node.js에서 직접 실행하던 스크립트, 마이그레이션 러너, 개발 서버 설정은 프로세스 시작 단계에서 실패하게 된다. type stripping이 stable로 승격되어 플래그 없이 .ts를 실행할 수 있다는 내용은 서드파티 자료에서만 확인되고 공식 문서에서는 확인되지 않았으므로, 대체 실행 방식은 릴리스 노트를 직접 확인한 뒤 정하는 것이 맞다.
코드베이스에서 제거 대상 찾기
제거 항목은 문자열 검색으로 대부분 걸러진다. 애플리케이션 코드와 의존성을 따로 검색하면 수정 주체를 구분하기 쉽다.
grep -rnE "writeHeader\(|_stream_(wrap|readable|writable|duplex|transform|passthrough)" src/
grep -rlE "_stream_(wrap|readable|writable|duplex|transform|passthrough)" node_modules/ | head -n 20
grep -rn "experimental-transform-types" package.json scripts/ Dockerfile
NestJS 프로젝트에서 검색 결과가 주로 나오는 위치는 다음과 같은 구조로 정리할 수 있다.
src/
├── main.ts ← 로더 등록(module.register) 여부
├── app.module.ts
├── common/
│ ├── filters/ ← 원본 응답 객체에 writeHeader 호출
│ └── streams/ ← _stream_* require
├── health/
│ └── health.controller.ts ← 저수준 응답 작성
└── auth/
└── crypto.service.ts ← CryptoKey 전달(DEP0203), GCM authTagLength 미지정(DEP0182)
런타임 deprecation으로 격상된 항목과 대응
v26에서 경고 단계가 올라간 항목은 당장 서버를 멈추지 않는다. 대신 해당 API를 처음 사용할 때 DeprecationWarning이 기본적으로 한 번 출력되고, 다음 메이저 버전에서 제거될 가능성이 커진 상태다.
| 코드 | 대상 | v26 상태 | 대응 방향 |
|---|---|---|---|
| DEP0205 | module.register() | 런타임 deprecation | 대체 방식 확인 후 로더 등록 코드 이전 (module.registerHooks() 검토) |
| DEP0203 | node:crypto API에 CryptoKey 직접 전달 | 런타임 deprecation | 전달 경로 분리 |
| DEP0204 | 추출 불가능한 CryptoKey로 KeyObject.from() 호출 | 런타임 deprecation | 키 생성·변환 흐름 재검토 |
| DEP0201 | Duplex.toWeb()에 options.type 전달 | 런타임 deprecation | options.readableType으로 변경 |
module.register() 사용처 점검
module.register()는 커스텀 로더를 등록할 때 쓰인다. APM 에이전트, ESM 경로 별칭 처리, 테스트 도구의 계측 코드가 내부에서 호출하는 경우가 많아 애플리케이션 코드에서 직접 호출하지 않아도 경고가 뜨기도 한다.
Deprecated APIs 문서의 DEP0205 항목에는 공식 대체 API가 적혀 있지 않다. 같은 node:module에 훅 등록 API인 module.registerHooks()가 있어 이전 후보로 검토할 수 있지만, 대체 경로는 node:module 문서에서 직접 확인해야 한다. 의존 패키지가 경고를 일으키는 경우에는 해당 패키지를 특정하고 Node 26 대응 버전을 추적하면 된다. 직접 로더를 작성한 경우에는 module.registerHooks()로 옮기는 것을 검토하되, 훅 구현 방식이 module.register()와 같지 않으므로 node:module 문서의 훅 명세를 확인한 뒤 옮겨야 한다.
CryptoKey 관련 두 항목
DEP0203과 DEP0204는 Web Crypto의 CryptoKey와 node:crypto의 키 객체를 섞어 쓰는 코드를 겨냥한다. JWT 서명, 웹훅 서명 검증처럼 NestJS 서비스 계층에서 키를 주고받는 모듈이 주요 점검 대상이다. 키를 node:crypto 쪽 객체로만 다룰지, Web Crypto 쪽으로 통일할지를 먼저 정해두면 수정 범위가 줄어든다.
런타임 deprecation 경고를 로그 필터로 숨기면 다음 메이저에서 제거될 때 사전 신호 없이 장애로 이어진다. 경고는 스테이징 환경에서 수집해 발생 위치와 패키지 이름을 목록으로 남기고, 서명·검증 코드처럼 보안과 직결되는 경로는 대체 방식으로 옮긴 뒤 테스트로 확인해야 한다.
v26.0.0 기준 V8은 14.6.202.33(Chromium 146 계열)이며, 이후 마이너 릴리스에서 패치 버전이 올라갈 수 있다. Temporal API가 기본으로 켜져 있고, [Weak]Map.prototype.getOrInsert()/getOrInsertComputed(), Iterator.concat()이 추가됐다. HTTP 클라이언트 구현인 Undici는 8.0.2로 올라갔다.
Node.js 26 Temporal API
Temporal은 기존 Date의 가변 객체 문제와 시간대 처리 한계를 대체하는 표준 API다. Node.js 쪽 사용 예제는 릴리스 노트에 포함되어 있지 않아, 아래 코드는 언어 표준에 정의된 기본 사용 형태만 보여준다.
const now = Temporal.Now.instant();
console.log(now.toString()); // ← 현재 시각 (UTC ISO 문자열)
const release = Temporal.PlainDate.from('2026-05-05');
const sixMonths = release.add({ months: 6 });
console.log(sixMonths.toString()); // ← 2026-11-05
const diff = release.until(sixMonths, { largestUnit: 'day' });
console.log(diff.days); // ← 184
add()가 원본을 바꾸지 않고 새 객체를 반환한다는 점이 Date와의 차이다. NestJS에서 날짜 변환 파이프나 DTO 직렬화를 Temporal로 옮길 경우, 사용 중인 TypeScript 버전이 Temporal 타입 정의를 제공하는지 먼저 확인해야 한다. 또한 v24 이하 런타임과 코드를 공유한다면 기능 존재 여부를 분기하지 않고서는 쓸 수 없다.
Map getOrInsert와 Iterator.concat
getOrInsert()는 키가 없을 때만 기본값을 넣고 값을 반환한다. 캐시나 그룹핑 코드에서 반복되던 has() → set() → get() 패턴이 한 줄로 줄어드는 것이다.
function loadConfig(key) {
return { key, loadedAt: Date.now() };
}
const cache = new Map();
cache.getOrInsert('users', []).push('kim');
cache.getOrInsert('users', []).push('lee');
console.log(cache.get('users')); // ← ['kim', 'lee']
const config = cache.getOrInsertComputed('config', loadConfig);
console.log(config.key); // ← 'config'
const merged = [...Iterator.concat([1, 2], new Set([3, 4]))];
console.log(merged); // ← [1, 2, 3, 4]
getOrInsertComputed()는 키가 없을 때만 콜백을 호출하므로, 기본값 생성 비용이 큰 경우에 적합하다.
NODE_MODULE_VERSION 147과 네이티브 애드온 재빌드
NODE_MODULE_VERSION은 네이티브 애드온의 ABI 호환성을 나타내는 값이다. v26에서는 147이며, V8·NAN에 직접 의존해 이전 값으로 빌드된 .node 바이너리는 로드되지 않는다. 반면 Node-API만 사용하는 애드온은 ABI가 메이저 버전 간에 유지되므로 재컴파일 없이 로드될 수 있다. 해시, 이미지 처리, DB 드라이버처럼 네이티브 바인딩을 포함한 패키지는 각각 Node-API 기반인지 먼저 확인해야 한다.
현재 런타임의 값은 표준 API로 바로 확인할 수 있다.
console.log(process.version); // ← v26.x.x
console.log(process.versions.modules); // ← '147'
console.log(process.versions.v8); // ← 14.6 계열
재빌드는 CI와 컨테이너 이미지에서 Node 26 기준으로 node_modules를 새로 설치하는 방식이 가장 확실하다. 로컬에서 v24로 설치한 node_modules를 이미지에 복사하는 Dockerfile은 이 단계에서 실패하기 쉬운 구조다. 소스에서 Node.js를 직접 빌드하는 환경은 GCC 13.2가 필요하고 Python 3.9 지원이 종료됐으므로 빌드 이미지도 함께 갱신해야 한다.
네이티브 패키지가 플랫폼별 사전 빌드 바이너리를 배포하는 경우, Node-API를 쓰지 않는 패키지라면 ABI 147용 바이너리가 없을 때 설치 시 소스 빌드로 넘어간다. 빌드 도구가 없는 slim 이미지에서는 이 시점에 설치가 실패하므로, 의존성별 Node 26 지원 릴리스를 먼저 확인해두는 편이 안전하다.
수치와 조건으로 보는 24 LTS 유지 vs Node.js 24에서 26 마이그레이션
성능 비교 수치는 공식 릴리스 노트에 벤치마크 형태로 제공되지 않는다. 따라서 선택 근거는 성능이 아니라 확인 가능한 버전 수치와 지원 기간, 그리고 코드베이스의 변경 비용이다.
| 항목 | v24 (Krypton) 유지 | v26 전환 |
|---|---|---|
| 현재 상태 | LTS | Current → 2026년 10월 LTS 예정 |
| V8 | 이전 계열 | 14.6 계열 (v26.0.0 기준 14.6.202.33) |
| Undici | 이전 메이저 | 8.0.2 |
NODE_MODULE_VERSION | 이전 값 | 147 (Node-API 미사용 애드온은 재빌드 필요) |
| 제거된 API 영향 | 없음(기존 코드 동작) | writeHeader(), _stream_* 즉시 실패 |
| 신규 API | Temporal 지원 여부 확인 필요 | Temporal, getOrInsert() 사용 가능 |
| 다음 전환 시점 | 27 이후 연간 주기 | 27 이후 연간 주기 |
조건별 선택 기준
제거된 API 검색 결과가 비어 있고 네이티브 의존성이 없다면 v26 전환 비용은 낮은 편이다. 이 경우 LTS 전환을 기다리며 스테이징에서 Current를 먼저 검증하고, LTS 전환 직후 프로덕션에 반영하는 순서가 무난하다.
네이티브 애드온이 여러 개이거나 유지보수가 멈춘 스트림 유틸리티에 의존한다면 사정이 다르다. v24 LTS를 유지하면서 의존성 교체를 먼저 끝내는 쪽이 위험이 적다. v24는 아직 LTS이므로 급하게 옮길 이유가 약하다.
v22에 머물러 있는 서비스라면 v24를 거칠지, 26으로 바로 갈지가 문제가 된다. 제거 항목이 v24에서 항상 런타임 경고로 드러나는 것은 아니므로, v24에서 검색으로 제거 대상을 먼저 정리하고 경고도 없앤 뒤 26으로 넘어가면 원인 분리가 쉬워진다. v20은 EOL이라 판단의 여지가 없다.
Temporal을 적극적으로 쓰고 싶은 신규 프로젝트는 v26에서 시작하는 것이 자연스럽다. 기존 서비스는 도입 동기로 삼기에는 Temporal 하나만으로 근거가 부족한 경우가 많다.
관련 글
- NestJS Clean Architecture 패턴 3가지 비교 — Hexagonal, Onion, Layered 선택 가이드 – NestJS에서 적용 가능한 Clean Architecture 패턴을 Hexagonal, Onion, Layered 세 가지로 나누어 계층…
- NestJS 시작하기 – 설치부터 설정까지 – NestJS 를 시작 하려면 첫걸음부터 시작해야 합니다. 이 포스트에서는 설치부터 초기 설정에 이르는 NestJS 시작 가이드를 제공하며,…
자주 묻는 질문
Node 26 업그레이드 변경사항 중 가장 먼저 확인할 항목은 무엇인가?
프로세스를 멈추게 하는 항목부터 확인한다. writeHeader() 호출, _stream_* require, --experimental-transform-types 플래그 사용, 네이티브 애드온 목록이 여기에 해당한다. 런타임 deprecation은 경고만 남기므로 그 다음 순서로 둬도 된다.
writeHeader removed 에러는 NestJS 코드를 쓰지 않아도 생기나?
프레임워크 추상화만 쓰는 컨트롤러에서는 드물다. 원본 응답 객체를 직접 다루는 예외 필터나 의존 패키지 내부에서 호출하는 경우가 대부분이다. 스택 트레이스의 파일 경로로 애플리케이션 코드인지 node_modules인지를 먼저 구분하면 된다.
module.register deprecated 경고는 지금 고쳐야 하나?
v26에서는 경고만 출력되고 동작은 유지된다. 직접 작성한 로더는 node:module 문서에서 대체 방식을 확인한 뒤 module.registerHooks() 등으로 옮기는 것을 검토하고, 경고를 일으키는 의존 패키지는 특정한 뒤 해당 패키지의 업데이트를 추적하면 된다.
Node 26 LTS 일정은 확정됐나?
아직 확정되지 않았다. 알려진 전환 예정 시점은 2026년 10월이지만, 릴리스 일정 페이지에는 Active LTS 시작일이 날짜로 적혀 있지 않다. 배포 일정은 전환 직전에 이 페이지를 다시 확인한 뒤 정해야 한다.
업그레이드 전 점검 순서와 이어지는 주제
공식 문서에는 22·24에서 26으로 가는 단계별 마이그레이션 가이드가 없다. 위 항목을 조합하면 점검 순서는 다음과 같이 정리된다.
grep으로writeHeader(,_stream_*,--experimental-transform-types를 검색해 제거 대상을 목록화writeHeader()는writeHead()로,_stream_*는node:streamimport로 교체- 네이티브 의존성을 확인하고 Node 26 이미지에서
node_modules를 새로 설치 - 스테이징에서
DeprecationWarning로그를 수집해 DEP0205·DEP0203·DEP0204·DEP0201 발생 위치 기록 - AES-GCM 복호화 코드에서
authTagLength없이 짧은 인증 태그를 받는 곳이 있는지 DEP0182 기준으로 확인 - LTS 전환 시점에 맞춰 프로덕션 기준 버전을 확정
체크리스트가 끝난 뒤에는 Node.js 26 Temporal API로 날짜 처리 계층을 옮기는 작업과, NestJS 예외 필터에서 원본 응답 객체 의존을 줄이는 패턴이 자연스럽게 이어진다. Node.js 27 연간 릴리스 전환 이후의 버전 고정 전략과 컨테이너 이미지의 네이티브 애드온 빌드 캐시 구성도 같은 흐름에서 다룰 주제다.