툴체인, 하루 만에 만들기

okorion·2025년 11월 14일

“Toolchains, the hard parts” 요약 및 재구성

현대 개발 환경에서 “툴체인(toolchain)”이라는 말은 아주 많이 쓰이지만, 정작 직접 만들어본 사람은 많지 않다. 이 글은 Biome 유지보수자가 툴체인을 만들면서 겪은 경험을 바탕으로,

  • 툴체인이 무엇인지
  • 왜 JavaScript 세계의 툴체인은 유난히 복잡한지
  • 진짜 어려운 부분이 무엇인지(DX, 에러 메시지, 포매터 등)
    를 정리한 내용을 기반으로 재구성한 것이다.

1. 툴체인이란 무엇인가?

1.1 정의

툴체인(toolchain)
→ 보통 CLI 형태로 제공되며, 여러 개발 도구를 하나의 인터페이스로 묶어서 제공하는 소프트웨어.

예: Rust의 cargo

  • 패키지 매니저(의존성 관리)
  • formatter (cargo fmt)
  • linter (clippy 연동)
  • test runner (cargo test)
  • task runner / 스크립트
  • compiler (cargo build)
  • 기타 부가 명령

특징:

  • 개발자 입장에서 “하나의 명령어(cargo)”를 통해 프로젝트 라이프사이클 전체를 다룰 수 있음
  • DX가 좋고, 비교적 버그가 적으며, 일관된 사용성 제공

1.2 JS/TS 생태계의 “툴체인 비슷한 것들”

JS/TS에는 cargo 같은 단일 공식 툴체인은 없다. 대신 프레임워크 단위로 비슷한 개념이 등장한다.

  • NestJS: Nest CLI를 통해 프로젝트 생성, 빌드, 테스트 등 제공
  • Astro: Astro 프로젝트 생성·빌드·프리뷰 등 일체형 CLI 제공
  • Next.js: next dev, next build, next lint 등 프레임워크 중심 툴체인

이들은 프레임워크 전용 툴체인에 가깝고, 언어 자체의 공식 툴체인과는 다르다.


2. 새로운 세대의 툴체인: Bun, Deno, 그리고 Node의 변화

최근에는 런타임 자체가 툴체인 역할을 포함하는 흐름이 나타난다.

2.1 Bun

  • JavaScript/TypeScript 런타임
  • 제공 기능:
    • 패키지 매니저
    • 번들러
    • 테스트 러너
    • 태스크 러너
    • (그리고 더 많은 기능)

→ 런타임 + 패키지 매니저 + 빌드 + 테스트를 한 번에 다루려는 시도.

2.2 Deno

  • TypeScript/JavaScript 런타임
  • 제공 기능:
    • 패키지 매니저
    • 번들러
    • 테스트 러너
    • 문서 생성
    • formatter
    • linter
    • language server
    • 태스크 러너
    • 기타

→ JS/TS + 툴체인을 하나의 “플랫폼”으로 통합하려는 모델.

2.3 Node.js 도 역시 따라가는 중

Node도 이 흐름을 인지하고 언어 레벨에서 툴 기능을 추가하고 있다.

  • node:test – 내장 테스트 러너
  • node run – 태스크 러너 성격의 기능

→ JS/TS 생태계 전체가 “언어 + 툴체인 일체형” 모델로 수렴하는 중이라는 이야기.


3. 툴체인의 진짜 난이도: DX(Developer Experience)

글쓴이는 Biome 유지보수자로서 툴체인을 만들며 다음을 깨달았다고 한다.

“툴체인의 가장 어려운 부분은 성능이나 기능이 아니라 개발자 경험(DX) 이다.”

3.1 DX는 “동료 개발자”를 위한 경험이다

  • UX와 달리, DX는 개발자(동료)가 대상
  • 하지만 “동료”라고 해서 모두 같은 지식을 가진 것이 아니다.
    • 언어·배경·경력·습관이 모두 다르고
    • 시니어라고 해도 그 툴체인에 대해서는 완전 초보일 수 있음
  • 따라서 툴체인의 DX는 “처음 쓰는 초보 → 숙련자”까지 성장 경로를 고려해서 설계해야 한다.

결론:

  • 툴 개발자는 “전문가끼리 통하는 도구”를 만드는 게 아니라,
  • 항상 처음 쓰는 사람을 염두에 두고 설계해야 한다.

4. 좋은 에러 메시지 vs 나쁜 에러 메시지

4.1 과거: 에러 메시지는 “부가 요소”

10년 전만 해도 DX에 대한 관심은 거의 없었고, 에러 메시지는 대충 “SyntaxError: XXX” 수준으로 끝나는 경우가 많았다.

예: 다음 코드

const something;
  • 브라우저(Firefox) 에러:
Uncaught SyntaxError: missing = in const declaration

기술적으로는 맞는 말이지만:

  • 어느 위치인지 시각적으로 잡아주지 않고
  • “missing =” 같은 표현이 초보 입장에서는 직관적이지 않다.

4.2 Biome식 메시지

같은 코드를 Biome에 돌리면:

 at file.js:1:7 parse ━━━━━━━━━━━━━━━━━━━━

  ✖ Const declarations must have an initialized value.
  
  > 1 │ const something;
      │       ^^^^^^^^^
  
  ℹ This variable needs to be initialized.

특징:

  • 위치를 시각적으로 표시
  • 규칙을 문장으로 설명
    • “const 선언은 초기화가 필요하다”
  • 해석까지 제공
    • “이 변수는 초기화되어야 한다”

4.3 또 다른 예시: var foo = ;

브라우저:

Uncaught SyntaxError: expected expression, got ';'

Biome:

 at file.js:1:11 parse ━━━━━━━━━━━━━━━━━━━━

  ✖ Expected an expression, or an assignment but instead found ';'.
  
  > 1 │ var foo = ;
      │           ^
  
  ℹ Expected an expression, or an assignment here.

여전히 “expression”이라는 용어는 어렵지만,

  • 위치를 보여주고,
  • “여기에 뭔가 값(식)이 와야 한다”는 점을 분명히 설명해준다.

DX를 고려하면, 에러 메시지는 “문법 설명”이 아니라 “이 코드를 어떻게 고치면 되는지”를 중심으로 설계해야 한다.


5. Actionable 메시지: “고치는 방법”까지 말해줘야 한다

5.1 Actionable 메시지란?

“이 에러를 어떻게 해결해야 하는지까지 안내하는 메시지”

단순히 “문제가 있다”고만 말하면 사용자가 막힌다.
“어떻게 고칠 수 있는지”까지 알려줘야 DX가 완성된다.

5.2 예: ESLint vs oxlint vs Biome

다음 코드:

async function foo() {
  doSomething();
}

ESLint require-await 메시지

1:1   error  Async function 'foo' has no 'await' expression  require-await
  • 문제: “await expression이 없다”는 사실만 알려준다.
  • 어디를 고쳐야 하는지, 어떻게 고치는 게 좋은지 설명이 없다.
  • Actionable하지 않다.

oxlint 메시지

  × eslint(require-await): Async function has no 'await' expression.
   ╭─[file.js:1:16]
 1 │ async function foo() {
   ·                ───
 2 │     doSomething();
   ╰────
  help: Consider removing the 'async' keyword.
  • 에러 위치를 보여주고
  • “async를 제거하는 것을 고려하라”는 구체적인 해결책을 제시
  • Actionable

Biome 메시지

 at file.js:1:1 lint/suspicious/useAwait ━━━━━━━━━━━

  ⚠ This async function lacks an await expression.
  
  > 1 │ async function foo() {
      │ ^^^^^^^^^^^^^^^^^^^^^^
  > 2 │     doSomething();
  > 3 │ }
      │ ^
  
  ℹ Remove this async modifier, or add an await expression in the function.
  
  > 1 │ async function foo() {
      │ ^^^^^^^^^^^^^^^^^^^^^^
  > 2 │     doSomething();
  > 3 │ }
      │ ^
  
  ℹ Async functions without await expressions may not need to be declared async
  • 해결책 두 가지를 명시:
    • async 제거
    • await 추가
  • 왜 문제인지까지 설명:
    • await 없는 async 함수는 굳이 async일 필요가 없다는 점

5.3 패턴

  • ESLint: 오래된 도구, 여전히 DX에 취약한 부분이 존재
  • oxlint / Biome: 후발 주자 → 과거의 피드백을 반영해 DX를 더 공격적으로 개선

6. “툴체인이라면 기본으로 다 있어야 한다”는 기대

현대 개발자는 툴체인에서 다음을 당연하게 기대한다:

  1. 컴파일러 / 빌드 / 인터프리터
  2. 에디터 지원 (LSP 등)
  3. linter
  4. formatter

하지만 이 네 가지를 모두, 높은 완성도로, 일관된 철학 아래 제공하는 것은 매우 어렵다.

6.1 JS 생태계의 극단적인 분절

대표적인 예:

  • ESLint: 자체 parser + 자체 rule 엔진
  • Prettier: 자체 parser + 별도 포매팅 로직

→ 언어(ECMAScript)와는 별도의 조직·팀이 각각 구현
→ 프로젝트마다:

  • 번들러
  • 트랜스파일러
  • linter
  • formatter
  • test runner
  • task runner

를 각각 따로 설치·설정·연결해야 한다.

“새로운 웹 프로젝트 셋업에 얼마나 많은 시간을 쓰고 있는지 떠올려 보라”는 의미.

6.2 Deno, Go, Rust의 다른 접근

  • Deno
    • formatter 제공 (dprint 기반)
    • 다만 마찬가지로 내부/외부 도구 조합 구조
  • Go
    • gofmt가 언어 소스 코드 레벨에 사실상 내장
    • 포매터가 “언어 표준”으로 인식됨
  • Rust
    • rustfmt(포매터), clippy(linter)가 공식 org 안에 존재
    • 컴파일러와 완전히 하나는 아니지만, 언어와 함께 움직이는 공식 툴로 자리잡음

젊은 언어일수록 “언어 + 공식 툴체인” 모델이 강하다.


7. 툴체인에서 가장 어려운 컴포넌트: 컴파일러가 아니다, 포매터다

많은 사람이 “컴파일러가 제일 어렵겠지”라고 생각하지만, 실제로는:

  • 컴파일러 구현에 대한 지식은 매우 풍부하다.
    • 교과서, 논문, 책, 강의, 블로그, 예제 코드 등 방대
  • 포매터(formatter)는 다르다.
    • “소스코드를 어떻게 예쁘게, 안정적으로, 예측 가능하게 포맷할 것인가?”
    • 이건 난도가 높은 문제인데,
    • 그에 비해 지식·자료가 거의 없다.

사례:

  • Dart 포매터를 만든 Bob Nystrom(Crafting Interpreters 저자)이 그 어려움을 글로 남김
  • Prettier의 Christopher Chedeau도 “Prettier의 탄생” 글에서 포매터 구현의 난이도를 다룸

Biome 공동 작성자로서 글쓴이의 코멘트:

  • 포매터는 정말 어렵다.
  • 개인적인 조언:
    • 포매터를 직접 만들고 싶다면
      Prettier + Biome 방식의 아키텍처를 참고하는 게 좋다.
  • 실제로 Ruff, oxfmt도 Biome 포매팅 인프라의 fork를 사용한다.

8. 정리: 툴체인은 생각보다 훨씬 어렵고, DX는 아직도 진화 중이다

핵심 포인트 정리:

  1. 툴체인은 단순히 명령어 묶음이 아니라, 개발자의 전 과정을 다루는 플랫폼이다.
  2. 컴파일러보다 에러 메시지·Diagnostic·포매터·린터의 DX 설계가 훨씬 어렵다.
  3. JS/TS 생태계는 오랫동안 분절된 툴 조합을 수작업으로 묶는 세계였고,
    Bun/Deno/Node의 변화, Biome/oxlint/Vite+/Rust 등은 이를 정리하려는 시도다.
  4. 포매터는 가장 구현 난이도가 높은 구성요소 중 하나고,
    최근에서야 Prettier/Biome 등을 통해 그 노하우가 축적되기 시작했다.
  5. 전체적인 방향성:
    • 과거: “도구는 돌아만 가면 된다”
    • 현재/미래: “도구는 누가 써도 막히지 않게 설계되어야 한다

툴체인은 “당연히 있는 것”이 아니라, 매우 고난이도의 소프트웨어 공학 결과물이다.
앞으로도 언어·런타임·툴 체인은 계속 진화할 것이고, DX 기준도 점점 높아질 것이다.


profile
Tech Blog

0개의 댓글