Node.js 네이티브 TypeScript 실행 완전 가이드
기준 버전: Node.js 24 LTS. Node.js 26에서 달라지는 동작(enum 처리 변화 등)은 본문 중 별도 구획에서 다룹니다.
node app.ts가 그냥 된다는 게 어느 시점부터 사실이 되었습니다. 빌드 없이, 추가 도구 없이, 플래그 없이. Node.js 24 LTS에서 타입 스트리핑(type stripping)이 Stable로 승격되면서 .ts 파일을 직접 실행하는 것이 공식 표준 워크플로우로 자리잡았습니다.
이 글에서 다루는 내용은 세 가지입니다. 타입 스트리핑이 내부에서 어떻게 동작하는지, tsc 빌드 단계 없이 프로덕션에 배포하는 구체적인 패턴, 그리고 네이티브 실행을 써야 할 상황과 쓰지 말아야 할 상황을 구분하는 의사결정 기준. 세 번째가 실무에서 가장 직접적으로 유용합니다.
한 가지 전제: 이 글 전체는 ESM(ECMAScript Modules) 기반 코드베이스를 전제합니다. "module": "NodeNext", package.json의 imports 필드, .ts 확장자 컨벤션은 모두 ESM 환경에서 동작합니다. CommonJS 코드베이스를 사용하는 경우 일부 설정이 다르게 적용될 수 있으며, 해당 지점은 본문에서 별도로 표시합니다.
언제 네이티브 실행을 쓰고, 언제 쓰지 않을지
도입 개념 설명 전에 의사결정 기준을 먼저 제시합니다. 프로젝트가 이 조건을 만족하는지 확인하는 것이 가장 빠른 출발점이기 때문입니다.
핵심 개념
타입 스트리핑이란 무엇인가
TypeScript의 타입 어노테이션은 런타임에서 아무런 역할을 하지 않습니다. 그러니 실행 전에 그것들을 제거하면 됩니다. 바로 그게 타입 스트리핑입니다. tsc처럼 코드를 변환하거나 다운레벨링하는 게 아니라, 타입 관련 구문을 동일한 길이의 공백으로 치환합니다.
// 원본 .ts
function greet(name: string): string {
return `Hello, ${name}`;
}
// 스트리핑 후 (V8에 전달되는 실제 코드)
function greet(name ) {
return `Hello, ${name}`;
}공백으로 치환하기 때문에 라인 번호가 그대로 유지됩니다. 소스맵이 필요 없는 이유가 여기에 있습니다. 스택 트레이스에 찍히는 줄 번호가 원본 .ts 파일의 번호와 완전히 일치합니다.
이 과정을 담당하는 것이 Amaro입니다. Node.js 내장 TypeScript 로더로, 내부적으로 @swc/wasm-typescript — Rust 기반 SWC 파서의 WebAssembly 빌드 — 를 감싼 형태입니다. 별도 설치 없이 Node.js에 번들로 포함되어 있습니다.
Amaro는 외부 도구가 아니라 Node.js 로더 레이어의 내장 구현체입니다.
버전별 진화 경로
Deno와 Bun이 네이티브 TypeScript 지원으로 주목받으면서 Node.js 진영도 빠르게 움직였습니다. 아래는 각 릴리즈 노트 기준입니다.
| 버전 | 변화 |
|---|---|
| Node.js v22.6 (2024-08) | --experimental-strip-types 플래그 최초 도입 |
| Node.js v22.7 (2024-08) | --experimental-transform-types 추가 (enum·네임스페이스 지원) |
| Node.js v23.6 (2025-01) | 플래그 없이 기본 활성화 |
| Node.js v22.18 / v24.3 | 실험적 경고 메시지 제거 (각 릴리즈 노트 기준) |
| Node.js v24.12 | 타입 스트리핑 Stable 정식 승격 (릴리즈 노트 기준) |
| Amaro v1.0 (2025-06) | 공식 로더 1.0 릴리즈, TypeScript 5.8 지원, node_modules 처리 가능 (릴리즈 노트 기준) |
Node.js 26 관련 변경 (별도 구획): Node.js 26에서
--experimental-transform-types플래그가 제거되었습니다. Node.js 24 LTS에서는 아직 사용 가능하지만, 이 플래그에 의존하는 설계는 추후 버전 업그레이드 시 부담이 됩니다. enum 처리는 런타임이 아닌 빌드 도구 레이어로 이관되는 방향입니다.
지원되는 구문과 지원되지 않는 구문
타입 스트리핑이 모든 TypeScript를 처리하는 건 아닙니다. "지울 수 있는" 구문(erasable syntax)만 처리합니다. 런타임 코드를 생성하는 구문은 단순히 지워지지 않으니까요.
지원 (erasable syntax):
- 타입 어노테이션, 인터페이스, 타입 별칭
import type, 제네릭as캐스팅,satisfies연산자
미지원 (runtime code generation):
enum— 실제 JavaScript 객체를 생성함const enum— 인라인 상수를 생성함- 파라미터 프로퍼티 (
constructor(private name: string)) - 런타임 코드를 포함하는 네임스페이스
- 레거시
experimentalDecorators(NestJS 구버전 등)
"
--experimental-transform-types플래그를 쓰면 되지 않나?" Node.js 24 LTS에서는 이 플래그를 켜면enum과 네임스페이스를 런타임에서 변환할 수 있습니다. 다만 Node.js 26에서 이 플래그가 제거되었고, 신규 프로젝트에서 이에 의존하는 설계는 추후 버전 업그레이드 시 전면 수정이 필요해집니다. 프로덕션 권장 전략은enum을 리터럴 유니언이나as const객체로 대체하는 방향입니다.
그리고 중요한 점 하나 더: 타입 스트리핑은 타입 검사를 수행하지 않습니다. 타입 오류가 있어도 런타임에서 그냥 실행됩니다. CI에서 tsc --noEmit을 반드시 별도로 돌려야 하는 이유가 이것입니다.
실전 적용
개발 환경 — watch 모드
가장 간단한 시작점입니다. tsconfig.json 하나만 있으면 됩니다.
node --watch src/index.tsnodemon도, ts-node도, tsx도 필요 없습니다. 파일이 변경되면 자동으로 재시작됩니다. package.json scripts에 그대로 씁니다.
{
"scripts": {
"dev": "node --watch src/index.ts",
"start": "node src/index.ts",
"typecheck": "tsc --noEmit"
}
}.cts와 .mts 확장자도 동일하게 지원됩니다. CommonJS 파일을 .cts로 명시하는 혼합 모듈 환경에서도 네이티브 실행이 적용됩니다.
tsconfig.json 설정
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"erasableSyntaxOnly": true
}
}"erasableSyntaxOnly": true 옵션은 tsc --noEmit 실행 시 타입 스트리핑이 처리하지 못하는 구문(enum 등)을 에러로 잡아줍니다. 네이티브 실행으로 전환하기로 결정했다면 가장 먼저 켜두는 것이 좋습니다.
경로 별칭 처리
tsconfig.json의 paths 설정 — @/utils 같은 경로 별칭 — 은 런타임에서 해석되지 않습니다. Node.js는 tsconfig.json을 보고 모듈을 찾아주지 않습니다.
해결책은 package.json의 imports 필드입니다.
{
"imports": {
"#utils/*": "./src/utils/*.ts",
"#config": "./src/config/index.ts"
}
}// 사용할 때
import { logger } from '#utils/logger.ts';네이티브 실행 환경에서는 .ts 확장자를 그대로 씁니다. tsconfig.json에 "allowImportingTsExtensions": true를 추가하면 tsc --noEmit 단계에서도 이 확장자를 에러 없이 처리합니다. @ 대신 #을 prefix로 쓰는 것은 Node.js imports 필드의 규칙입니다.
기존 코드베이스에 @/ 경로 별칭이 많다면 마이그레이션 비용을 고려해야 합니다. 이 경우 tsx나 기존 빌드 단계를 유지하는 것이 더 현실적인 선택입니다.
Docker 프로덕션 이미지 최적화
기존 멀티스테이지 빌드와 비교해보면 차이가 명확합니다.
기존 방식:
# 빌드 스테이지
FROM node:24-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY src/ ./src/
COPY tsconfig.json .
RUN npm run build
# 런타임 스테이지
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]네이티브 실행:
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src/ ./src/
# tsconfig.json은 런타임에서 읽지 않으므로 COPY 생략 가능
CMD ["node", "src/index.ts"]typescript 패키지, ts-node, 빌드 스크립트, dist/ 디렉터리, 멀티스테이지 빌드가 모두 사라집니다. 이미지 크기와 빌드 시간 모두 줄어듭니다.
--omit=dev사용 이유: npm v9+에서--production플래그는 deprecated되었으며--omit=dev가 현재 권장 방식입니다.
AWS Lambda에서 .ts 핸들러 직접 배포
AWS Lambda가 Node.js 24 런타임을 공식 지원하면서 컨테이너 이미지 기반 Lambda에서 .ts 핸들러를 그대로 패키징하는 패턴이 가능해졌습니다.
// handler.ts
import type { APIGatewayProxyEvent } from 'aws-lambda';
export const handler = async (event: APIGatewayProxyEvent) => {
return {
statusCode: 200,
body: JSON.stringify({ message: 'Hello from TypeScript' }),
};
};dist/ 컴파일 없이 소스를 그대로 패키징하면 배포 단계가 단순해집니다.
단, 콜드 스타트 성능에 대해서는 주의가 필요합니다. 네이티브 실행은 init마다 Amaro와 SWC-WASM이 구동되어 타입 스트리핑을 수행합니다. 미리 컴파일된 .js 아티팩트는 그 비용을 생략합니다. 패키지 크기 감소가 콜드 스타트 개선으로 직결되지는 않으며, 실제 영향은 함수 크기와 실행 환경에 따라 다릅니다. 콜드 스타트가 핵심 요구사항이라면 실제 환경에서 측정 후 결정하는 것이 안전합니다.
CI 파이프라인 구성
# .github/workflows/ci.yml
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
- name: 의존성 설치
run: npm ci
- name: 타입 검사
run: npx tsc --noEmit
- name: 테스트
run: node --test
- name: 스모크 테스트 (앱 부팅 확인)
run: node src/index.ts타입 검사와 실행이 분리되어 있습니다. tsc --noEmit은 타입 검사만 하고 파일을 생성하지 않으므로 빌드 없이 타입 안전성을 검증하는 용도로 적합합니다. 마지막 스텝은 앱이 정상 기동되는지 확인하는 스모크 테스트입니다. 별도 통합 테스트 스위트가 있다면 해당 명령으로 교체하시면 됩니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 빌드 파이프라인 제거 | tsc 컴파일 단계 불필요 — CI 시간 단축, Docker 레이어 감소 |
| 빠른 기동 시간 | 로컬 개발 환경에서 ts-node 대비 약 9배 빠른 스타트업 (sebastian-staffa.eu 기준, 600줄 Express 서버에서 ts-node ~1.4초 vs 네이티브 200ms 미만; 측정 버전·환경에 따라 다를 수 있음) |
| 소스맵 불필요 | 타입을 공백으로 치환해 라인 번호가 원본 .ts와 동일하게 유지됨 |
| 제로 설정 | Node.js 24+에서 추가 플래그 없이 동작 |
| 공급망 단순화 | ts-node, tsx 등 devDependency 제거 가능 |
| watch 모드 내장 | node --watch와 조합 시 nodemon 불필요 |
단점 및 제한사항
| 항목 | 내용 |
|---|---|
| 타입 검사 없음 | 실행 시 타입 오류를 잡지 않음. CI에서 tsc --noEmit 별도 실행 필수 |
| enum 미지원 (기본 모드) | const enum, 일반 enum 기본 모드에서 사용 불가. --experimental-transform-types로 우회 가능하나 Node.js 26에서 제거됨 |
| tsconfig 경로 별칭 미지원 | paths 매핑이 런타임에서 해석되지 않음. package.json의 imports 필드로 대체 필요 |
| 레거시 데코레이터 미지원 | experimentalDecorators: true 기반 데코레이터 불가 (NestJS 구버전 등) |
| Lambda 콜드 스타트 | init마다 SWC-WASM 구동 비용 발생. 미리 컴파일된 JS 대비 중립~불리할 수 있음 |
실무에서 자주 겪는 실수
1. 타입 검사를 생략하는 경우
node app.ts가 잘 돌아가니 CI에서 타입 검사가 빠질 때가 있습니다. 타입 스트리핑은 타입을 검증하지 않습니다. tsc --noEmit을 CI 파이프라인에 명시적으로 추가해두는 것이 필요합니다.
2. enum을 쓰다가 런타임 에러를 만나는 경우
기존 코드베이스에 enum이 있는 채로 마이그레이션하면 SyntaxError가 납니다. tsconfig 설정 섹션에서 언급한 "erasableSyntaxOnly": true를 추가해두면 tsc --noEmit 단계에서 미리 잡을 수 있습니다. 네이티브 실행 전환 시 가장 먼저 켜두기를 권장하는 이유가 이것입니다.
3. 경로 별칭이 런타임에서 해석되길 기대하는 경우
@/utils/logger가 tsconfig에 있어서 IDE에서는 잘 되는데, 실행하면 Cannot find module 에러가 납니다. Node.js는 tsconfig.json의 paths를 보지 않습니다. package.json의 imports 필드로 전환하거나, tsx를 계속 쓰는 방향을 고려하면 됩니다.
4. Node.js v22.6 미만 환경에서 시도하는 경우
v22.6 미만에서는 이 기능이 없습니다. 플래그 자체가 인식되지 않습니다. 인프라 Node.js 버전 확인이 먼저입니다.
마치며
타입 스트리핑은 TypeScript의 타입 어노테이션을 공백으로 치환해 V8이 직접 실행할 수 있게 만드는 메커니즘이며, Node.js 24 LTS에서 Stable로 승격되어 추가 설정 없이 사용할 수 있습니다. 빌드 단계 제거와 Docker 이미지 경량화가 실질적인 이점이고, 타입 검사를 수행하지 않으므로 CI에서 tsc --noEmit은 반드시 유지해야 합니다. enum, 레거시 데코레이터, 복잡한 경로 별칭을 쓰는 프로젝트는 tsx나 기존 빌드 단계가 더 안정적인 선택입니다.
지금 바로 적용해보고 싶다면 이 순서로 진행하면 됩니다.
1단계 — 현재 프로젝트에 enum과 레거시 데코레이터가 있는지 확인합니다.
tsconfig.json에 "erasableSyntaxOnly": true를 추가한 뒤 tsc --noEmit을 돌려보면 호환되지 않는 구문을 한번에 확인할 수 있습니다.
2단계 — 개발 환경에서 먼저 적용해봅니다.
package.json의 dev 스크립트를 ts-node나 tsx에서 node --watch src/index.ts로 바꿔봅니다. 별도 빌드 없이 파일 변경 감지가 되는 것을 확인할 수 있습니다.
3단계 — CI 파이프라인에 tsc --noEmit을 명시적으로 추가합니다.
네이티브 실행으로 전환한 뒤에도 타입 안전성은 CI가 지켜줘야 합니다. node --test와 함께 파이프라인을 구성해두면 빌드 없는 TypeScript 워크플로우가 완성됩니다.
참고 자료
- Running TypeScript Natively — Node.js 공식 가이드
- Modules: TypeScript — Node.js v26.5.0 Documentation
- Node.js Moves Toward Stable TypeScript Support With Amaro 1.0 — InfoQ
- Node.js 24 Native TypeScript: Running .ts Files in Production Without a Build Step — jsmanifest
- Node.js Native TypeScript in 2026: Type Stripping Guide — HireNodeJS
- Running TypeScript in Node.js: tsx vs. ts-node vs. native — LogRocket Blog
- TSX vs Native Node.js TypeScript — Better Stack
- A look at native TypeScript performance — sebastian-staffa.eu
- Node.js v23 Natively Supports TypeScript — Medium
- Node.js 26 Quietly Killed Your TypeScript Enums — jsgurujobs
- GitHub — nodejs/amaro: Node.js TypeScript wrapper
- Node.js stabilizes built-in TypeScript execution — Progosling News
- AWS Lambda: Node.js 24 runtime now available
- TypeScript Without tsc in 2026: Type-Stripping in Node.js 24, Bun, and Deno Compared — jsmanifest