~/web-app-dev.md
WEB_APP_DEV

Node가 TypeScript를 바로 실행해도, 웹앱 빌드 계약은 사라지지 않는다

10분 읽기·2026.06.12·출처 4·00
오늘의 질문

node script.ts가 동작하기 시작하면, 우리 팀의 TypeScript 설정과 빌드 경계는 더 단순해질까 아니면 더 명시적이어야 할까?

web-app-dev.md
SURVIVE.exe

Node가 TypeScript를 바로 실행해도, 웹앱 빌드 계약은 사라지지 않는다

1. 왜 지금 봐야 하나

최근 웹앱 개발에서 TypeScript는 더 이상 “빌드 도구가 처리해 주는 언어”로만 남아 있지 않다. Node.js 문서는 TypeScript 실행 지원을 별도 장으로 다루고 있고, Node.js 22.18.0 LTS 릴리스는 type stripping이 기본 활성화되어 .ts 파일을 추가 설정 없이 실행할 수 있다고 설명했다. 현재 Node 문서 기준으로는 이 기능이 안정화되어, Node가 지울 수 있는 타입 문법만 포함한 TypeScript 파일을 실행할 수 있다.

겉으로 보면 반가운 변화다. 작은 백오피스 스크립트, 마이그레이션 보조 코드, seed 파일, cron 작업을 만들 때 tsx, ts-node, 별도 빌드 단계 없이 node something.ts를 기대할 수 있기 때문이다. AI 코딩 도구가 만든 짧은 운영 스크립트를 바로 돌려보는 흐름도 쉬워진다.

하지만 여기서 한 가지 오해가 생기기 쉽다.

Node가 TypeScript를 실행한다는 말은, Node가 TypeScript 프로젝트를 빌드한다는 뜻이 아니다.

웹앱 팀에 중요한 차이는 바로 여기에 있다. 타입을 지우는 실행 경로와, 앱을 배포 가능한 JavaScript로 만드는 빌드 경로는 서로 다른 계약이다. 이 차이를 모르고 “이제 tsconfig는 덜 중요해졌다”고 생각하면, 로컬 스크립트는 잘 돌아가는데 Next.js 서버 코드, API 패키지, 워커 번들, 테스트 러너가 서로 다른 모듈 해석을 하는 문제가 생긴다.

2. 핵심 개념

이번 글의 핵심 개념은 실행 계약과 빌드 계약을 분리해서 보는 것이다.

실행 계약: Node가 지금 이 파일을 어떻게 읽을 것인가

Node 문서의 TypeScript 지원은 기본적으로 type stripping이다. 즉 타입 주석처럼 런타임에 의미가 없는 TypeScript 문법을 공백으로 바꾸고 실행한다. 문서에는 중요한 제약이 함께 적혀 있다.

  • 타입 검사는 하지 않는다.
  • tsconfig.json을 읽지 않는다.
  • paths alias나 낮은 JavaScript 버전으로의 변환처럼 tsconfig에 의존하는 기능은 의도적으로 지원하지 않는다.
  • 기본 경로는 “지울 수 있는 TypeScript 문법”에 맞춰져 있다.

이 계약은 가볍다. const name: string = "World" 같은 문법은 실행 전에 타입 부분만 지우면 된다. 반대로 enum, parameter property, namespace처럼 JavaScript로 바꾸는 변환이 필요한 문법은 단순히 지우는 것만으로는 충분하지 않다.

빌드 계약: 프로젝트 전체를 어떤 JavaScript로 만들 것인가

빌드 계약은 다르다. Next.js, Vite, Hono API, Supabase Edge Function, 패키지 배포용 라이브러리는 파일 하나가 아니라 프로젝트 전체를 대상으로 한다. 여기서는 다음 질문이 중요하다.

  • ESM으로 낼 것인가, CommonJS로 낼 것인가?
  • 브라우저 번들러가 해석할 코드인가, Node 런타임이 직접 해석할 코드인가?
  • import 확장자를 어떻게 쓸 것인가?
  • 경로 alias를 빌드와 런타임 모두 이해하는가?
  • 타입 전용 import가 런타임 import로 남지 않는가?

TypeScript의 module 옵션 문서가 node16, node18, node20, nodenext, preserve, esnext 같은 선택지를 길게 설명하는 이유가 여기에 있다. “TypeScript를 쓴다”는 하나의 선택이 아니라, 어느 런타임과 번들러에게 어떤 모듈 형태를 약속할지 정하는 문제다.

3. 최신 이슈와 연결

Node.js 22.18.0 릴리스는 type stripping이 기본 활성화되었다고 알렸다. 이후 Node 문서는 현재 버전에서 type stripping을 안정화된 기능으로 설명하고, 가벼운 사용을 위한 추천 tsconfig 예시도 제시한다. 예시에는 target: "esnext", module: "nodenext", rewriteRelativeImportExtensions: true, erasableSyntaxOnly: true, verbatimModuleSyntax: true 같은 옵션이 들어간다.

이와 맞물려 TypeScript 5.9 릴리스는 두 가지 신호를 준다.

첫째, tsc --init 결과가 더 작고 현대적인 기본값으로 바뀌었다. 예전처럼 많은 주석 옵션을 늘어놓는 대신, 실제 프로젝트에서 자주 쓰는 기준을 더 간결하게 보여주려는 방향이다.

둘째, --module node20 지원이 추가되었다. TypeScript 문서는 node16, node18, node20, nodenext 모드가 Node의 네이티브 ESM 지원과 통합된다고 설명한다. 특히 node20은 Node 20의 동작을 더 고정적으로 겨냥하고, nodenext는 최신 안정 Node 동작에 따라 변할 수 있는 모드다.

즉 최신 흐름은 “설정이 필요 없어졌다”가 아니다. 오히려 반대에 가깝다.

  • 단일 실행 스크립트는 더 가벼워졌다.
  • 프로젝트 모듈 계약은 더 명확히 골라야 한다.
  • AI가 생성한 코드일수록 import, 확장자, alias, 타입 전용 import가 실제 런타임에서 맞는지 확인해야 한다.

4. 개발자 관점 해석

웹앱 개발자가 여기서 조심해야 할 실패 모드는 네 가지다.

1) “로컬 스크립트 성공”을 “배포 코드 성공”으로 착각한다

node scripts/cleanup.ts가 실행됐다고 해서 앱의 서버 코드가 같은 방식으로 안전하다는 뜻은 아니다. Node type stripping은 tsconfigpaths를 읽지 않는다. 따라서 로컬에서 번들러가 @/lib/db를 잘 해석하더라도, Node가 직접 실행하는 스크립트에서는 같은 alias가 깨질 수 있다.

2) 타입 검사와 실행을 같은 단계로 본다

Node 문서는 type stripping이 타입 검사를 수행하지 않는다고 명확히 말한다. 따라서 node file.ts가 통과했다는 것은 문법을 지우고 실행할 수 있었다는 뜻이지, 타입 안정성이 검증됐다는 뜻이 아니다. 운영 스크립트라면 더 위험하다. 마이그레이션이나 결제 정산 스크립트는 “실행됨”보다 “잘못된 입력을 타입과 테스트로 막았음”이 중요하다.

3) 모듈 옵션을 프레임워크가 알아서 맞춰준다고 믿는다

Next.js 앱, Hono 서버, Node CLI, 브라우저 번들러는 모두 import를 다르게 해석할 수 있다. module: "preserve"는 번들러 중심 프로젝트에 자연스러울 수 있고, module: "nodenext"node20은 Node 직접 실행 패키지에 더 적합할 수 있다. 하나의 monorepo 안에서도 앱, 패키지, 스크립트의 계약이 다를 수 있다.

4) AI가 만든 편의 문법을 그대로 실행한다

AI 코딩 도구는 종종 enum, path alias, extension 없는 상대 import, 타입 전용 import 누락을 섞어서 만든다. 번들러가 있는 앱 코드에서는 우연히 통과할 수 있지만, Node 직접 실행 스크립트에서는 바로 실패하거나, 더 나쁘게는 환경마다 다른 방식으로 해석될 수 있다.

5. 내 프로젝트에 적용할 체크포인트

오늘 바로 볼 체크리스트는 이렇다.

  • scripts/, tools/, migrations/, seed/ 폴더 중 Node가 직접 실행하는 .ts 파일이 있는가?
  • 그 파일들이 @/, ~, src/* 같은 tsconfig path alias에 의존하는가?
  • node file.ts로 실행할 파일과 next build, vite build, tsc -b로 처리할 파일이 같은 규칙을 공유한다고 가정하고 있지는 않은가?
  • npm run typecheck가 실제로 별도 단계로 존재하는가?
  • 운영 스크립트 실행 전 tsc --noEmit 또는 테스트가 CI에서 먼저 돈다는 보장이 있는가?
  • Node 직접 실행용 tsconfig에 erasableSyntaxOnly처럼 변환이 필요한 문법을 미리 막는 옵션을 둘 수 있는가?
  • 라이브러리 패키지는 module, exports, type, 파일 확장자 정책을 문서화했는가?

작은 팀이라면 모든 것을 한 번에 바꾸기보다, 먼저 “Node 직접 실행 파일”과 “번들러/프레임워크가 처리하는 파일”을 README에 나눠 적는 것만으로도 사고를 줄일 수 있다.

6. 오늘 10분 액션

10분만 써서 아래를 해보자.

  1. 프로젝트에서 Node가 직접 실행하는 TypeScript 파일을 하나 고른다. 예: scripts/seed.ts, scripts/check-env.ts, tools/backfill.ts.
  2. 그 파일의 import를 본다. 상대 경로인지, path alias인지, 확장자가 있는지 표시한다.
  3. 아래 세 명령 중 현재 프로젝트에 맞는 것을 실행하거나 package script로 추가한다.
zsh — 생존확인.sh
node scripts/check-env.ts
npm run typecheck
npx tsc --noEmit
  1. 실패하면 “Node 실행 실패”와 “TypeScript 타입 실패”를 구분해서 적는다.
  2. AI 도구에게 바로 수정을 맡기기 전에 이렇게 요청한다.
zsh — 생존확인.sh
이 파일은 Node가 직접 type stripping으로 실행할 수 있어야 한다.
tsconfig paths alias에 의존하지 말고, 타입 검사는 별도 npm run typecheck에서 통과해야 한다.
변환이 필요한 TypeScript 문법(enum, parameter property 등)을 피해서 수정해줘.

핵심은 새 도구를 많이 붙이는 것이 아니다. 실행 경로와 타입 검증 경로를 분리해서 실패를 읽는 습관을 만드는 것이다.

7. 더 볼 자료

  • Node.js TypeScript 문서의 “Type stripping”과 “Determining module system” 섹션
  • TypeScript TSConfig module 옵션 문서
  • TypeScript 5.9 릴리스의 tsc --init, --module node20 설명
  • 현재 프로젝트의 package.json scripts, tsconfig.json, tsconfig.node.json, next.config.* 또는 vite.config.*

중복 회피 메모

로컬 content/generated와 Supabase의 최근 web_app_dev 글을 확인했다. 최근 글은 Hono 보안 릴리스의 미들웨어 입력 계약, Supabase RLS Tester, Supabase Auth 이메일, Next.js AI 디버깅, PWA 설치 수명주기, Expo/React Native 운영, Android 배포 신원 검증을 다뤘다. 이번 글은 특정 프레임워크 보안이나 Supabase 권한 정책이 아니라, Node의 TypeScript type stripping과 TypeScript 5.9의 모듈 설정 변화를 바탕으로 “실행 계약과 빌드 계약을 분리해야 한다”는 TypeScript 운영 관점에 집중했다.

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)