Creco's Blog

Tech

TSC 빌드 없이 TypeScript 커맨드 툴 개발하기

조회수302

읽는 시간: 7분



안녕하세요, Frontend Engineer 정석호입니다.


지난 글에서는 shell 기반의 커맨드 툴을 소개했었죠? 이번에는 TypeScript로 CLI를 만들면서 로컬 개발 속도를 높이기 위해 어떤 환경 구성이 유용했는지 소개해 보려고 해요. 매번 tsc로 빌드해서 dist를 확인하는 건 꽤 번거롭잖아요. 그래서 요즘은 esbuild나 tsx처럼 빠르게 실행 가능한 방식이 대세인데요, 저도 이것저것 시도해 보다가 괜찮은 셋업을 정리하게 됐습니다. 편의를 위해 Node.js의 ESM 환경에서 진행하는 걸 보여드릴게요!


CLI 개발에서 고려할 점들


CLI 툴을 만들 때 대부분의 개발자는 src 디렉토리에 TypeScript 소스를 작성하고, bin 디렉토리에는 실행 스크립트를 두는 구조를 선호하실 거예요. 저도 그렇게 구성했는데, 다음과 같은 요구사항이 생겼습니다.



  1. 개발 중엔 TypeScript 소스를 직접 실행할 수 있어야 해요.

  2. 배포 후에는 tscesbuild를 통해 번들된 JavaSript 파일을 실행해야겠죠.

  3. import 구문은 import { foo } from "./foo.js"처럼 .js 확장자를 쓰도록 ESM 표준을 따릅니다.


이걸 만족하려면 bin/dev-cli.js는 직접 src/index.ts 파일을 불러와야 하고, bin/cli.js는 번들된 dist/index.js를 참조해야 해요.


register가 뭐길래?


ts-nodetsx 같은 도구가 이런 걸 가능하게 해 주는 이유는 바로 register 라는 개념 덕분이에요.
Node.js에서 어떤 파일을 실행할 때, 해당 파일을 JS가 아닌 다른 형식(예: ts, jsx 등)으로 동작하게 하고 싶을 때 loaderregister hook을 이용하죠.


즉, 실행 전에 컴파일 과정을 자동으로 끼워넣는 방식이에요.
이걸 활용하면 src/index.ts를 미리 빌드하지 않고도 직접 실행할 수 있게 됩니다.


궁금하다면 이곳에서 더 자세한 정보를 확인할 수 있어요!


ts-node로 실행 환경 구성하기


먼저 ts-node 방식부터 볼게요. 이 방법은 Node.js에 좀 더 가깝고, 디버깅도 익숙한 느낌이에요.


bash 실행 시


node --experimental-specifier-resolution=node --experimental-loader ts-node/esm.mjs ./src/index.ts

dev-cli.js (개발용)


#!/usr/bin/env node

import { register } from "node:module";
import { pathToFileURL } from "node:url";

register("ts-node/esm", pathToFileURL("./"));

import("../src/index.ts");

cli.js (배포용)


#!/usr/bin/env node

import("../dist/index.js");

구성은 간단해 보여도, tsconfig 설정과 import 경로, 확장자까지 꼼꼼히 챙겨야 제대로 동작합니다. 특히 ESM 환경에서는 확장자를 빼먹으면 바로 에러가 나요.ㅎㅎ


tsx로 구성해 보기


이번엔 tsx 기반이에요. esbuild를 기반으로 동작하기 때문에 실행 속도가 정말 빠르다는 게 장점이에요.


bash 실행 시


yarn tsx ./src/index.ts

dev-cli.js (개발용)


#!/usr/bin/env tsx

import { tsImport } from "tsx/esm/api";

await tsImport("../src/index.ts", import.meta.url);

cli.js (배포용)


#!/usr/bin/env node

import("../dist/index.js");

이 방식은 tsx가 알아서 loader 역할까지 다 해 주기 때문에 Node.js 옵션을 따로 설정할 필요가 없어요.
그리고 tsImport를 쓰면 import 문도 동적으로 처리 가능해서 유연해요.


ts-node vs tsx, 직접 비교해본 실행 속도


이론만 봐선 차이가 잘 와닿지 않죠? 실제로 시간 측정을 해봤습니다.


➜  hello-tsx-cli git:(main) ✗ time yarn hello-tsx-cli-dev
Hello, World!
yarn hello-tsx-cli-dev 0.21s user 0.04s system 123% cpu 0.204 total

➜ hello-ts-node-cli git:(main) ✗ time yarn hello-ts-node-cli-dev
Hello, World!
yarn hello-ts-node-cli-dev 0.98s user 0.08s system 215% cpu 0.495 total

결과를 보면 알 수 있듯이 tsx 쪽이 훨씬 빠르더라구요. 물론 이건 아주 단순한 실행 환경에서 측정한 거라 복잡한 프로젝트에선 차이가 달라질 수 있겠죠.


마무리하며


개발 속도를 끌어올리고 싶은 상황이라면 tsx가 꽤 매력적인 선택이에요. 특히 setup이 간단하고 빠르게 작동하는 게 인상적이었어요.
반대로 ts-node는 좀 더 Node.js의 철학에 가까워서 디버깅이나 설정을 미세하게 조절하고 싶을 땐 유리합니다.


결국은 팀 상황이나 프로젝트 성격에 따라 선택이 달라지겠지만, 둘 다 훌륭한 옵션이에요.
개발 중에는 tsx, 배포는 번들된 js를 쓰는 하이브리드 구성이 참 좋더라구요.


혹시 비슷한 고민 중이신 분이 있다면, 저처럼 한 번 직접 비교해보시는 걸 추천드려요!


제가 작성한 코드가 궁금하시다면, 여기로 오시면 됩니다.


공유하고 싶은 사례가 있거나 궁금한 점은 댓글이나 이슈로 남겨주세요 🙌