JS/TS 프로젝트에 코드 품질 도구를 붙이려면 보통 두 개를 따로 설정합니다. 포맷은 Prettier, 린트는 ESLint, 거기에 eslint-plugin-prettier로 둘이 충돌하지 않게 연결하고, TypeScript를 쓰면 @typescript-eslint/parser까지 추가됩니다. 설정 파일이 여러 개로 늘어나는 것도 번거롭지만, 저장할 때마다 포맷터와 린터가 각각 파일을 파싱하느라 느려지는 게 더 크게 느껴집니다. Biome은 이 둘을 Rust로 다시 구현해 하나의 바이너리로 합친 도구입니다.
핵심 개념
- 단일 설정, 단일 바이너리:
biome.json하나로 포맷터·린터·import 정리(assist)를 전부 제어합니다. Node.js 없이도 동작하는 네이티브 바이너리라, CI에서npm install단계를 거치지 않고도 쓸 수 있습니다. - 포맷터는 Prettier 호환을 목표로 함: 공식 저장소는 Prettier와 97% 호환된다고 밝히고 있습니다. 완전히 같지는 않아서, 기존 Prettier 설정이 있던 프로젝트라면 마이그레이션 직후 diff가 예상보다 커질 수 있습니다.
- 린터는 500개 이상 규칙을 ESLint·
typescript-eslint·관련 플러그인에서 포팅해왔고, 수정 가능한 규칙은biome check --write로 한 번에 고칩니다. - 타입 인식 규칙은
tsc없이 자체 추론 엔진으로 처리합니다. 다만noFloatingPromises처럼 모듈 그래프 전체를 봐야 하는 규칙은 프로젝트 스캐너가 의존성까지 훑어야 정확하게 동작하고, 느린 의존성은files.experimentalScannerIgnores로 제외할 수 있습니다. - 모노레포는 2.0부터 정식 지원합니다. 루트
biome.json에"root": true를 두고, 각 패키지 설정에서"extends": "//"를 쓰면 루트 설정을 그대로 물려받습니다. - 아직 불안정한 규칙은
nursery그룹에 따로 모아둡니다. 안정화 전까지는 기본값(recommended프리셋)에 포함되지 않고, 직접 설정에 추가해야 켜집니다.
실전 예시
설치는 dev 의존성 하나로 끝납니다.
npm install --save-dev --save-exact @biomejs/biome
npx @biomejs/biome init
init이 만들어주는 기본 설정은 이렇습니다.
{
"$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"linter": {
"enabled": true,
"rules": {
"preset": "recommended"
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
문제가 있는 파일 하나로 테스트해봤습니다.
// src/sync.js
import { fetchItems } from "./api.js"
import { unused } from "./util.js"
async function syncItems() {
const items = fetchItems();
for (var i = 0; i < items.length; i++) {
console.log(items[i])
}
}
syncItems()
biome check를 돌리면 포맷 문제와 린트 문제를 한 번에 보여줍니다.
src/sync.js:2:8 lint/correctness/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! This import is unused.
1 │ import { fetchItems } from "./api.js"
> 2 │ import { unused } from "./util.js"
│ ^^^^^^^^^^
i Unsafe fix: Remove the unused imports.
src/sync.js:6:10 lint/correctness/noInnerDeclarations ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
× This var should be declared at the root of the enclosing function.
4 │ async function syncItems() {
5 │ const items = fetchItems();
> 6 │ for (var i = 0; i < items.length; i++) {
│ ^^^^^^^^^
Found 2 errors.
Found 1 warning.
--write를 붙이면 포맷은 바로 고치지만, 미사용 import 제거는 "unsafe fix"로 분류돼 있어서 기본값으로는 건드리지 않습니다.
npx @biomejs/biome check --write src/sync.js
// 포맷만 정리된 결과 — import는 그대로 남아 있음
import { fetchItems } from "./api.js";
import { unused } from "./util.js";
async function syncItems() {
const items = fetchItems();
for (var i = 0; i < items.length; i++) {
console.log(items[i]);
}
}
syncItems();
정말 지워버리길 원하면 --unsafe 플래그를 명시해야 합니다. 안전하지 않다고 분류된 수정을 자동화 스크립트가 조용히 적용해버리는 사고를 막기 위한 구분으로 보입니다.
타입 인식이 필요한 noFloatingPromises는 recommended에 없어서 직접 켜야 합니다.
{
"linter": {
"rules": {
"preset": "recommended",
"nursery": {
"noFloatingPromises": "error"
}
}
}
}
// src/float.ts
async function syncItems() {
fetchItems();
}
async function fetchItems() {
return [];
}
src/float.ts:2:3 lint/nursery/noFloatingPromises FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
× A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.
1 │ async function syncItems() {
> 2 │ fetchItems();
│ ^^^^^^^^^^^^^
3 │ }
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future.
i Unsafe fix: Add await operator.
tsconfig.json도 typescript 패키지도 없는 상태에서 나온 결과입니다. 함수 시그니처만 보고 반환형이 Promise라는 걸 추론해 잡아낸 셈입니다.
비슷한 도구와 비교
| 항목 | Biome | ESLint + Prettier | Oxlint |
|---|---|---|---|
| 역할 | 포맷 + 린트 + import 정리 통합 | 포맷(Prettier)과 린트(ESLint) 분리 | 린트만(포맷터 없음) |
| 구현 언어 | Rust | JavaScript | Rust |
| 설정 파일 | biome.json 하나 |
.eslintrc, .prettierrc 등 여러 개 |
.oxlintrc.json |
| 규칙 수 | 500개 이상 | 플러그인에 따라 수천 개까지 확장 가능 | Biome보다 적음, 빠르게 추가 중 |
| 생태계 성숙도 | 빠르게 성장 중, 일부 규칙은 nursery로 유동적 |
가장 오래되고 플러그인 생태계가 넓음 | 가장 최근 시작, VoidZero(Vite 팀) 주도 |
ESLint는 플러그인이 압도적으로 많아서 프레임워크별 세세한 규칙(예: 특정 상태관리 라이브러리 전용 린트)이 필요하면 아직 ESLint 쪽이 유리합니다. Oxlint는 린트 속도만 보면 더 빠르다는 평가가 많지만, 포맷터가 없어서 Prettier나 Biome 포맷터를 따로 붙여야 합니다. 세 도구를 같이 쓰는 조합(Oxlint로 빠른 1차 검사 후 ESLint로 나머지 규칙 보완)도 실제로 쓰이는 걸 봤습니다.
넘어갈 때 걸리는 부분
기존 ESLint·Prettier 설정이 있다면 biome migrate eslint, biome migrate prettier로 상당 부분 자동 변환됩니다. 다만 커스텀 플러그인 규칙이나 Biome이 아직 포팅하지 않은 규칙은 변환되지 않고 건너뛰므로, 마이그레이션 뒤에는 biome.json을 열어서 비어 있는 부분이 없는지 한 번 확인해야 합니다.
VSCode 확장을 쓰는 경우 ESLint·Prettier 확장을 동시에 켜두면 저장할 때 포맷이 두 번 겹쳐 적용되면서 충돌할 수 있습니다. Biome 확장을 기본 포맷터로 지정하고 나머지는 꺼두는 게 안전합니다.