콘텐츠로 건너뛰기
Codex

Node 22에서 TypeScript를 빌드 없이 실행하기

TECH NOTE · AUTOMATION OPS

Node 22는 –experimental-strip-types로 TypeScript를 빌드 없이 실행합니다. 테스트도 node –test가 내장이라 프레임워크가 필요 없지만, 타입을 지우기만 하므로 enum은 못 씁니다.

예제에 빌드 단계를 넣으면 예제가 아니다

블로그 글에 딸린 코드 예제를 만들고 있었다. 원칙이 하나 있었다 — 받아서 한 줄로 돌아가야 한다.

TypeScript로 쓰고 싶었는데 여기서 막혔다. tsc를 넣으면 npm install이 앞에 붙고, package.jsontsconfig.json이 따라오고, node_modules가 생긴다. 판정 로직 100줄을 보여주려고 그걸 다 받게 할 수는 없다.

Node 22가 이걸 해결한다.

node --experimental-strip-types sheet.ts

빌드 단계가 없다. 설정 파일도 없다.

타입을 지우고 실행한다

이름 그대로다. --experimental-strip-types타입 표기를 지우고 남은 JavaScript를 실행한다.

export interface FrameRect {
  readonly x: number;
  readonly y: number;
}

export function resolveFrames(
  manifest: SpriteManifest,
  state: string,
): readonly FrameRect[] | null {
  return manifest.layout[state] ?? null;
}

interface는 통째로 사라지고, 함수의 타입 표기(: SpriteManifest, : readonly FrameRect[] | null)도 지워진다. 남는 건 함수 본문뿐이다.

타입 검사를 하지 않는다. 지우기만 한다. 타입이 틀려도 그냥 돌아간다. 검사가 필요하면 tsc --noEmit을 따로 돌려야 한다.

Node 22.6부터 들어갔고, 22.18부터는 플래그 없이도 동작한다. 확인은 이렇게 한다.

node --version

테스트도 내장이다

여기가 실제로 편한 부분이다. Node에 테스트 러너가 들어 있어서 Jest나 Vitest를 안 받아도 된다.

import { test } from "node:test";
import assert from "node:assert/strict";
import { resolveFrames } from "./sheet.ts";

test("행을 넘는 상태에서 계산이 깨진다", () => {
  const actual = resolveFrames(SAMPLE, "walk")!;
  assert.equal(actual[0].y, 512);
  assert.equal(actual[4].y, 512 * 5);
});

돌리는 명령이다.

node --experimental-strip-types --test sheet.test.ts

출력은 TAP 형식이라 그대로 읽힌다.

# pass 10
# fail 0

의존성 0개로 타입이 있는 코드에 테스트까지 붙는다.

걸리는 곳 두 군데

편하지만 제약이 있다. 모르고 쓰면 왜 안 되는지 한참 헤맨다.

`enum`을 못 쓴다

타입을 지우기만 하므로, 런타임에 코드를 만들어 내는 문법은 통하지 않는다.

enum Owner { UI, CHARACTER }        // 안 된다

enum은 지운다고 사라지는 게 아니라 실제 객체로 컴파일되어야 한다. 지우면 Owner가 존재하지 않게 되므로 Node가 거부한다.

같은 이유로 파라미터 프로퍼티(constructor(private x: number))와 네임스페이스도 안 된다.

대신 이렇게 쓴다.

export const Owner = {
  UI: "ui",
  CHARACTER: "character",
} as const;

객체 리터럴은 그냥 JavaScript라 지울 것이 없다.

`import` 경로에 확장자를 적어야 한다

import { resolveFrames } from "./sheet";     // 안 된다
import { resolveFrames } from "./sheet.ts";  // 이렇게

번들러를 쓸 때는 확장자를 생략하는 게 관행이라 손이 먼저 나간다. Node는 ESM 규칙을 따르므로 경로를 그대로 해석하고, 확장자가 없으면 파일을 못 찾는다.

.js로 적는 것도 안 된다. 실제 파일이 .ts이기 때문이다. 이 점이 tsc의 관행과 반대라 헷갈린다.

어디에 쓸 만한가

예제와 작은 도구에 맞는다. 받는 사람이 아무것도 설치하지 않아도 되는 것이 핵심 가치다.

실무 프로젝트에는 권하기 어렵다. 타입 검사를 안 하므로 CI에서 tsc --noEmit이 따로 필요하고, enum을 쓰는 기존 코드가 있으면 걸린다.

정리하면 이렇다.

상황 맞나
블로그 예제, 저장소 샘플 맞다
스크립트 한두 개짜리 도구 맞다
타입 검사가 필요한 프로덕션 코드 tsc를 따로 돌려야 한다
enum을 쓰는 기존 코드베이스 고쳐야 한다

정리

  • --experimental-strip-types는 타입을 지우고 실행한다. 검사하지 않는다
  • node --test가 내장이라 테스트 프레임워크가 필요 없다
  • enum·파라미터 프로퍼티·네임스페이스는 못 쓴다. 런타임 코드를 만들어 내기 때문이다
  • import 경로에 .ts 확장자를 적어야 한다
  • 의존성 없이 배포할 예제에 특히 맞는다

“받아서 한 줄로 돌아간다”를 지키려면 빌드 단계를 없애야 하는데, TypeScript를 포기하지 않고도 그게 된다.

이 블로그 더 보기

이 블로그는 실제 프로젝트를 AI와 함께 굴리면서 남긴 기록입니다.

  • 새 글은 RSS로 받아볼 수 있습니다.
  • 시리즈 전체는 여기에 정리해 두었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다