🔥 환경 한 번에: bun, tsx, tsc 6.0

처음 TypeScript 환경을 손으로 만들었을 때 나는 도구 이름 세 개가 한꺼번에 들어왔다. npm install -g typescript 로 tsc 를 깔고, ts-node 도 따로 깔고, node 와 npm 은 그대로 썼다. 같은 일을 두 가지 방식으로 처리하는 명령이 자꾸 함께 나와서 무엇을 골라야 할지 한참 고민했다. 시간이 좀 지나고 나서야 세 도구가 서로 다른 일을 맡는다는 점을 이해했다.
bun, tsc, tsx 의 역할은 뚜렷하다. bun 은 패키지 매니저와 런타임을 맡고, tsc 6.0 은 타입을 검사하고, tsx 는 빌드 없이 실행한다. 그래서 hello.ts 한 파일만 있으면 설치, 검사, 실행을 같은 디렉토리에서 차례로 확인할 수 있다.
세 도구가 각자 맡는 일
세 도구는 이름이 비슷해 보여도 역할이 다르다. 셋 다 TypeScript 와 관련이 있고, 셋 다 명령줄에서 한 줄로 부른다. 그래서 bun, tsx, tsc 가 같이 묶여 있는 걸 처음 봤을 때 나는 셋이 겹치는 것처럼 느꼈다.
실제로는 각자 다른 일을 맡는다.
- bun 은 패키지 매니저와 런타임이다.
bun install로 의존성을 받고,bun run으로 스크립트를 돌린다. node 와 npm 의 역할을 한 도구로 합친다. - tsc 는 TypeScript 6.0 의 타입 검사기다.
tsc --noEmit으로 코드의 타입 일관성만 검사하고 컴파일 산출물은 만들지 않는다. - tsx 는 TypeScript 직접 실행기다.
tsx hello.ts한 줄로 빌드 단계 없이 .ts 파일을 그대로 돌린다. 내부적으로 esbuild 가 타입 주석을 벗기고, 그 결과를 node 에 넘긴다.
셋 사이의 관계는 단순하다. bun 으로 환경을 깔고, tsc 로 타입을 검사하고, tsx 로 실제 코드를 돌린다. 한 .ts 파일을 두 명령에 차례로 넣는 구조다.

한 디렉토리에 워크스페이스 만들기
hello.ts 한 파일을 돌리려면 디렉토리 하나에 package.json 한 개와 tsconfig.json 한 개가 있으면 된다. bun init 한 줄이 그 둘을 같이 만들어 준다.
mkdir hello-ts && cd hello-ts
bun init -y
bun add -d typescript@6 tsxmkdir hello-ts && cd hello-ts
bun init -y
bun add -d typescript@6 tsx결과는 package.json, tsconfig.json, bun.lock, 그리고 비어 있는 index.ts 다. tsconfig.json 의 기본값이 TS 6.0 의 권장 조합과 거의 같다. strict: true, target: "es2025", module: "nodenext", moduleResolution: "nodenext", verbatimModuleSyntax: true 가 한 줄씩 들어 있다. 다섯 옵션 모두 TS 6.0 권장 기본값 그대로다.
index.ts 를 지우고 hello.ts 를 새로 적는다.
// file: hello.ts
export {};
const greet = (name: string): string => `Hello, ${name}!`;
console.log(greet("TypeScript"));
// 아래 줄의 주석을 풀면 tsc 가 멈춘다.
// console.log(greet(42));
// Argument of type 'number' is not assignable to parameter of type 'string'.// file: hello.ts
export {};
const greet = (name: string): string => `Hello, ${name}!`;
console.log(greet("TypeScript"));
// 아래 줄의 주석을 풀면 tsc 가 멈춘다.
// console.log(greet(42));
// Argument of type 'number' is not assignable to parameter of type 'string'.// file: hello.ts 헤더는 본문 코드 블록의 약속이다. 모든 코드 블록 첫 줄에 파일 이름을 적어 둔다. export {}; 한 줄은 이 파일을 ESM 모듈로 만든다. 모듈로 두지 않으면 같은 함수 이름이 다른 파일에서 또 나올 때 컴파일러가 충돌로 본다.
이제 타입 검사를 한 줄로 돌린다.
bunx tsc --noEmitbunx tsc --noEmit출력이 한 줄도 없으면 통과다. --noEmit 옵션은 타입 검사만 하고 .js 파일을 만들지 않는다는 뜻이다. TS 6.0 에서 권장하는 모드다. 출력 .js 가 필요할 때는 별도 빌드 도구(esbuild, swc, bun build 등)에 넘기고, tsc 는 타입 검사 전담으로 둔다.

빌드 없이 그대로 돌리기
같은 hello.ts 를 그대로 실행한다.
bunx tsx hello.tsbunx tsx hello.ts출력은 Hello, TypeScript! 한 줄만 나온다. 별도 빌드 단계가 없다. tsx 는 esbuild 로 타입 주석만 벗기고 그 결과를 node 에 넘긴다. ts-node 를 써 본 적이 있다면 그 후속 도구라고 보면 된다. esbuild 기반이라 ts-node 보다 시작 속도가 빠르다.
tsc --noEmit 은 컴파일 시점에 타입을 검사하고 tsx hello.ts 는 런타임에 코드를 실행한다. 두 명령을 같은 .ts 파일에 차례로 적용하면 타입 안전성과 실행 결과를 함께 확인할 수 있다. 나는 새로운 .ts 파일을 만들 때마다 거의 반사적으로 두 명령을 차례로 친다.

타입 에러가 잡히는 방식
hello.ts 에서 주석으로 막아 둔 마지막 줄, console.log(greet(42)); 를 활성화한 상태를 가정한다. bunx tsc --noEmit 을 다시 돌리면 다음 출력이 나온다.
hello.ts:7:21 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
7 console.log(greet(42));
~~
Found 1 error in hello.ts:7hello.ts:7:21 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
7 console.log(greet(42));
~~
Found 1 error in hello.ts:7같은 상태에서 bunx tsx hello.ts 를 돌리면 실행은 그대로 된다. 출력은 Hello, TypeScript! 와 Hello, 42! 두 줄이다. JavaScript 는 숫자 42 를 문자열로 자동 변환해서 결과를 만든다.
greet(42) 한 줄이 tsc 와 tsx 를 따로 두는 이유를 설명한다. 런타임은 결과를 끝까지 만들고, 타입 검사기는 그 결과가 의도와 어긋나는지 컴파일 시점에 막는다. 실무 코드 베이스에서는 tsc --noEmit 을 CI 의 첫 단계에 두고, 빨간 줄이 하나도 없는 상태에서만 실행을 허용한다.

다섯 단계로 묶이는 흐름
hello.ts 실습은 다섯 명령으로 끝난다. bun init 으로 디렉토리 하나에 package.json 과 tsconfig.json 을 만들고, bun add -d typescript@6 tsx 로 두 도구를 넣고, hello.ts 를 적고, bunx tsc --noEmit 으로 타입을 검사하고, bunx tsx hello.ts 로 실행한다. 환경 세팅은 여기까지다.
hello.ts 한 파일은 두 도구가 차례로 보는 시범 케이스다. tsc 는 컴파일 시점에 타입을 보고, tsx 는 런타임에 코드를 실행한다. 그래서 같은 함수가 값 레벨에서는 실행 대상이고 타입 레벨에서는 검사 대상이라는 점을 한 파일에서 바로 확인할 수 있다.










