목차
- 기존 TypeScript 실행 방식과 빌드 파이프라인의 문제
- Node.js 24 네이티브 TypeScript 실행 방법: Type Stripping의 동작 원리
- 권장 tsconfig.json 설정과 각 옵션별 근거
- Non-Erasable 구문 제한: enum, namespace, 데코레이터 대응
- Import 경로 규칙과 node_modules 제약사항
- Node.js 22에서 24로 마이그레이션 체크리스트
- 프로덕션 환경 적용 시 한계점과 현실적 판단
- 다음 단계: Type Stripping 이후의 TypeScript 생태계
결론부터 말하면, Node.js 24에서는 별도 빌드 단계 없이 node example.ts 한 줄로 TypeScript 파일을 직접 실행할 수 있다. Node.js 24 네이티브 TypeScript 실행 방법의 핵심은 type stripping이라는 메커니즘이다. v24.12.0에서 Stable로 승격된 이 기능은 TypeScript 구문을 공백으로 치환하여 JavaScript 런타임이 해석 가능한 코드로 변환하는 방식이다. 타입 체크는 수행하지 않으며, .tsx 파일은 지원하지 않는다. 다만 enum, namespace 같은 non-erasable 구문을 사용하면 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX 에러가 발생하므로, 기존 프로젝트 마이그레이션 시 주의가 필요하다.
이 글에서는 type stripping의 동작 원리부터 tsconfig.json 권장 설정의 각 옵션별 의미, non-erasable 구문 제한사항, import 경로 규칙, 그리고 실전 마이그레이션 체크리스트까지 공식 문서 기반으로 분석한다.
기존 TypeScript 실행 방식과 빌드 파이프라인의 문제
Node.js 24 이전까지 TypeScript를 실행하려면 반드시 중간 빌드 단계가 필요했다. 대표적인 방식은 세 가지로 나뉜다.
첫째, tsc로 트랜스파일 후 생성된 .js 파일을 node로 실행하는 전통적 방식이다. tsconfig.json 설정에 따라 outDir에 JavaScript 파일이 생성되고, 소스맵을 별도 관리해야 하며, 개발 중 파일 변경마다 재빌드가 필요하다. 풀스택 프로젝트에서 프론트엔드와 백엔드의 tsconfig를 각각 관리하는 것은 상당한 설정 오버헤드를 수반하는 셈이다.
둘째, ts-node를 사용하는 방식이다. 런타임에서 TypeScript를 해석하므로 빌드 단계는 줄어들지만, 콜드 스타트 시간이 길어진다. ESM과 CommonJS 혼용 환경에서 --loader 플래그 설정이 복잡해지는 경우가 있다.
셋째, tsx(TypeScript Execute) 같은 서드파티 러너를 사용하는 방식이다. esbuild 기반으로 빠르지만, 프로젝트 의존성에 추가 패키지를 넣어야 한다는 점은 동일하다.
| 방식 | 빌드 단계 | 콜드 스타트 | 추가 의존성 | ESM 지원 |
|---|---|---|---|---|
| tsc + node | 필요 | 빠름 | typescript | 설정 필요 |
| ts-node | 불필요 | 느림 | ts-node, typescript | 복잡 |
| tsx | 불필요 | 빠름 | tsx | 자동 |
| Node.js 24 native | 불필요 | 빠름 | 없음 | 기본 지원 |
이 세 가지 방식 모두 공통적으로 Node.js 외부 도구에 의존한다는 구조적 한계가 존재한다. CI/CD 파이프라인에서 빌드 스텝이 추가되고, Docker 이미지에 devDependencies를 포함시켜야 하며, 새 팀원이 프로젝트를 클론한 뒤 실행까지 도달하는 데 불필요한 설정 단계를 거치게 되기도 한다.
Node.js 24 네이티브 TypeScript 실행 방법: Type Stripping의 동작 원리

Node.js의 type stripping은 TypeScript 소스에서 타입 관련 구문만 제거하고 나머지 JavaScript 코드를 그대로 실행하는 접근법이다. 내부적으로는 Amaro 로더와 SWC 트랜스폼 엔진 기반으로 동작한다. Amaro는 SWC의 경량 래퍼로, Rust나 Go를 Node.js 빌드 도구 체인에 추가하지 않기 위해 WebAssembly 바이너리와 JavaScript 바인딩만으로 구성되었다.
이 설계 결정의 배경을 이해하려면 원본 PR #53725을 살펴볼 필요가 있다. Marco Ippolito가 작성하여 2024년 7월 24일 머지된 이 PR은 @swc/wasm-typescript를 사용하며, Deno와 동일한 SWC 기반 접근법을 채택했다. WebAssembly를 선택한 이유는 Node.js 빌드 시스템에 네이티브 컴파일러 의존성을 추가하지 않기 위해서다.
TypeScript 구문을 공백으로 치환하여 원본 소스의 줄 번호를 유지한다. 별도 소스맵 없이도 에러 스택 트레이스의 줄 번호가 원본 `.ts` 파일과 일치하는 것이 핵심이다.
# 타입 체크는 별도 실행
npx tsc --noEmit
# 직접 실행
node example.ts
이 분리 구조는 의도적인 설계다. 런타임 실행과 정적 분석을 분리함으로써, 개발 중에는 빠른 피드백 루프를 유지하고 CI 단계에서 타입 체크를 수행하는 워크플로우를 지원하기 위한 것이다.
v23.6.0부터 기본 활성화되어 별도 플래그 없이 동작하며, --no-strip-types 플래그로 비활성화할 수 있다. v24.12.0에서 Stable로 승격되었으므로 프로덕션 사용이 공식적으로 지원되는 상태다.
권장 tsconfig.json 설정과 각 옵션별 근거
Node.js 24의 type stripping과 함께 사용할 권장 tsconfig.json 설정은 다음과 같다.
{
"compilerOptions": {
"noEmit": true,
"target": "esnext",
"module": "nodenext",
"rewriteRelativeImportExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true
}
}
각 옵션이 왜 필요한지 구체적으로 분석한다.
noEmit: true
tsc를 타입 체크 전용으로 사용하겠다는 선언이다. Node.js가 type stripping을 직접 수행하므로 tsc가 JavaScript 파일을 생성할 필요가 없다. outDir 설정도 불필요해지며, 빌드 산출물 관리 부담이 사라지는 셈이다.
target: esnext
구문 다운레벨링을 하지 않겠다는 의미다. Node.js의 type stripping은 구문 다운레벨링을 지원하지 않으므로, target을 es2020 같은 하위 버전으로 설정하면 tsc 체크 시와 실제 런타임 동작 사이에 불일치가 발생할 수 있다. esnext로 설정하면 TypeScript 컴파일러가 최신 구문을 그대로 통과시키므로 Node.js 런타임과 동일한 코드를 검증하게 된다.
module: nodenext
Node.js의 ESM/CommonJS 모듈 해석 규칙을 TypeScript가 동일하게 따르도록 지정하는 옵션이다. package.json의 type 필드에 따라 .ts 파일의 모듈 시스템이 결정된다. "type": "module"이면 ESM, 없으면 CommonJS로 해석하는 방식이다.
erasableSyntaxOnly: true
이 옵션은 TypeScript 컴파일러가 erasable하지 않은 구문(enum, namespace 등)을 사용했을 때 컴파일 에러를 발생시키도록 강제한다. Node.js의 type stripping이 처리할 수 없는 구문을 사전에 차단하는 안전장치 역할이다.
이 옵션 없이 개발하면 `tsc –noEmit`은 통과하지만 `node example.ts` 실행 시 `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`가 발생하는 상황이 생긴다. 개발-실행 간 불일치를 방지하려면 반드시 활성화해야 한다.
TypeScript 5.7에서 도입된 이 옵션은 상대 import 경로의 .ts 확장자를 .js로 자동 변환한다. Node.js의 type stripping에서는 import 경로에 .ts 확장자를 명시해야 하므로, 이 옵션을 활성화하면 에디터 자동완성과 실제 런타임 해석의 경로 표기가 일치한다.
verbatimModuleSyntax: true
import type 구문을 강제하여, 타입 전용 import가 런타임 코드에 남지 않도록 보장하는 옵션이다. type stripping은 import type { Foo } 형태의 구문을 안전하게 제거할 수 있지만, 일반 import { Foo }에서 Foo가 타입인지 값인지 판단하기 어렵다. 이 옵션을 활성화하면 타입과 값의 import를 명시적으로 구분하게 되는 것이다.
tsconfig.json 없이도 `node example.ts`는 동작한다. 다만 타입 체크 정확도와 에디터 지원을 위해 위 설정을 권장한다. 특히 팀 프로젝트에서는 `erasableSyntaxOnly`가 없으면 팀원 간 호환성 문제가 발생할 때가 있다.
Node.js 24 네이티브 TypeScript 실행 방법에서 가장 주의해야 할 부분이 non-erasable 구문 제한이다. enum 선언, namespace(런타임 코드 포함), 생성자 파라미터 프로퍼티, import alias 등 erasable하지 않은 TypeScript 구문은 기본 type stripping으로 실행 시 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX 에러가 발생한다.
// enum 사용 시
node --experimental-transform-types app.ts
// 에러 발생 예시 (플래그 없이)
namespace A {
export let x = 1; // ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
}
erasable 구문과 non-erasable 구문의 구분
type stripping이 처리할 수 있는 erasable 구문은 제거해도 JavaScript의 런타임 동작에 영향을 주지 않는 것들이다. 타입 어노테이션, 인터페이스 선언, type 키워드로 선언한 타입, 제네릭 파라미터 등이 여기에 해당한다.
반면 non-erasable 구문은 제거하면 런타임 동작이 변하는 것들이다. enum은 JavaScript 객체로 변환되어야 하고, namespace는 IIFE(즉시 실행 함수)로 변환되어야 하는데, 단순한 공백 치환으로는 이 변환이 불가능하기 때문이다.
| 구문 | erasable 여부 | type stripping 지원 | 대안 |
|---|---|---|---|
| type, interface | ✅ erasable | ✅ 지원 | – |
| 타입 어노테이션 | ✅ erasable | ✅ 지원 | – |
| enum | ❌ non-erasable | ❌ 기본 미지원 | const object + as const |
| namespace (런타임 코드) | ❌ non-erasable | ❌ 기본 미지원 | ES 모듈 |
| 생성자 파라미터 프로퍼티 | ❌ non-erasable | ❌ 기본 미지원 | 명시적 필드 선언 |
| 데코레이터 (TC39 Stage 3) | ❌ | ❌ 아예 미지원 | 미지원 |
–experimental-transform-types 플래그
enum이나 namespace를 반드시 사용해야 하는 경우, --experimental-transform-types 플래그로 실행할 수 있다. 이 플래그는 단순 type stripping 대신 SWC 기반 변환을 수행하여 non-erasable 구문을 JavaScript로 트랜스폼하는 방식이다.
다만 이 플래그는 이름에서 알 수 있듯 아직 experimental 상태다. 공식 문서에도 --experimental-transform-types 사용 시 소스맵 지원 여부에 대한 명확한 문서가 부족한 상태이므로, 프로덕션 환경에서는 신중하게 판단해야 한다.
데코레이터(TC39 Stage 3)는 --experimental-transform-types로도 지원하지 않는다. NestJS처럼 데코레이터에 의존하는 프레임워크는 현 시점에서 Node.js 네이티브 TypeScript 실행으로 전환이 불가능한 것이다.
실전 마이그레이션: enum 대체 패턴
기존 프로젝트에서 enum을 많이 사용하고 있다면, as const 객체 패턴으로 전환하는 것이 가장 현실적인 대안이다.
// ❌ 기존: enum (type stripping에서 에러)
// enum Status {
// Active = 'active',
// Inactive = 'inactive',
// }
// ✅ 대체: as const 객체
const Status = {
Active: 'active',
Inactive: 'inactive',
} as const;
type Status = typeof Status[keyof typeof Status];
// Status 타입은 'active' | 'inactive'
이 패턴은 런타임에서 일반 JavaScript 객체로 동작하므로 type stripping과 완전히 호환된다. 타입 레벨에서도 enum과 동일한 수준의 타입 안전성을 제공하기도 한다.
Import 경로 규칙과 node_modules 제약사항
Node.js 24의 type stripping에서 import 경로는 엄격한 규칙을 따른다. 확장자 없는 import는 동작하지 않으며, .ts 확장자를 명시해야 한다.
import './file.ts'; // ✅ 동작
import './file'; // ❌ 동작하지 않음
require('./file.ts'); // ✅ 동작
이 제약의 근본 원인은 Node.js의 모듈 해석 알고리즘에 있다. 기존 tsc나 ts-node는 .ts → .js 확장자 변환을 자동으로 수행하지만, type stripping은 모듈 해석 로직을 변경하지 않는다. Node.js의 기본 모듈 해석기가 .ts 확장자를 인식하되, 확장자 없는 경로에서 자동으로 .ts를 탐색하지는 않는 구조인 것이다.
node_modules 내부 TypeScript 미지원
node_modules 내부의 TypeScript 파일에는 type stripping이 적용되지 않는다. 이 결정은 의도적인 설계로, npm 패키지가 배포 시 JavaScript로 컴파일되어야 한다는 기존 생태계 규칙을 유지하기 위한 것이다.
이 제약은 모노레포 환경에서 특히 주의가 필요한 부분이다. 워크스페이스 패키지가 node_modules에 심링크되는 경우, 해당 패키지의 .ts 파일이 type stripping 없이 로드되어 구문 에러가 발생할 수 있다. 모노레포에서는 워크스페이스 패키지를 상대 경로로 import하거나, 각 패키지를 사전 빌드하는 방식이 필요하게 된다.
tsconfig paths 미지원
tsconfig.json의 paths 설정이나 구문 다운레벨링 등 tsconfig 의존 기능은 지원하지 않는다. @/components/Button 같은 경로 별칭을 사용하는 프로젝트는 Node.js 네이티브 실행으로 전환 시 별도 조치가 필요하다.
tsconfig의 `paths` 설정에 의존하는 프로젝트는 Node.js의 `–experimental-import-meta-resolve`나 `package.json`의 `imports` 필드(`#` prefix subpath imports)로 대체해야 한다. type stripping은 tsconfig를 읽지 않으므로 paths 매핑이 무시된다.
기존 프로젝트를 Node.js 24 네이티브 TypeScript 실행 방법으로 전환할 때 확인해야 할 항목들을 정리한다.
사전 점검 단계
첫째, 프로젝트에서 non-erasable 구문 사용 여부를 확인한다. enum, namespace, 생성자 파라미터 프로퍼티, import = 구문을 검색하여 해당 패턴의 규모를 파악하는 것이 우선이다. 소규모라면 as const 패턴으로 전환하고, 대규모라면 --experimental-transform-types 플래그 사용을 검토한다.
둘째, import 경로에서 확장자 누락 여부를 확인한다. ESLint의 import/extensions 규칙을 활성화하면 확장자 없는 import를 자동으로 감지할 수 있다.
셋째, tsconfig.json의 paths 설정 의존도를 확인한다. 경로 별칭을 사용하고 있다면 package.json의 imports 필드로 마이그레이션 계획을 세워야 하는 것이다.
tsconfig.json 업데이트
기존 tsconfig.json에서 권장 설정으로 전환할 때 변경이 필요한 옵션들을 정리하면 다음과 같다.
변경 전 변경 후
─────────────────────────────────────────────────
"target": "es2020" → "target": "esnext"
"module": "commonjs" → "module": "nodenext"
(없음) → "erasableSyntaxOnly": true
(없음) → "verbatimModuleSyntax": true
(없음) → "rewriteRelativeImportExtensions": true
"outDir": "./dist" → 삭제 (noEmit: true와 함께)
빌드 파이프라인 정리
Node.js 24 네이티브 실행으로 전환하면 제거할 수 있는 것들이 있다.
ts-node, tsx, @swc/cli 같은 런타임 트랜스파일러를 devDependencies에서 제거할 수 있다. package.json의 scripts에서 tsc && node dist/index.js 패턴을 node src/index.ts로 단순화할 수 있는 것이다. Docker 이미지에서 빌드 스텝을 제거하면 이미지 빌드 시간이 줄어들기도 한다.
다만 tsc --noEmit은 CI 파이프라인에서 유지해야 한다. type stripping은 타입 체크를 수행하지 않으므로, 타입 안전성 검증은 여전히 TypeScript 컴파일러의 역할이다.
# CI 파이프라인 예시
# 1단계: 타입 체크
npx tsc --noEmit
# 2단계: 테스트 실행 (직접 .ts 파일 실행)
node --test src/**/*.test.ts
# 3단계: 프로덕션 실행
node src/index.ts
데이터베이스 관련 코드 마이그레이션 시 고려사항
풀스택 프로젝트에서 데이터베이스 레이어의 TypeScript 코드는 특별한 주의가 필요하다. ORM에서 enum 타입을 데이터베이스 컬럼 매핑에 사용하는 패턴이 일반적이기 때문이다.
// ❌ Prisma에서 enum 사용 (type stripping 미호환)
// enum OrderStatus { Pending, Shipped, Delivered }
// ✅ 대체 패턴: string literal union
const OrderStatus = {
Pending: 'PENDING',
Shipped: 'SHIPPED',
Delivered: 'DELIVERED',
} as const;
type OrderStatus = typeof OrderStatus[keyof typeof OrderStatus];
// SQL 쿼리에서 직접 사용
const query = `
SELECT id, status, total
FROM orders
WHERE status = '${OrderStatus.Pending}'
ORDER BY created_at DESC
`;
TypeORM, Prisma, Drizzle 등 주요 ORM은 enum 대신 string literal 타입을 스키마 정의에 사용할 수 있다. 각 ORM의 마이그레이션 가이드를 참조하여 데이터베이스 스키마와 TypeScript 타입 간 일관성을 유지해야 한다.
Node.js 24의 네이티브 TypeScript 실행은 강력하지만, 프로덕션 적용 전 인지해야 할 한계점이 존재한다.
성능 오버헤드 측정의 어려움
프로덕션 환경에서의 성능 벤치마크, 즉 type stripping 오버헤드에 대한 공식 데이터는 공식 문서에도 명시되어 있지 않다. SWC WebAssembly 기반 변환이므로 네이티브 바이너리 대비 오버헤드가 존재할 수 있으나, 정확한 수치는 워크로드에 따라 다를 것이다. 콜드 스타트가 중요한 서버리스 환경에서는 이 오버헤드가 유의미할 가능성이 있는 반면, 장기 실행 서버에서는 시작 시점 1회 비용이므로 영향이 미미한 편이다.
생태계 호환성
데코레이터(TC39 Stage 3)를 아예 지원하지 않는다는 점은 NestJS, TypeORM(데코레이터 기반 엔티티 정의), Angular 같은 프레임워크 사용자에게 진입 장벽이 된다. 이런 프레임워크를 사용하는 프로젝트는 당분간 기존 빌드 파이프라인을 유지해야 하는 것이 현실이다.
.tsx 파일 미지원은 React 서버 컴포넌트를 Node.js에서 직접 실행하려는 시도에 제약을 가하기도 한다. 프론트엔드 빌드 도구(Vite, Webpack 등)는 여전히 필요하므로, Node.js 네이티브 TypeScript 실행의 주요 수혜 대상은 백엔드·CLI·스크립트 영역이 되는 셈이다.
적합한 사용 시나리오
현 시점에서 Node.js 24 네이티브 TypeScript가 가장 적합한 영역은 명확하다.
✅ 적합 ❌ 부적합
────────────────────────────────────────────────
CLI 도구, 스크립트 NestJS, Angular 프로젝트
Express/Fastify 백엔드 데코레이터 기반 ORM 엔티티
Node.js 내장 테스트 러너 .tsx 파일이 포함된 SSR
모노레포의 공유 유틸리티 복잡한 paths 별칭 의존 프로젝트
프로토타이핑, PoC namespace를 광범위하게 사용하는 레거시
다음 단계: Type Stripping 이후의 TypeScript 생태계
Node.js 24의 네이티브 TypeScript 실행 방법이 Stable로 자리잡으면서, TypeScript 생태계의 다음 변화도 주목할 만하다.
Amaro 로더의 발전 방향이 첫 번째 관심사다. 현재 WebAssembly 기반으로 동작하는 SWC 트랜스폼이 네이티브 바이너리로 전환될 가능성이 있으며, 이 경우 콜드 스타트 오버헤드가 크게 줄어들 수 있다. --experimental-transform-types가 Stable로 승격되면 enum과 namespace 지원이 정식화되어 마이그레이션 장벽이 낮아질 것이다.
tsconfig.json의 erasableSyntaxOnly 옵션에 대한 TypeScript 공식 문서의 상세 설명은 아직 충분하지 않은 상태다. TypeScript 팀이 Node.js의 type stripping과 더 긴밀하게 협력하면서 이 부분이 보완될 가능성이 높다.
데이터베이스 도구 체인 관점에서는 Drizzle ORM처럼 데코레이터 없이 스키마를 정의하는 방식이 Node.js 네이티브 TypeScript와 자연스럽게 결합된다. Prisma도 스키마 파일 기반이므로 TypeScript 코드에서는 enum 대신 string union 패턴을 적용하면 호환성 문제가 없는 편이다.
Node.js 24 LTS TypeScript 지원이 안정화되면, 그다음으로 살펴볼 주제는 Node.js 내장 테스트 러너와 TypeScript의 조합, 그리고 ESM 전용 패키지 생태계에서의 type stripping 활용 전략이다. Node.js 24 네이티브 TypeScript 실행 방법은 빌드리스 개발의 시작점이며, 이를 기반으로 한 워크플로우 최적화가 본격화될 것이다.
관련 글
- TypeScript 7 베타 벤치마크 실측 5가지 — CI 환경 성능 병목 완전 분석 – TypeScript 7의 Go 네이티브 컴파일러가 10배 빠르다고 알려졌지만, GitHub Actions CI 환경에서는 28%에 그치는 …