TypeScript를 쓰는 Node.js 프로젝트는 보통 tsc, tsx, ts-node, 번들러 중 하나를 거쳐 실행합니다. 이 방식은 큰 서비스에서는 여전히 자연스럽지만, 작은 CLI 스크립트나 내부 자동화 도구에서는 빌드 단계 자체가 부담이 될 때가 있습니다. 파일 하나를 고쳤을 뿐인데 컴파일 설정, 출력 디렉터리, sourcemap, 실행 명령까지 챙겨야 하기 때문입니다.

최근 Node.js는 TypeScript 타입 스트리핑(type stripping) 흐름을 제공하면서 이런 작은 실행 경로를 단순하게 만들고 있습니다. 핵심은 .ts 파일에서 타입 문법을 제거한 뒤 JavaScript처럼 실행하는 것입니다. 타입 검사를 대신해 주는 기능은 아니지만, 타입만 붙은 간단한 스크립트를 빠르게 실행하는 데는 꽤 유용합니다. 이 글에서는 Node.js TypeScript 타입 스트리핑의 역할, 지원 범위, tsconfig 작성법, watch 모드와 test runner에 연결하는 패턴, 도입 전 체크리스트를 정리합니다. 개발 중 자동 재시작이 필요하다면 Node.js watch 모드 가이드: 파일 변경 시 개발 서버를 자동 재시작하는 법도 함께 보면 좋습니다.

Node.js TypeScript 타입 스트리핑이 해결하는 문제

H3. 작은 스크립트의 실행 단계를 줄인다

개발 프로젝트에는 서비스 코드만 있는 것이 아닙니다. 마이그레이션 보조 스크립트, 데이터 정리 도구, 릴리스 노트 생성기, 로컬 점검 CLI처럼 “짧지만 타입을 붙이고 싶은” 파일이 많습니다. 이런 파일을 실행하기 위해 매번 별도 런타임을 설치하면 저장소가 불필요하게 무거워질 수 있습니다.

예를 들어 다음처럼 간단한 TypeScript 스크립트가 있다고 가정해 보겠습니다.

// scripts/print-release-summary.ts
type Release = {
  version: string;
  changed: string[];
};

const release: Release = {
  version: '1.4.0',
  changed: ['watch mode 개선', '문서 링크 정리']
};

console.log(`${release.version}: ${release.changed.join(', ')}`);

타입 스트리핑이 가능한 Node.js 버전에서는 이런 파일을 별도 JavaScript 출력물 없이 바로 실행하는 흐름을 만들 수 있습니다.

node scripts/print-release-summary.ts

물론 이 명령이 “타입 검사를 했다”는 뜻은 아닙니다. Node.js는 실행을 위해 타입 주석을 제거할 뿐, Release 타입과 실제 값이 맞는지 깊게 검사하지 않습니다. 따라서 타입 안정성은 tsc --noEmit이나 에디터/CI의 타입 체크에 맡기고, Node.js 실행 경로는 빠른 런타임 용도로 보는 것이 좋습니다.

H3. 빌드 산출물 없는 도구를 만들 수 있다

작은 내부 도구에서 가장 귀찮은 부분은 산출물 관리입니다. src에서 dist로 컴파일하고, 실행 명령은 dist를 바라보게 하고, 변경할 때마다 다시 빌드해야 합니다. 이 구조가 필요한 프로젝트도 많지만, 단일 스크립트에는 과할 수 있습니다.

타입 스트리핑을 사용하면 다음과 같은 도구를 더 단순하게 관리할 수 있습니다.

  • 로컬 개발용 점검 스크립트
  • JSON, CSV, Markdown 생성기
  • 배포 전 체크리스트 자동화
  • 작은 CLI 프로토타입
  • 테스트 fixture 생성기

예를 들어 package.json에 이렇게 넣을 수 있습니다.

{
  "scripts": {
    "release:summary": "node scripts/print-release-summary.ts",
    "typecheck": "tsc --noEmit"
  }
}

실행은 빠르게 하고, 타입 검사는 별도 명령으로 명확히 분리합니다. 이 분리는 블로그 예제나 팀 온보딩 문서에서도 설명하기 쉽습니다. 환경 변수 로딩까지 단순화하고 싶다면 Node.js loadEnvFile 가이드: dotenv 없이 환경변수를 단순하게 관리하는 법을 같이 참고할 수 있습니다.

지원 범위: 타입만 제거할 수 있는 코드로 제한하기

H3. 값으로 바뀌는 TypeScript 문법은 피한다

타입 스트리핑은 이름 그대로 타입 정보를 지우는 방식입니다. 그래서 JavaScript로 자연스럽게 남을 수 있는 문법과, 변환이 필요한 TypeScript 문법을 구분해야 합니다.

대체로 다음 문법은 타입 제거만으로 실행 흐름을 만들기 쉽습니다.

type User = {
  id: string;
  name: string;
};

interface Logger {
  info(message: string): void;
}

function greet(user: User): string {
  return `hello ${user.name}`;
}

반면 다음처럼 런타임 값으로 변환되어야 하는 문법은 주의가 필요합니다.

// 변환이 필요한 대표 사례
const enum Status {
  Ready = 'ready',
  Done = 'done'
}

namespace LegacyHelpers {
  export const ok = true;
}

이런 문법은 단순히 타입을 지우는 것만으로는 JavaScript 실행 코드가 되지 않습니다. 프로젝트에서 반드시 필요하다면 기존 TypeScript 컴파일러나 번들러를 계속 사용하는 편이 안전합니다. 작은 스크립트에서는 enum 대신 객체 리터럴과 유니언 타입을 쓰는 방식이 더 잘 맞습니다.

const Status = {
  Ready: 'ready',
  Done: 'done'
} as const;

type Status = typeof Status[keyof typeof Status];

이렇게 작성하면 런타임 값은 일반 JavaScript 객체로 남고, 타입은 TypeScript가 개발 중에만 활용합니다. 동적 값 검증까지 필요하다면 타입만 믿지 말고 런타임 검사 코드를 별도로 두어야 합니다. 사용자 입력 URL을 다룬다면 Node.js URL.canParse 가이드: 사용자 입력 URL 검증을 예외 없이 안전하게 처리하는 법처럼 실제 값 검증 API를 함께 쓰는 것이 좋습니다.

H3. import 경로와 모듈 방식을 명확히 한다

TypeScript 실행 경로에서 자주 막히는 부분은 타입보다 import입니다. Node.js는 런타임이므로 실제 파일을 찾을 수 있어야 합니다. 따라서 작은 스크립트에서는 경로를 명확하게 쓰는 편이 좋습니다.

// scripts/report.ts
import { readFile } from 'node:fs/promises';
import { parseReport } from './parse-report.ts';

const raw = await readFile(new URL('./report.json', import.meta.url), 'utf8');
console.log(parseReport(raw));

상대 경로는 실제 파일 확장자를 포함해 작성하면 실행 시 혼란이 줄어듭니다. ESM 기준으로 파일 경로를 다뤄야 한다면 import.meta.url, import.meta.dirname, import.meta.filename 같은 흐름을 이해해 두는 것이 좋습니다. 자세한 경로 패턴은 Node.js import.meta.dirname 가이드: ESM에서 __dirname 대체를 가장 안전하게 처리하는 법에 정리해 두었습니다.

또한 paths alias, 커스텀 resolver, 번들러 전용 import 규칙에 의존하는 코드는 Node.js 단독 실행과 잘 맞지 않을 수 있습니다. 이런 기능이 이미 깊게 들어간 서비스 코드는 기존 빌드 도구를 유지하고, 타입 스트리핑은 별도 scripts/ 디렉터리부터 적용하는 방식이 현실적입니다.

tsconfig 설정: 실행과 타입 검사를 분리한다

H3. noEmit 타입 체크 스크립트를 둔다

Node.js가 .ts를 실행할 수 있다고 해서 tsc가 필요 없어지는 것은 아닙니다. 오히려 역할을 더 명확히 나누는 편이 좋습니다.

{
  "scripts": {
    "dev:script": "node --watch scripts/report.ts",
    "check:types": "tsc --noEmit"
  }
}

node는 실행 담당, tsc --noEmit은 타입 검증 담당입니다. CI에서는 최소한 check:types를 실행해 타입 오류가 main 브랜치에 들어가지 않도록 막습니다. 로컬에서는 에디터의 TypeScript 서버가 대부분의 오류를 빨리 보여주고, 커밋 전이나 PR에서 tsc --noEmit이 최종 확인을 맡습니다.

간단한 tsconfig.json은 다음처럼 시작할 수 있습니다.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["scripts/**/*.ts", "src/**/*.ts", "test/**/*.ts"]
}

여기서 중요한 점은 실행 결과물을 만들지 않는다는 것입니다. 빌드 산출물이 필요한 라이브러리 배포, 프론트엔드 번들링, 구버전 Node.js 지원 같은 요구사항이 있다면 별도 빌드 설정을 유지해야 합니다.

H3. Node 버전 조건을 문서화한다

타입 스트리핑은 Node.js 버전에 따라 지원 범위와 기본 동작이 달라질 수 있습니다. 팀 프로젝트라면 “어느 Node 버전 이상에서 이 명령을 지원한다”는 조건을 READMEpackage.jsonengines에 남겨야 합니다.

{
  "engines": {
    "node": ">=22.6.0"
  }
}

다만 실제로 사용할 최소 버전은 팀의 배포 환경과 로컬 개발 환경에 맞춰 정해야 합니다. 특정 버전에서 플래그가 필요한지, 경고가 출력되는지, 변환 TypeScript 문법을 허용할지 여부는 프로젝트 시작 시 한 번 직접 확인하는 편이 안전합니다. 블로그 예제처럼 재현 가능성이 중요한 글에서는 “내 환경에서만 된다”는 전제를 숨기지 않는 것이 신뢰를 높입니다.

버전 차이가 큰 팀이라면 .nvmrc, .node-version, Volta 설정 같은 방식으로 런타임을 고정하세요. 그렇지 않으면 어떤 개발자는 바로 실행되고, 다른 개발자는 알 수 없는 확장자 오류를 만나는 상황이 생깁니다.

watch와 test runner에 연결하는 실무 패턴

H3. watch 모드로 TypeScript 스크립트를 반복 실행한다

타입 스트리핑은 watch 모드와 함께 쓸 때 체감이 큽니다. 파일을 저장할 때마다 Node.js가 .ts 스크립트를 다시 실행하면 작은 자동화 도구를 빠르게 다듬을 수 있습니다.

{
  "scripts": {
    "dev:report": "node --watch --watch-preserve-output scripts/report.ts"
  }
}

이 패턴은 다음 작업에 잘 맞습니다.

  • Markdown 문서 생성 결과 확인
  • API 응답 샘플 정리
  • 로컬 데이터 변환 로직 실험
  • CLI 출력 문구 조정

단, watch는 실행 편의 기능일 뿐입니다. 스크립트가 파일을 계속 생성하고 그 결과물을 다시 감시하면 재시작 루프가 생길 수 있습니다. 입력 디렉터리와 출력 디렉터리를 분리하고, 필요한 경우 --watch-path로 감시 범위를 좁히세요. 재시작 범위 설계는 앞서 정리한 Node.js watch 모드 가이드의 원칙을 그대로 적용하면 됩니다.

H3. 내장 test runner와 함께 사용한다

Node.js 내장 test runner를 사용하는 프로젝트라면 테스트 파일도 TypeScript로 작성하고 싶을 수 있습니다. 지원되는 Node.js 버전과 문법 범위 안에서는 다음처럼 단순한 테스트 흐름을 만들 수 있습니다.

// test/sum.test.ts
import test from 'node:test';
import assert from 'node:assert/strict';
import { sum } from '../src/sum.ts';

test('sum adds numbers', () => {
  assert.equal(sum(1, 2), 3);
});
node --test test/**/*.test.ts

커버리지까지 확인하려면 다음처럼 실행할 수 있습니다.

node --test --experimental-test-coverage test/**/*.test.ts

내장 테스트 도구 자체가 낯설다면 Node.js test runner 가이드: 내장 테스트 도구로 빠르게 검증하는 법을 먼저 보고, 커버리지 흐름은 Node.js test runner coverage 가이드: 내장 커버리지로 테스트 누락 찾는 법을 이어서 보면 좋습니다.

도입 전 체크리스트

H3. 적용하기 좋은 경우

Node.js TypeScript 타입 스트리핑은 모든 프로젝트의 빌드 시스템을 대체하는 기능이 아닙니다. 대신 다음 조건에 가까울수록 효과가 좋습니다.

  • 최신 Node.js 버전을 비교적 쉽게 맞출 수 있다.
  • 실행 대상이 작은 스크립트나 내부 도구다.
  • 타입 검사는 tsc --noEmit으로 별도 수행한다.
  • enum, namespace, 복잡한 decorator 등 변환 의존 문법을 피할 수 있다.
  • 번들러 alias나 커스텀 loader에 강하게 의존하지 않는다.
  • 산출물 배포가 아니라 로컬 실행 편의가 목적이다.

이 기준에 맞는다면 scripts/ 하나부터 시작하는 것을 추천합니다. 처음부터 서비스 전체를 바꾸기보다, 실패해도 영향이 작은 자동화 스크립트에서 명령을 단순화해 보는 편이 안전합니다.

H3. 피하거나 보류해야 하는 경우

다음 상황에서는 기존 빌드 도구를 유지하는 편이 낫습니다.

  • npm 패키지로 배포할 JavaScript 산출물이 필요하다.
  • 구버전 Node.js 런타임을 지원해야 한다.
  • TypeScript 변환 문법을 많이 사용한다.
  • 번들러가 tree shaking, alias, asset import까지 맡고 있다.
  • 운영 서버 시작 경로가 안정적으로 검증된 빌드 결과물이어야 한다.

특히 운영 환경에서는 “로컬에서 실행된다”보다 “같은 산출물을 반복 배포할 수 있다”가 더 중요합니다. 서버 시작, 권한 제한, 종료 처리까지 함께 고려해야 한다면 Node.js Permission Model 가이드: 런타임 권한으로 파일·프로세스 접근을 제한하는 법처럼 실행 환경을 통제하는 주제와 함께 판단해야 합니다.

FAQ

H3. Node.js 타입 스트리핑은 TypeScript 타입 검사를 해주나요?

아니요. 타입 스트리핑은 실행을 위해 타입 문법을 제거하는 흐름에 가깝습니다. 타입 오류를 잡으려면 tsc --noEmit을 별도로 실행해야 합니다. 실무에서는 node script.tstsc --noEmit을 서로 다른 책임으로 분리하는 방식이 가장 명확합니다.

H3. tsx나 ts-node를 바로 제거해도 되나요?

바로 제거하기보다는 사용처를 나눠 보는 것이 좋습니다. 단순 스크립트는 Node.js 타입 스트리핑으로 옮기고, 복잡한 변환·alias·decorator·구버전 런타임 지원이 필요한 경로는 기존 도구를 유지하세요. 팀 저장소에서는 작은 디렉터리부터 바꾸고 CI에서 검증한 뒤 확장하는 방식이 안전합니다.

H3. 블로그 예제 코드에 사용해도 괜찮나요?

괜찮지만 Node.js 최소 버전과 타입 검사 명령을 같이 적어야 합니다. 독자가 자신의 환경에서 재현할 수 있어야 신뢰가 생깁니다. 또한 토큰, 쿠키, 실제 고객 데이터 같은 민감정보를 예제 로그에 넣지 않아야 합니다. 출력 예제를 정리할 때는 CLI 출력 정리 가이드: 블로그 예제에서 토큰과 개인정보를 안전하게 마스킹하기를 참고하세요.

마무리: 빌드 제거가 아니라 실행 경로 단순화로 이해하기

Node.js TypeScript 타입 스트리핑은 “이제 TypeScript 빌드가 필요 없다”는 신호가 아닙니다. 더 정확히는 작은 TypeScript 실행 경로를 가볍게 만들 수 있는 선택지입니다. 실행은 Node.js가 맡고, 타입 검사는 tsc --noEmit이 맡고, 배포 산출물이 필요한 경로는 기존 빌드 도구가 맡는 식으로 책임을 나누면 안정적으로 도입할 수 있습니다.

처음 적용한다면 서비스 엔트리보다 내부 스크립트 하나가 좋습니다. Node 버전, import 경로, 변환 문법 사용 여부, CI 타입 체크를 확인한 뒤 점진적으로 넓혀가세요. 그렇게 하면 TypeScript의 가독성과 Node.js의 단순한 실행 경험을 함께 가져갈 수 있습니다.