Biome을 팀에 도입할 때: biome.json 규칙 설정, Import 정렬, Git Hooks 연동

이 글의 핵심

도구를 바꾸는 것보다 어려운 일은 팀 전체가 같은 규칙을 지키게 만드는 것입니다. 훅이 길어지면 --no-verify가 남용되는 문제, 첫 도입 때 포맷 변경만 따로 커밋하는 이유, 모노레포에서의 설정 분리와 코드 리뷰 체크리스트를 다뤄 Biome 전환 뒤 규칙을 누가 어떻게 관리할지까지 정할 수 있게 합니다.

이 글의 핵심

Biome은 프런트엔드·풀스택 저장소에서 따로 쓰던 린터와 포맷터, 그리고 임포트 정리를 하나의 CLI와 설정 파일로 묶는 도구입니다. Rust로 구현되어 파싱부터 규칙 적용, 출력 포맷까지 하나의 네이티브 바이너리에서 처리하므로, 같은 저장소를 도구마다 여러 번 훑지 않아도 됩니다. 이 점이 로컬 개발과 CI 시간을 함께 줄여 줍니다.

기본 사용법은 Biome으로 ESLint·Prettier 대체하기: Rust 파서·규칙 엔진 구조와 에디터·CI 통합에서 다뤘으므로, 이 글은 팀에 도입할 때 필요한 설정, 훅 연동, 마이그레이션, 규칙 운영에 집중합니다.


Biome의 핵심 개념

단일 엔진과 일관성

Biome은 한 엔진이 진단과 포맷을 모두 담당합니다. ESLint와 Prettier를 함께 쓰던 시절에는 스타일 관련 ESLint 규칙과 Prettier의 출력이 충돌해, 린트 자동 수정 후 포맷을 돌리면 다시 린트가 실패하는 순환이 생기곤 했습니다. 이를 막으려고 eslint-config-prettier로 충돌 규칙을 끄는 설정이 따로 필요했습니다. Biome은 같은 도구 안에서 이 경계를 정하므로, 팀은 biome.json 한곳에서 스타일과 규칙 심각도를 맞추면 됩니다.

네이티브 바이너리가 주는 실무적 의미

Biome은 플랫폼별 네이티브 바이너리로 배포되므로 Node.js 버전과 무관하게 같은 CLI를 고정하기 쉽고, CI에서 수많은 플러그인 패키지를 로드하는 시작 비용도 없습니다. 다만 기존 ESLint 설정을 읽어 들이는 migrate eslint 명령은 extends와 플러그인을 해석하기 위해 Node.js가 필요합니다. 어떤 명령이 Node를 요구하는지 팀 문서에 적어 두면 CI 이미지를 줄일 때 헷갈리지 않습니다.

LSP와 에디터 통합

Biome은 Language Server Protocol로 VS Code, JetBrains 등에 진단·수정·포맷을 제공합니다. CLI가 강제하는 규칙과 에디터가 보여 주는 메시지가 어긋나지 않으려면 @biomejs/biome를 프로젝트 로컬 의존성으로 고정하고, 에디터 확장이 전역 설치 바이너리가 아니라 그 로컬 바이너리를 쓰도록 해야 합니다.

Rust 기반 파서와 CST

Biome의 파서는 rust-analyzer의 rowan을 포크한 구현 위에서 Green 트리와 Red 트리 패턴을 사용합니다. 핵심 자료구조는 전통적인 AST가 아니라 CST(Concrete Syntax Tree)입니다. CST는 키워드와 식뿐 아니라 공백·줄바꿈·주석처럼 소스를 그대로 복원하는 데 필요한 트리비아(trivia)를 트리에 남깁니다. 각 토큰은 앞뒤의 트리비아를 갖고 있어서, 포맷터가 원문을 어떻게 다시 쓸지 결정할 재료가 같은 트리 안에 있습니다.

파서는 문법 오류가 있어도 가능한 한 나머지 구문을 계속 해석하도록 설계되어 있습니다. 복구할 수 없는 구간은 Bogus 노드로 격리해, 깨진 구문이 뒤쪽 분석 단계로 퍼지지 않게 합니다. 에디터에서는 저장 직후나 리팩터링 도중처럼 코드가 반쯤 깨진 상태에서도 진단이 나와야 하므로, 이 성질이 사용 경험과 직결됩니다. 린트와 포맷이 같은 구문 트리를 공유한다는 점이 두 도구를 따로 쓸 때의 이중 파싱과 규칙 충돌을 줄이는 근거입니다.

린트 규칙 엔진

린트 규칙은 구문 트리를 순회하며 패턴을 찾는 방식으로 동작합니다. 구문만 보는 규칙도 있고, 스코프와 심볼 바인딩 같은 시맨틱 모델이 필요한 규칙도 있습니다. 예를 들어 “선언되지 않은 변수 사용”을 찾으려면 이 이름이 어느 선언에 묶이는지를 알아야 합니다.

각 규칙은 진단과 선택적인 코드 수정을 내고, 규칙들은 style, suspicious, correctness, a11y 같은 그룹으로 묶여 있습니다. 팀은 biome.json에서 권장 세트를 켜고 개별 규칙의 심각도를 조정합니다. 규칙 ID를 PR 설명이나 예외 주석에 함께 적는 습관을 들이면, 어떤 규칙이 무엇을 금지하는지가 팀의 공통 언어가 되어 예외 승인 절차가 단순해집니다.

포맷터와 Prettier 호환

포맷터는 CST의 구조와 트리비아를 입력으로 받아 정해진 규칙에 따라 소스를 다시 출력합니다. 들여쓰기, 줄 길이, 따옴표, 세미콜론 등은 formatter와 javascript.formatter에서 조정합니다.

Biome은 Prettier 출력과의 호환을 설계 목표로 삼고 있어, 대부분의 코드에서 Prettier와 같은 결과를 냅니다. 하지만 특정 Prettier 버전이나 플러그인, 드문 엣지 케이스까지 바이트 단위로 같다고 기대하기는 어렵습니다. 도입할 때는 전체 포맷을 한 번에 적용하고 남는 차이는 그대로 받아들이는 편이 현실적입니다. 주석 위치나 줄바꿈만 바뀌어도 리뷰 부담이 커지므로, 첫 적용은 로직 변경과 섞지 말고 포맷 전용 커밋으로 분리합니다. 그 커밋 해시를 .git-blame-ignore-revs에 넣어 두면 git blame에서도 포맷 변경이 가려집니다.

데몬과 워크스페이스

Biome은 데몬과 클라이언트 구조를 쓸 수 있습니다. 백그라운드에 떠 있는 서버가 에디터 요청을 처리하고, 워크스페이스가 열린 문서와 파싱·분석 결과를 세션 동안 유지하므로 저장할 때마다 처음부터 다시 읽지 않습니다. 빌드 시스템의 증분 컴파일과는 다르지만, 짧은 주기로 반복되는 편집·저장·진단에서 지연을 줄인다는 방향은 같습니다.

Biome 2에서는 파일을 열기 전에 프로젝트 구조를 훑는 스캐너가 추가되었습니다. 공식 문서에 따르면 noImportCycles처럼 여러 파일을 봐야 하는 project 도메인 규칙을 켜지 않으면 스캐너는 세션에 필요한 폴더와 설정 파일만 탐색합니다. 반대로 project 규칙을 켜면 모듈 그래프를 만들기 위해 저장소 전체를 색인하므로, 대규모 모노레포에서는 이 비용을 감안해 필요할 때만 켜는 것이 좋습니다.


설치와 기본 명령

프로젝트 루트에서 개발 의존성으로 설치하고 초기 설정 파일을 만듭니다. --save-exact로 버전을 고정하는 것이 공식 권장 사항입니다. Biome은 패치 버전에서도 규칙 동작이나 포맷 결과가 바뀔 수 있어서, 범위 지정(^)으로 두면 팀원마다 결과가 달라질 수 있기 때문입니다.

npm install --save-dev --save-exact @biomejs/biome
npx @biomejs/biome init

자주 쓰는 npm 스크립트는 다음과 같습니다.

{
  "scripts": {
    "check": "biome check .",
    "check:write": "biome check --write .",
    "format": "biome format --write .",
    "lint": "biome lint .",
    "ci": "biome ci ."
  }
}

biome check는 포맷 검사, 린트, 임포트 정리 같은 assist 액션을 한 번에 실행하는 상위 명령이고, --write를 붙이면 안전한 수정까지 적용합니다. biome ci는 파일을 고치지 않고 검증만 하며, 문제가 있으면 0이 아닌 코드로 종료해 파이프라인을 막습니다. CI 환경에서 쓰기 좋게 출력 형식과 병렬 처리가 조정되어 있고, GitHub Actions에서는 진단을 PR 주석 형태로 보여 줍니다. biome lint와 biome format은 각각 한 가지 역할만 실행하고 싶을 때 씁니다.


biome.json 설정

스키마와 최소 구성

$schema에는 설치한 버전에 맞는 스키마 URL을 지정하거나 ./node_modules/@biomejs/biome/configuration_schema.json을 가리켜 에디터 자동완성을 받을 수 있습니다. 아래는 Biome 2.x에서 출발점으로 쓸 만한 구성입니다.

{
  "$schema": "https://biomejs.dev/schemas/2.4.12/schema.json",
  "files": {
    "ignoreUnknown": false,
    "includes": ["**", "!**/dist", "!**/build", "!**/.next", "!**/coverage"]
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "assist": {
    "enabled": true,
    "actions": {
      "source": {
        "recommended": true
      }
    }
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double",
      "semicolons": "always",
      "trailingCommas": "all"
    }
  }
}

Biome 2의 files.includes는 포함 패턴과 제외 패턴을 한 배열에 함께 씁니다. **로 전체를 포함한 뒤 !로 시작하는 패턴으로 빼는 방식입니다. !는 린트·포맷 대상에서만 제외하고, !!는 스캐너의 색인 대상에서도 강제로 제외합니다. 빌드 산출물처럼 다른 파일이 import하지 않는 디렉터리라면 !!로 완전히 빼서 스캔 비용까지 줄일 수 있지만, project 규칙이 타입 정의 같은 파일을 참조해야 한다면 !로만 제외해야 합니다. Biome 1.x의 ignore/include 필드는 2.0에서 includes 하나로 통합되었으므로, 예전 예제를 복사할 때는 biome migrate --write로 설정을 변환하는 것이 안전합니다.

VCS와 무시 규칙

로컬과 CI에서 분석 대상이 달라지면 “내 컴퓨터에서는 통과”하는 문제가 생깁니다. Git 저장소에서는 vcs 블록으로 .gitignore를 그대로 따르게 합니다.

{
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  }
}

overrides로 경로별 완화

테스트, 스토리북, 레거시 폴더만 규칙을 완화하려면 overrides를 씁니다. Biome 2에서는 각 항목의 경로 필드도 includes입니다.

{
  "overrides": [
    {
      "includes": ["**/*.test.ts", "**/__tests__/**"],
      "linter": {
        "rules": {
          "suspicious": {
            "noExplicitAny": "off"
          }
        }
      }
    }
  ]
}

린트와 포맷

역할 구분

포맷터는 공백, 줄바꿈, 따옴표처럼 표현 형식만 통일하고 코드의 의미는 바꾸지 않습니다. 린터는 버그 가능성이 있는 패턴이나 팀 컨벤션 위반을 진단하며, 일부 규칙은 자동 수정을 제공합니다. 팀 문서에 “형식은 포맷터에 맡기고 리뷰에서 논의하지 않는다, 논리 규칙은 린트 심각도로 관리한다” 정도의 경계를 적어 두면 코드 리뷰에서 스타일 논쟁이 줄어듭니다.

check 한 번에 묶는 이유

로컬에서는 biome check --write .로 포맷·린트·임포트 정리를 한 번에 적용하고 커밋하는 흐름이 효율적입니다. 세 가지를 따로 돌리면 실행 순서에 따라 결과가 달라질 여지가 생기기 때문입니다. CI에서는 biome ci처럼 파일을 바꾸지 않는 경로를 써서, CI가 코드를 고치는 일이 없도록 합니다.

안전한 수정과 그렇지 않은 수정

Biome의 자동 수정은 safe와 unsafe로 나뉩니다. safe 수정은 의미를 바꾸지 않는다고 보장되는 것이라 --write가 바로 적용하고, unsafe 수정은 동작이 바뀔 수 있어 --write --unsafe를 명시해야 적용됩니다. 예를 들어 ==를 ===로 바꾸는 수정은 비교 대상의 타입에 따라 결과가 달라질 수 있으므로 unsafe입니다. unsafe 수정은 한 번에 저장소 전체에 적용하기보다 범위를 나눠 적용하고 테스트를 돌리는 편이 안전합니다.


Import 정렬

Assist와 소스 액션

Biome 2.x에서 임포트 정리는 린트 규칙이 아니라 assist의 소스 액션(organizeImports)으로 제공됩니다. 위 biome.json의 assist.actions.source.recommended가 이를 켭니다. biome check는 assist 액션도 검사하므로, 임포트 순서가 정리되지 않은 파일은 check와 ci에서 실패하고 check --write로 정리됩니다. 즉 임포트 순서도 포맷과 같은 수준으로 강제됩니다. 팀에서 이를 원하지 않으면 organizeImports를 명시적으로 끄면 됩니다.

VS Code에서 저장 시 정리

에디터에서 저장할 때 맞추려면 codeActionsOnSave에 Biome 액션을 넣습니다. 팀 전원이 같은 설정을 쓰도록 .vscode/settings.json을 저장소에 커밋하는 방식이 흔합니다.

{
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  }
}

source.fixAll.biome은 safe 수정만 적용합니다. 임포트 정리를 처음 켜면 거의 모든 파일의 임포트 순서가 바뀌므로, 이것도 포맷 마이그레이션 커밋에 함께 넣어 한 번에 끝내는 것이 좋습니다. 그렇지 않으면 이후 PR마다 건드린 파일의 임포트 순서 변경이 섞여 들어갑니다.


Git Hooks 통합

Git 훅은 커밋 직전에 최소한의 품질 검사를 거는 장치입니다. Biome과 흔히 조합하는 도구는 Husky + lint-staged, Lefthook, pre-commit 프레임워크입니다. 어느 쪽이든 원칙은 같습니다. 스테이징된 파일만 대상으로 해서 시간을 줄이고, 훅에서 자동 수정을 허용할지 검증만 할지를 팀이 정하며, CI의 biome ci와 같은 설정·버전을 쓰게 해서 로컬에서 통과한 코드가 CI에서 실패하는 일을 막습니다.

Husky와 lint-staged

lint-staged는 스테이징된 파일 경로만 명령 인자로 넘기고, 명령이 파일을 수정하면 수정된 내용을 자동으로 다시 스테이징합니다.

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx,json,css}": ["biome check --write --no-errors-on-unmatched"]
  }
}

--no-errors-on-unmatched는 넘겨받은 파일이 모두 Biome 처리 대상에서 제외된 경우(예: files.includes에서 뺀 경로) 오류로 끝나지 않게 합니다. Husky의 pre-commit 훅에서 npx lint-staged를 호출하는 구성이 전제입니다.

Lefthook

YAML로 훅을 선언하고 싶다면 Lefthook을 씁니다.

# lefthook.yml
pre-commit:
  commands:
    biome:
      glob: "*.{js,jsx,ts,tsx,json,jsonc,css}"
      run: npx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}
      stage_fixed: true

lint-staged와 달리 Lefthook은 훅이 고친 파일을 기본으로 다시 스테이징하지 않습니다. stage_fixed: true가 없으면 Biome이 파일을 고쳐도 커밋에는 고치기 전 내용이 들어가고, 수정분은 작업 디렉터리에 남습니다. 커밋은 통과했는데 CI에서 포맷 오류가 나는 경우 이 옵션이 빠진 것을 의심해 볼 만합니다.

훅을 운영할 때

훅이 느려지면 개발자들이 --no-verify로 건너뛰기 시작하고, 그 순간 훅은 형식적인 장치가 됩니다. 스테이징된 파일만 검사하고, 저장소 전체를 봐야 하는 검사는 CI로 보내 훅을 짧게 유지하는 것이 중요합니다. 훅이 실패했을 때 어떤 명령으로 고치면 되는지를 기여 가이드에 한 줄로 적어 두면 신규 팀원이 덜 헤맵니다.


ESLint / Prettier 마이그레이션

자동 마이그레이션

Biome은 기존 설정을 읽어 biome.json에 반영하는 명령을 제공합니다.

npx @biomejs/biome migrate eslint --write
npx @biomejs/biome migrate prettier --write

ESLint 마이그레이션은 레거시 .eslintrc와 flat config를 모두 읽지만, 앞서 말했듯 extends와 플러그인을 해석하려면 Node.js가 필요하고 YAML 형식 설정은 지원하지 않습니다. Prettier 마이그레이션도 JSON이나 JS 설정에서 가장 잘 동작하므로, JSON5·YAML·TOML 설정은 JSON으로 바꾼 뒤 실행하는 편이 낫습니다. ESLint 규칙 중 Biome에 대응하는 것이 없는 규칙은 옮겨지지 않으며, --include-inspired 옵션을 주면 ESLint 규칙에서 영감을 받았지만 동작이 완전히 같지는 않은 규칙까지 함께 켭니다.

규칙 1:1 대응을 기대하지 않기

Biome 규칙 이름은 ESLint 규칙 이름과 다르고, 대응 규칙이 있어도 세부 동작이 다를 수 있습니다. 대응하는 규칙이 아예 없는 플러그인 규칙도 있습니다. 마이그레이션 직후 biome check 전체 결과를 보고 켤 규칙을 팀에서 다시 정하는 것이 현실적입니다. 경고가 지나치게 많으면 recommended에서 시작해 그룹 단위로 단계적으로 늘립니다.

주석과 점진적 전환

Biome은 eslint-disable 주석을 인식하지 않으므로, 그런 주석으로 끄던 코드는 // biome-ignore lint/suspicious/noExplicitAny: 사유 같은 Biome 주석으로 옮겨야 합니다. Biome의 억제 주석은 사유를 반드시 적게 되어 있어, 옮기는 과정에서 그동안 이유 없이 꺼 두었던 규칙이 드러납니다. 한 번에 정리하기 어렵다면 디렉터리 단위로 나누어 옮깁니다. 전환 기간에 ESLint를 병행할 수는 있지만, 오래 두면 같은 문제를 두 도구가 다르게 보고해 혼란이 커지므로 종료 시점을 정해 두는 것이 좋습니다.


팀 규칙 운영

버전과 심각도 정책

@biomejs/biome 버전을 정확히 고정하고, Biome을 올리는 일은 별도 PR로 처리해 그 PR에서 생긴 포맷·진단 변화만 검토합니다. 심각도도 정책이 필요합니다. error는 CI를 막는 것, warn은 정리할 기한이 정해진 것으로 정해 두지 않으면 경고가 쌓이기만 하고 아무도 보지 않게 됩니다. overrides로 특정 폴더 규칙을 완화할 때도 누가 승인했고 언제 걷어 낼지를 주석이나 이슈로 남겨 두지 않으면 예외가 계속 누적됩니다.

모노레포 설정

루트 biome.json에 포맷·린트 기본값과 files.includes를 두고, 패키지마다 중첩 biome.json으로 필요한 부분만 덮어쓰는 구성이 흔합니다. Biome 2에서 중첩 설정 파일은 "root": false를 지정해야 하고, "extends": "//"로 루트 설정을 상속할 수 있습니다. 공유 프리셋을 npm 패키지로 배포해 extends로 가져오는 경우에는 그 패키지 버전도 고정해야, 로컬과 CI가 서로 다른 설정을 읽는 일이 없습니다.

Turborepo나 Nx로 태스크를 나누는 저장소라면 biome check를 루트 스크립트 하나로 두는 편이 단순합니다. Biome 자체가 충분히 빨라 패키지별로 나눌 이득이 크지 않은 경우가 많고, 패키지별로 나누면 캐시 키에 lockfile, Biome 버전, 루트와 패키지의 biome.json을 모두 넣어야 정확해집니다. CI에서 변경된 패키지만 검사하면 빠르지만 루트 설정 변경 같은 경우를 놓칠 수 있으므로, 최종 단계에서 루트 기준 biome ci를 한 번 더 돌릴지 여부를 명시적으로 정해 둡니다.

기여 가이드

기여 가이드에는 로컬 명령(npm run check:write), CI 명령(biome ci), 설치할 에디터 확장, 훅이 실패했을 때의 대처를 적어 둡니다. 도구 설정 파일만으로는 팀원이 무엇을 언제 실행해야 하는지까지 전달되지 않습니다.


트러블슈팅

로컬, CI, IDE가 서로 다른 Biome 버전을 쓰면 같은 파일에 대해 다른 진단이 나옵니다. 에디터 확장이 프로젝트의 node_modules에 있는 바이너리를 쓰고 있는지 확장의 출력 로그에서 확인하고, 필요하면 확장 설정에서 바이너리 경로를 명시합니다.

Prettier에서 옮겨 온 뒤 들여쓰기가 탭으로 바뀌었다면, Biome의 기본 indentStyle이 tab이라는 점 때문입니다. Prettier의 기본은 스페이스이므로, 마이그레이션 명령을 거치지 않고 손으로 설정을 만들었다면 formatter.indentStyle을 space로 지정해야 합니다.

생성된 대용량 파일이 분석 대상에 들어가면 실행 시간이 크게 늘어납니다. files.includes의 제외 패턴과 vcs.useIgnoreFile을 함께 점검합니다. Biome은 기본적으로 1MiB보다 큰 파일을 건너뛰며(files.maxSize), 이런 파일에 대해 진단 대신 크기 초과 경고가 나온다면 제외 목록에 넣는 것이 맞습니다.


같이 보면 좋은 글