Prologue: 당연하지만 당연하지 않았던 TS 실행
처음 웹 개발을 배울 때 가장 충격적이었던 도구 중 하나는 타입스크립트(TypeScript)였습니다. 사학을 전공하면서 모호한 역사적 텍스트를 고증하던 습관이 남아있어서 그런지, 자바스크립트의 말랑말랑하고 불안정한 타입 체계에 타입스크립트라는 명확한 역사적 '교차 검증' 도구가 더해졌을 때의 안도감은 이루 말할 수 없었습니다.
하지만 실제로 프로젝트를 구성하면서 엄청난 피로감을 느꼈습니다. 바로 **"왜 타입스크립트 파일(.ts)을 노드(Node.js)에서 바로 실행할 수 없는가?"**라는 근본적인 의문 때문이었습니다.
타입스크립트를 실행하기 위해 제가 거쳐야 했던 빌드 파이프라인의 삽질 변천사는 대략 이렇습니다.
tsc로 번역해서dist/폴더에 자바스크립트 파일을 굽고,node dist/index.js로 실행하기 (매번 빌드를 새로 해야 해서 너무 귀찮음)ts-node설치하기 (하지만 ES Modules와 CommonJS 설정이 충돌하여tsconfig.json과package.json설정을 며칠 동안 뜯어고쳐야 했음)- 최근에는 속도가 빠른
tsx패키지를 설치해 개발 환경을 돌림
"그냥 파이썬처럼 파일 하나 치면 바로 작동할 수는 없을까?" 하고 생각했는데, Node.js 22.6.0 버전부터 도입되기 시작한 --experimental-strip-types 옵션을 접하고는 드디어 그 갈증이 해소되는 느낌을 받았습니다.
Concept: 타입 스트리핑(Type Stripping)이란 무엇인가
노드가 타입스크립트를 직접 실행한다고 했을 때, 처음에는 "노드 엔진 안에 타입스크립트 컴파일러(tsc)가 내장되었나?" 하고 생각했습니다. 하지만 실제 원리는 훨씬 가볍고 명확했습니다. 바로 '타입 스트리핑(Type Stripping)' 방식입니다.
타입 스트리핑은 **"타입스크립트 파일에서 타입에 해당하는 코드들만 가위로 싹둑 잘라내고(Strip), 남은 순수 자바스크립트 코드를 실행하는 것"**입니다.
여기서 핵심은 타입 검사(Type Checking)를 하지 않는다는 점입니다.
// 원래 타입스크립트 코드
function greet(name: string): string {
return `Hello, ${name}!`;
}
// 타입 스트리핑을 거친 결과 (자바스크립트)
function greet(name) {
return `Hello, ${name}!`;
}
타입 컴파일러는 코드 분석과 변환, 그리고 수많은 타입 체크를 수행하느라 아주 무겁습니다. 하지만 노드는 이 무거운 연산들을 다 건너뛰고 오직 구문 레벨에서 타입 정의 부분만 지워버립니다.
이 개념을 처음 이해했을 때 무릎을 쳤습니다. "맞아, 브라우저나 런타임 입장에서는 타입이 맞는지 틀린지 검사할 필요가 없지. 그건 개발할 때 에디터(VS Code)나 빌드 파이프라인에서 검사하면 되니까!"
즉, 런타임의 속도를 깎아먹지 않으면서도 타입스크립트 파일을 다이렉트로 읽어 들여 실행할 수 있게 된 것입니다.
Deep Dive: --experimental-strip-types 작동 원리와 제약 사항
노드의 네이티브 TS 실행은 내부적으로 Rust로 작성된 파서 라이브러리인 amaro (SWC의 경량 버전)를 사용하여 타입을 지워버립니다. 자바스크립트 파일로 변환된 후에는 노드의 내부 V8 엔진이 평소처럼 코드를 해석하여 실행합니다.
하지만 빌드 도구(Webpack, Esbuild)나 tsc를 거치지 않고 오직 텍스트만 지워서 실행하는 구조이기 때문에, 몇 가지 명확한 제약 사항이 존재합니다.
1. 런타임에 영향을 주는 TS 고유 스펙 사용 불가
타입스크립트 중에는 순수하게 타입으로만 끝나지 않고, 자바스크립트 실행 코드(객체 등)를 생성해내는 독자적인 문법들이 있습니다. 네이티브 노드 실행 시 이 코드들은 단순 텍스트 삭제만으로 처리할 수 없어서 에러가 발생합니다.
- 일반 Enum: Enum은 컴파일되면 실제 JS 객체로 변환되어야 하므로 에러가 납니다. 대신
const enum을 쓰거나 자바스크립트의 일반 객체(as const)를 써야 합니다. - 클래스의 매개변수 프로퍼티 (Parameter Properties): 클래스 생성자 안에서
constructor(public name: string)과 같이 단축형으로 작성하면 실제 클래스 멤버 변수를 자동 할당해 주는 문법인데, 이 역시 동작하지 않습니다. 수동으로 속성을 선언하고 할당해 줘야 합니다. - 네임스페이스 (Namespace): 타입스크립트 고유의 모듈화 문법인 네임스페이스 역시 런타임 객체를 만들기 때문에 사용이 금지됩니다.
2. 타입 검사의 부재 (No Type Checking)
앞서 설명했듯이 노드는 단순히 타입 텍스트를 제거할 뿐입니다. 따라서 코드에 타입 오류가 있어도 프로그램이 그냥 실행됩니다.
// 이 코드는 노드에서 오류 없이 실행됨 (하지만 런타임 에러가 나거나 비정상 작동할 것)
let message: string = "Hello";
message = 123; // 타입 에러가 나야 하지만, 노드는 스트리핑 후 그냥 실행함
따라서 실제 개발 환경이나 CI/CD 환경에서는 반드시 tsc --noEmit 명령어를 병행하여 타입 검사를 수행해 주어야 합니다.
Application: 로딩 속도 비교와 도커(Docker) 빌드 리팩토링
이 기능을 내 토이 프로젝트 서버에 적용해 보며 성능과 설정 변화를 체감해 보았습니다.
기존에 개발 서버를 띄울 때 사용했던 tsx 개발 런타임과 노드의 네이티브 스트리핑 옵션의 첫 구동(Cold Start) 속도를 비교했습니다.
# 1. tsx 패키지로 실행 시
$ time npx tsx src/server.ts
Server running on port 3000...
real 0m0.485s
# 2. Node.js 네이티브 실행 시
$ time node --experimental-strip-types src/server.ts
Server running on port 3000...
real 0m0.180s
tsx도 내부적으로 esbuild를 써서 매우 빠르지만, 노드가 기본 탑재된 C++ 및 Rust 엔진으로 다이렉트 스트리핑을 수행하니 구동 속도가 약 2.5배 이상 단축되었습니다. 개발 중 코드가 변경되어 서버를 재시작하는 속도가 눈에 띄게 쾌적해졌습니다.
더 유용했던 점은 도커 파일(Dockerfile)의 단순화였습니다. 이전에는 도커 이미지 안에서 타입스크립트 빌드를 돌려 JS로 빌드된 파일만 이미지에 담거나, 이미지 내에 ts-node 등 개발 의존성을 추가로 설치해야 했습니다.
# 기존 도커파일 빌드 전략 (멀티스테이지 필수)
FROM node:22 AS builder
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build # tsc 빌드로 dist 생성
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
RUN npm install --only=production
CMD ["node", "dist/server.js"]
새로운 기능을 활용하면, 굳이 중간 컴파일 과정을 거치지 않고도 단순하고 빠르게 이미지를 작성할 수 있습니다.
# 리팩토링 후 도커파일
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install --only=production
# Node.js 22 네이티브 실행
CMD ["node", "--experimental-strip-types", "src/server.ts"]
도커 빌드 시간이 획기적으로 줄었고, dist/ 폴더 관리나 소스맵(Source Map) 동기화 오류로 인한 프로덕션 디버깅의 번거로움도 크게 줄어들었습니다.
Summary: 도구가 런타임에 내장될 때의 해방감
사학을 배울 때 기술의 발전 역사를 되짚어보면 항상 동일한 패턴이 있었습니다. 처음에는 여러 파편화된 외부 도구들이 난립하다가, 기술이 성숙하면 핵심적인 기능들이 중심 플랫폼(런타임) 안으로 내장되는 과정입니다.
자바스크립트 생태계의 복잡한 번들러와 트랜스파일러 환경도 마찬가지인 것 같습니다. 타입스크립트가 업계 표준이 된 이상, 런타임이 이를 직접 받아들이는 것은 시간 문제였습니다. Deno와 Bun이 처음부터 이 해방감을 제공하며 치고 나갔고, 결국 업계 공룡인 Node.js도 이를 수용했습니다.
물론 아직 실험적(Experimental) 딱지가 붙어 있고 제약 사항도 존재하지만, 복잡한 컴파일러 설정과 CJS/ESM 빌드 모듈 충돌에서 벗어나 **"그냥 노드로 바로 실행한다"**라는 본질적인 심플함을 다시 맛본 것만으로도 앞으로의 백엔드 개발 환경이 한층 더 쾌적해질 것이라는 확신이 듭니다.