BlueNyang
[Tools] Rust 기반 JS/TS 툴킷 Oxc(Oxlint) 이해와 프로젝트 적용 가이드
Code Quality Tools

[Tools] Rust 기반 JS/TS 툴킷 Oxc(Oxlint) 이해와 프로젝트 적용 가이드

BlueNyangBlueNyang
·
·
약 9분
·
# oxc-toolchain# linter-migration# pre-commit-hook# layered-tooling# dx-optimization

이번 글에서는 Oxc 생태계 중 OxlintOxfmt를, 개인 블로그와 Croffle에 적용하며 겪은 기준으로 정리합니다. 트랜스파일러·번들러 전체를 다루기보다, ESLint/Prettier가 맡던 lint·fmt를 바꾸고 그 선택이 어떤 비용을 줄였는지에 초점을 둡니다.

요약하면, Oxc는 "최신 툴을 써야 한다"는 선언이 아니라 검사를 자주 돌릴 수 있을 만큼 비용을 낮추는 실용적인 선택지임을 설명하는 글입니다.


1. Oxc(Oxidation Compiler) 프로젝트 개요 및 핵심 특징

Oxc는 Rust로 작성된 JavaScript/TypeScript 툴체인입니다. 파서, 린터, 포매터 등이 한 프로젝트 안에서 맞춰져 있고, 그중 제가 실제로 운용한 것은 Oxlint와 Oxfmt입니다.

Rust를 선택한 이유까지 깊게 파고들 필요는 없습니다. Node 위에서 플러그인 체인을 길게 쌓는 구조와 달리, 네이티브에 가까운 실행으로 전체 스캔 시간을 줄이는 쪽에 설계 무게가 실려 있다고 보면 됩니다. 이 글에서 중요한 것은 그 설계가 제 커밋 훅·로컬 루프에서 체감되는가입니다.

독자 입장에서 기억할 한 가지: Oxc는 ESLint/Prettier를 통째로 대체할 수도 있고, 규칙 갭이 남는 구간만 레이어로 병행할 수도 있습니다. 만능 교체 선언이 아니라, 속도와 규칙 커버리지를 어떻게 나눌지의 문제에 가깝습니다.


2. 기존 린트·포맷 스택의 병목과 측정

이 장에서는 "왜 바꿨는지"와 "얼마나 달랐는지"에 대해 이야기합니다. 마이그레이션 방식과 룰 작성 전략은 다음 장을 참고해주세요.

2.1. husky 앞에서 느낀 병목 — typecheck는 그대로, lint·fmt가 끊던 루프

저는 프로젝트를 진행할 때, 항상 husky로 commit 직전에 lint·fmt·typecheck를 돌립니다. 이 글에서 다루는 개인 블로그와 Croffle은 Oxc로 바꾼 뒤에도 이 구조 자체는 같았습니다. CoSider는 아직 ESLint/Prettier 본류이지만, 같은 husky 전제에서 속도를 비교하는 실험 대상입니다.

tsc / vue-tsc로 도는 typecheck 시간은 전과 후가 크게 다르지 않았습니다. pre-commit의 동작 시간을 늘리던 쪽은 ESLint와 Prettier였습니다. 캐싱이 되어있지 않다면 짧게는 3초 안팎, 길게는 10초를 넘기기도 했고, Oxc(lint+fmt)로 바꾼 뒤에는 길어봐야 3~4초 안에서 끝나는 편이었습니다.

초 단위 차이만 보면 작아 보일 수 있습니다. 다만 커밋마다 반복되면 "한 번만 더 고치고 커밋"이 부담이 되고, 검사를 건너뛰고 싶은 유혹이 생깁니다. 이렇게 흐름이 끊기다보니 종종 commit을 하지 않은 채 작업을 진행한 뒤, commit을 여러 개로 나누어 해버리는 일도 잦았습니다. 저는 그 루프를 끊지 않으려고 검사 쪽 비용을 먼저 줄여보고자, Oxc를 도입해보기로 했습니다.

Note
이후 등장하는 값은 typecheck없이 eslint --fix ., prettier --write .와 같이 직접 명령어를 실행한 결과를 비교했습니다. 실제로 husky를 통해 커밋 전 동작하는 시간은 몇 초정도 더 소요됩니다.

2.2. Croffle에서 잰 숫자 — ESLint 4.21s, Oxlint 87ms

husky 전체 체감과 별개로, lint만 따로 잰 값이 있습니다. Croffle에서 ESLint/Prettier를 쓰던 당시 대상은 대략 110~130개 파일이었습니다. pnpm lint로 ESLint를 돌리면 평균 4.21s였고, 지금은 약 120개 파일로 oxlint가 평균 87ms 수준으로 끝납니다. 다음은 Croffle 프로젝트에서 commit전, 루트 디렉토리에서 lint와 format을 수행한 결과입니다.

sh
pnpm lint
# output:
# $ oxlint .
# Found 0 warnings and 0 errors.
# Finished in 80ms on 117 files with 214 rules using 20 threads.
pnpm format
# output:
# $ oxfmt --write .
# Finished in 750ms on 363 files using 20 threads.

수치는 환경·캐시·룰셋에 따라 달라질 수 있습니다. 여기서 말하는 것은 "항상 N배"가 아니라, 이 규모에서 lint 단독 비용이 초 단위에서 밀리초 단위로 내려갔다는 경험입니다.

2.3. CoSider 모노레포 실험 — 초 단위에서 밀리초로

팀 프로젝트인 @team-croffle/cosider는 turbo 기반 모노레포이고, 현재 본류 툴체인은 여전히 ESLint입니다. Oxc로 전환한 상태가 아닙니다. 대략 170개 파일 기준으로 현행 ESLint는 약 6.6s, Prettier는 약 107개 파일에 대해 약 3.1s였습니다.

속도가 궁금해서 임시 브랜치에 Oxc를 올려 보았습니다. oxlint는 170개 파일에 45ms, oxfmt는 209개 파일에 411ms였습니다. oxfmt 쪽은 당시 ignore가 적용되지 않아 파일 수가 더 많았습니다.

Note
CoSider 수치는 본류 전환 결과가 아니라 실험 브랜치 기준입니다. oxfmt는 ignore 미적용 상태라 비교 조건이 완전히 같지 않습니다.

파일이 더 많은 조건인데도 lint·fmt가 초 단위에서 수백 밀리초 이하로 내려가는 패턴은 Croffle과 같았습니다. 다만 팀이 바로 Oxc로 갈아탄 것은 아니며, 이 장은 "가능성"을 확인한 기록에 가깝습니다.


3. 도입 경로 두 가지 — 마이그레이션 vs 룰 직접 작성

ESLint 설정을 Oxc로 옮기는 마이그레이션 툴을 사용하면 비교적 쉽게 .oxlintrc.json을 빌드할 수 있습니다. 몇몇 에러가 있을 수는 있지만 기본적으로 잘 동작합니다. 문제는 빌드된 .oxlintrc.json 결과물입니다. 제가 겪기로는 lint 설정만 대략 850라인까지 불어났고, 당장 쓰지 않을 룰까지 같이 들어왔습니다.

설정이 그 정도로 커지면, 룰을 하나 고치거나 추가할 때 "이미 있는 항목과 충돌하지 않는가"를 체크하기 어렵습니다. 속도는 얻었는데 규칙 통제권을 잃은 셈입니다.

Croffle에서는 그 경로를 채택하지 않았습니다. ESLint를 마이그레이션으로 이식하지 않고 제거한 뒤, Oxc 쪽에 필요한 룰만 직접 작성했습니다. 팀에서 유지보수하는 프로젝트였기 때문에, 추가·수정할 때 충돌을 점검하기 쉽게 유지하는 쪽이 더 중요했습니다.

반대로, 룰을 거의 손대지 않고 속도와 파이프라인만 바꾸고 싶다면 마이그레이션만으로도 충분하다고 봅니다. "어떤 경로가 정답인가"보다 앞으로 룰을 얼마나 다듬을지가 기준에 가깝습니다.

Warning
ESLint → Oxc 마이그레이션 결과물이 수백 라인으로 비대해지면, 이후 룰 변경 비용이 커질 수 있습니다. 속도만 보고 설정을 방치하지 않는 편이 낫습니다.

4. Oxlint·Oxfmt·에디터 — 양쪽 프로젝트의 공통 운용

개인 블로그와 Croffle 모두 Oxlint와 Oxfmt를 함께 씁니다. 로컬에서는 Cursor IDE의 Oxc extension으로 편집 중에도 같은 축의 피드백을 받고, commit 전에는 husky가 기존과 같이 검사를 돌립니다.

sh
git commit -m "feat: drizzle schema"
# husky output:
# $ pnpm -r --parallel run typecheck
# Scope: 3 of 4 workspace projects
# apps/desktop typecheck$ tsc --noEmit -p tsconfig.node.json --composite false && vue-tsc --noEmit -p tsconfig.web.json --composite false
# packages/cli typecheck$ tsc --noEmit -p tsconfig.json
# packages/cli typecheck: Done
# apps/desktop typecheck: Done
# ⋯ Backing up original state…
# ✔ Done backing up original state (62ef740)!
# ⋯ Running tasks for staged files…
#     *.{js,jsx,mjs,ts,tsx,mts,cts,vue} — 9 files
#       ⋯ oxlint --fix
#       ⋯ oxfmt --write
# 
# ✔ oxlint --fix
# ✔ oxfmt --write
# 
# ✔ Done running tasks for staged files!
# ⋯ Staging changes from tasks…
# ✔ Done staging changes from tasks!
# ⋯ Cleaning up temporary files…
# ✔ Done cleaning up temporary files!

# 소요시간(typecheck 포함): 약 5.9s
Cursor Oxc extension 설정/동작 화면
Cursor Oxc extension 설정/동작 화면

4.1. Oxfmt는 아직 베타다

Oxlint와 달리, Oxfmt는 글을 쓰는 시점 기준으로 아직 베타입니다. 공식 발표(Oxfmt Beta) 이후 Prettier 호환·안정화 쪽으로 계속 올라가고 있지만, stable 전 단계라는 점은 도입 전에 분명히 두는 편이 좋습니다.

베타라는 말이 곧 "쓰면 안 된다"는 뜻은 아닙니다. 저는 개인 블로그와 Croffle처럼 빠른 피드백이 더 중요한 저장소에서는 Prettier를 Oxfmt로 바꿔 쓰고 있습니다. 다만 아래는 감수할 전제로 둡니다.

  • 버전을 올릴 때 포맷 결과가 미묘하게 바뀔 수 있다.
  • Prettier 플러그인·특수 포맷(임베디드 언어 등) 쪽은 로드맵에 남아 있는 구간이 있다.
  • 팀 전체가 포맷 결과에 민감하거나, 플러그인 의존이 크면 레이어드(Prettier 유지 + Oxlint만 Oxc) 가 더 안전할 수 있다.

그 위험을 감수할 수 있었던 이유 중 하나는, 제가 Prettier 쪽에서 사실상 필수에 가깝게 쓰던 Tailwind CSS 클래스 정렬이 Oxfmt에 내장되어 있었고, 그 외 사용하던 플러그인은 딱히 없었기 때문입니다. prettier-plugin-tailwindcss 같은 별도 플러그인에 묶여 있었다면 oxfmt의 도입이 어렵다고 판단했겠지만, 내장이 되어있으면 "플러그인 공백" 부담이 줄고, 속도 이득을 보는 쪽의 손익이 맞다고 판단했습니다.

Warning
Oxfmt는 베타입니다. husky·CI에 넣을 수는 있지만, "Prettier와 영원히 동일한 출력"을 전제로 두지 않는 편이 맞습니다. 포맷 드리프트가 부담이거나, Tailwind 정렬 외에 꼭 필요한 Prettier 플러그인이 남아 있으면 fmt만 Prettier에 남겨도 됩니다.

typecheck는 여전히 tsc / vue-tsc에 맡깁니다. Oxc가 타입 검사까지 대체했다고 쓰지 않습니다. 역할 분리는 "포맷·정적 규칙 / 타입"으로 나눈 상태로 두는 편이 제게는 명확했습니다.

설정 파일의 구체적인 경로와 스크립트 이름은 저장소마다 다르니, 블로그는 my-blog를 기준으로 보는 것이 좋습니다.

직접 작성한 oxlintrc 설정 일부
직접 작성한 oxlintrc 설정 일부

5. 프로젝트 적용기 — 개인 블로그 첫 도입

Oxc를 처음 넣은 곳은 본 블로그입니다. Nuxt SSR 기반 개인 블로그이고, lint·fmt 쪽을 Oxc로 옮기며 툴링을 단순화하는 쪽이 목적이었습니다. 저장소는 https://github.com/bluenyang/my-blog입니다.

블로그 쪽 룰 구성은 Oxc에서 제공하는 ESLint 마이그레이션 툴을 사용했고, 847라인의 .oxlint.json이 빌드되었습니다. 속도 체감은 #2.1과 같은 패턴이었고, 세부 벤치 숫자는 Croffle·CoSider 쪽에 모아 두었습니다.

블로그는 잘못된 코드를 수정하는 커밋이 많은 편이었기 때문에 커밋 주기가 짧고 많았습니다. 그리고 이때 Oxc의 첫 도입에서 얻은 것은 "동작한다"보다 커밋 훅을 유지한 채 대기 시간을 줄일 수 있다는 확신이었습니다.


6. 프로젝트 적용기 — Croffle: 제거 후 갈아타기

Croffle은 Electron 등으로 이어 온 일정 관리 SW이고, 최근에는 다른 프로젝트 일정으로 혼자 유지보수 중이지만 기본적으로 5인 팀 협업으로 개발 중입니다. 최근 리팩토링 과정에서 ESLint를 걷어낸 뒤, .eslint.config.mjs에서 작성한 규칙을 토대로 Oxc로 교체했습니다. #3에서 말한 것처럼 마이그레이션 산출물(~850라인, 불필요 룰 포함)을 그대로 쓰지 않고, 필요한 규칙만 남기는 쪽을 골랐습니다.

팀원이 설정을 읽고 규칙을 바꿀 수 있어야 하므로, 설정 파일이 "동작하는 덤프"보다 읽을 수 있는 허용 목록에 가까운 편이 낫습니다. 규칙을 바꿀 일이 생기면 충돌을 추적할 수 있어야 하고, 그게 안 되면 결국 검사를 느슨하게 두거나 우회하게 됩니다.

lint 단독 속도는 #2.2의 4.21s → 87ms가 그 결과의 한 단면입니다. 에디터 extension과 husky까지 맞춰 두면, 로컬에서 보는 기준과 커밋 직전 기준이 크게 어긋나지 않습니다.


7. 생태계가 덜 성숙할 때의 선택 — 레이어드 운용

Oxc 생태계는 아직 ESLint/Prettier만큼 플러그인·규칙 커버리지가 무겁지 않습니다. 특히 Oxfmt가 베타인 점(#4.1)까지 겹치면, "lint는 Oxc, fmt는 Prettier"처럼 층을 나누는 선택이 더 설득력 있습니다. 프로젝트에 꼭 필요한 규칙이나 포맷 동작이 Oxc만으로 안 채워지면, Oxc와 ESLint/Prettier를 레이어로 나누면 됩니다.

예를 들어 빠른 피드백이 필요한 검사는 Oxlint에 두고, 포맷 안정성이나 플러그인이 필요한 구간만 Prettier/ESLint에 남기는 식입니다. "전부 버리거나 전부 갈아타기"가 유일한 답은 아닙니다.

속도 이득과 규칙 커버리지는 교환 관계에 가깝습니다. 레이어를 늘리면 설정과 CI 단계가 다시 복잡해지므로, 갭이 실제로 발목을 잡을 때만 검토하는 편이 맞습니다. 이 글의 블로그·Croffle 적용기는 전면 교체 쪽에 가깝고, 레이어드는 필요할 때 쓸 수 있는 선택지로 남겨 둡니다.


결론

Oxlint/Oxfmt는 제게 "트렌드 교체"가 아니라 husky 앞에서의 대기lint 단독 비용을 줄이는 선택이었습니다. Croffle에서는 4.21s 급이던 ESLint가 87ms 근처로 내려갔고, CoSider 실험에서도 초 단위 검사가 수십~수백 밀리초 구간으로 내려가는 것을 확인했습니다.

다만 설정 전략은 하나만이 아닙니다. 룰을 거의 건드리지 않을 거면 마이그레이션으로 충분할 수 있고, 룰을 계속 통제할 거면 Croffle처럼 필요한 항목만 직접 쓰는 편이 낫습니다. Oxfmt는 아직 베타라는 전제를 두되, 제 경우에는 Tailwind 클래스 정렬이 내장되어 있어 플러그인 공백 부담이 작았습니다. 규칙·포맷 갭이 남으면 Oxc와 ESLint/Prettier를 레이어로 나누는 여지도 있습니다.

검사는 부담 없이 자주 돌아가야 의미가 있습니다. 그 조건을 맞추는 도구로 Oxc를 쓰는 중이고, 숫자와 운용 방식은 위 경험 범위 안에서만 이야기했습니다.

BlueNyang
작성자BlueNyang
라이선스
CC BY NC
BlueNyang

BlueNyang

BlueNyang의 개발 log

카테고리

  • Development
  • Framework
  • Language
  • Dev Tools
  • DevOps & Infra
  • Studies

페이지

© 2026 BlueNyang. All rights reserved.

Made with Nuxt.js and Directus