본문으로 건너뛰기
개발 머꼬
개발 노트JavaScript
hohyeon.dev17

Node에서 .ts를 그냥 실행했더니 enum에서만 에러가 난 이유

  • #Engineering Note
  • #JavaScript
  • #Node.js
  • #TypeScript

문제 발생

빌드 단계를 없애려고 스크립트를 node script.ts로 직접 실행하도록 바꿨습니다. 대부분 잘 돌았는데 한 파일에서만 에러가 났습니다.

enum Level { Info, Warn, Error }   // ❌ 실행 실패

같은 파일의 인터페이스와 타입 별칭, 함수 시그니처의 타입은 전혀 문제가 없었습니다.

원인 분석

Node의 TypeScript 지원은 타입 스트리핑입니다. 이름 그대로 타입을 공백으로 치환해서 지우는 것이 전부입니다. 타입 검사도 하지 않고, tsconfig.json도 읽지 않으며, 문법을 다른 문법으로 변환하지 않습니다. 그래서 지원 대상이 정확히 "지우면 되는 문법(erasable syntax)"으로 한정됩니다.

enum은 지워지지 않습니다. enum Level { Info }는 런타임에 존재하는 객체를 만들어야 하므로 코드 생성이 필요합니다. 지울 수 있는 타입이 아니라 값을 만드는 선언이기 때문입니다.

같은 이유로 실패하는 것들:

문법왜 안 되는가
enum런타임 객체를 생성해야 함
런타임 코드가 있는 namespace값을 담은 객체를 만들어야 함 (타입만 있는 namespace는 가능)
파라미터 프로퍼티 constructor(private x: string)생성자 본문에 대입문을 만들어야 함
데코레이터TC39 제안이 Stage 4가 아님

한 가지 더 조심할 것은 import입니다. 타입만 가져오는 import에 type 키워드를 빼면, 스트리핑 후에도 값 import로 남아 존재하지 않는 것을 불러오려다 런타임에 실패합니다.

import { User } from "./types.ts";        // ❌ 런타임에 User를 찾는다
import type { User } from "./types.ts";   // ✅ 통째로 지워진다
import { fn, type FnParams } from "./fn.ts";  // ✅ 섞어 쓸 수 있다

해결 방안

  1. 지울 수 없는 문법을 대체합니다. enum은 대부분 as const 객체로 충분하고, 번들에도 남지 않습니다.
const Level = { Info: 0, Warn: 1, Error: 2 } as const;
type Level = (typeof Level)[keyof typeof Level];

파라미터 프로퍼티는 필드 선언과 대입으로 풉니다.

class Service {
  private readonly repo: Repo;
  constructor(repo: Repo) { this.repo = repo; }
}
  1. erasableSyntaxOnly를 켜서 컴파일 타임에 막습니다. 실행해 봐야 아는 대신, 지울 수 없는 문법을 쓰면 타입 체커가 바로 알려 줍니다.
{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "rewriteRelativeImportExtensions": true
  }
}

verbatimModuleSyntax가 켜져 있으면 import type을 빠뜨린 것도 함께 잡힙니다.

  1. 타입 검사는 여전히 따로 해야 합니다. Node는 타입을 지울 뿐 검사하지 않습니다. tsc --noEmit을 CI에 남겨 두는 것이 전제입니다.
  2. 상대 경로 import에는 확장자를 씁니다. ./types.ts처럼 실제 파일명을 그대로 적어야 Node가 찾습니다. 기존 코드가 확장자를 생략하고 있다면 rewriteRelativeImportExtensions가 도와줍니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.