본문 바로가기

FE

biome , left-hook 설정

1. 배경 및 목적

케어나는 건강검진 수치 등 민감한 의료 성격의 데이터를 다루는 서비스입니다. 따라서 기능 개발 속도만큼이나 코드 품질이 일정하게 유지되는 기본 장치가 필요합니다.

 

 자동화 없을 때 생기는 문제

  • 사람마다 코드 스타일 다름
  • 린트 규칙 기억 못함
  • PR 리뷰가 코드 스타일 지적 위주가 됨
  • 버그가 아니라 코드 품질에 시간 낭비

구조 흐름도

개발자가 코드 작성
        ↓
git commit
        ↓
Lefthook 실행
   ├── Biome (린트 + 포맷)
   ├── commitlint (메시지 검사)
        ↓
git push
        ↓
type-check
        ↓
CI

 

문제가 생기면 PR 올리기 전에 로컬에서 1차 차단 가능!!

 

왜 CI만으로는 부족한가?

CI만 사용할 때 로컬 Hook 사용할 때
PR 올리고 나서 터짐 커밋 단계에서 막힘
컨텍스트 전환 발생 바로 수정 가능
팀원 대기 시간 증가 개인 단계에서 해결

CI는 최종 방어선, Lefthook은 1차 방어선


2. 왜 ESLint + Prettier 대신 Biome인가?

Biome이란? 

Biome은 Rome이라는 프로젝트에서 시작되었습니다. TypeScript로 작성된 Rome은 2020년에 등장한 JavaScript 툴체인으로, 번들링, 컴파일, 포매팅, 린팅, 테스트 등 모든 작업을 하나의 CLI에서 수행하는 것을 목표로 하였습니다. 그러나 Rome 프로젝트는 회사의 어려움으로 인해 중단되었고,  Rome에 남아있던 일부 인원들이 프로젝트를 fork 하여 새로운 프로젝트를 시작했는데, 이것이 바로 Biome입니다.
 
도입 근거:

  1. 압도적인 속도: Rust 기반으로 작성되어 기존 JS 기반 도구보다 월등히 빠릅니다. 로컬 Hook이나 CI에서 실행할 때 속도 체감이 큽니다.
  2. 설정 복잡도 감소: ESLint와 Prettier를 같이 쓸 때 발생하는 설정 충돌, 플러그인 의존성 문제에서 벗어날 수 있습니다. package.json에 수많은 eslint 플러그인을 설치할 필요가 없고 biome.json 하나로 처리됩니다.
  3. 쉬운 온보딩: 복잡한 설정 없이 초기 세팅 비용을 줄일 수 있습니다.
항목 ESLint + Prettier Biome
성능 JS 기반 → 상대적으로 느림 Rust 기반 → 매우 빠름
설정 eslint + prettier + plugin 충돌 biome.json 하나
유지보수 플러그인 버전 관리 필요 내장 규칙 위주
온보딩 설정 이해 필요 바로 사용 가능

 

ESLint + Prettier는 대규모 프로젝트에서 수 초~수십 초
Biome은 수백 ms ~ 1초대 

 

이번 초기 세팅에서 린트와 포맷터 도구로 Biome을 선택했습니다.

 


3. Biome 설정 (biome.json)

기본적으로 recommended 규칙을 따르되, 우리 서비스 특성에 맞춰 몇 가지 규칙을 설정했다.

 

  • formatter.enabled: true:  Biome 기준으로 통일합니다.
  • organizeImports.enabled: true: 저장할 때마다 import 순서를 자동으로 정렬하고, 안 쓰는 import는 정리해 줍니다.
  • linter.rules 커스텀:
    • a11y (접근성) → warn: 의료 서비스 특성상 웹 접근성은 중요합니다. 하지만 초기 개발 속도를 고려해, 당장 배포를 막는 Error 대신 Warning으로 설정하여 품질과 속도 사이의 균형을 맞췄습니다. 추후 스프린트 과정에서 개선해 나갈 예정입니다 .
    • noFloatingPromises  error: Promise를 쓰고 await catch를 안 하면, 나중에 원인을 알 수 없는 버그가 될 수 있기 때문에 비동기 처리는 엄격하게 Error 로 잡도록 하였습니다.
    • noConsole  warn: 민감한 환자 데이터가 콘솔에 찍히는 보안 사고를 방지하게 위해 경고를 띄웁니다.
 
{
  // Biome 설정 파일의 자동 완성 및 유효성 검사를 위한 스키마 URL입니다.
  "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",

  // 린트 및 포맷팅을 적용할 파일 범위를 지정합니다.
  "files": {
    "includes": [
      "**/*.{ts,tsx}",  // TypeScript 소스 파일(*.ts, *.tsx)만 대상으로 합니다.
      "!**/*.d.ts",      // 타입 선언 파일(.d.ts)은 대상에서 제외합니다.
      "!dist",           // 빌드 결과물 폴더는 제외합니다.
      "!node_modules",   // 의존성 패키지 폴더는 제외합니다.
      "!**/*.js"         // 일반 JS 파일은 제외하고 TS 위주로 엄격하게 관리합니다.
    ]
  },

  // 코드 포맷터설정
  "formatter": {
    "enabled": true // 포맷팅 기능을 활성화하여 코드 스타일을 자동 통일합니다.
  },

  // 린터설정
  "linter": {
    "enabled": true, // 린트 기능을 활성화하여 코드 오류를 잡아냅니다.
    "rules": {
      // Biome이 권장하는 기본 규칙 세트를 활성화합니다.
      "recommended": true,

      // 접근성(a11y) 관련 규칙은 개발 속도를 위해 경고 수준으로 낮춥니다.
      // 엄격한 에러로 막기보다, 점진적으로 개선하기 위해서!!
      "a11y": "warn",

      // 코드의 정확성을 보장하는 규칙
      "correctness": {
        // React Hooks를 조건문/반복문 안에서 호출하지 못하게 막습니다. 
        "useHookAtTopLevel": "error",
        // useEffect 등의 의존성 배열 누락을 에러로 잡습니다. 
        "useExhaustiveDependencies": "error",
        // 사용하지 않는 변수는 경고 처리합니다.
        "noUnusedVariables": "warn",
        // 사용하지 않는 import 문은 경고 처리합니다. 
        "noUnusedImports": "warn"
      },

      // 의심스러운 코드 패턴에 대한 설정
      "suspicious": {
        // 'void' 타입 사용 시 혼동을 줄 수 있는 규칙은 끕니다. (TS 패턴과 충돌 방지)
        "noConfusingVoidType": "off",
        // 코드에 로그가 남지 않도록
        "noConsole": "warn"
      },

      // 실험적 기능에 있는 규칙 중 유용한 것을 가져옵니다.
      "nursery": {
        // (비동기 로직 누락 방지)
        "noFloatingPromises": "error"
      }
    }
  },

  // 코드 어시스트 관련 설정
  "assist": {
    "actions": {
      "recommended": true,
      "source": {
        // 저장하면 import 구문을 자동으로 정렬하고 최적화합니다.
        "organizeImports": "on"
      }
    }
  }
}

 


 
 

4.   Lefthook

4-1.  왜 Husky 대신 Lefthook을 선택했는가?

보통 커밋, 푸시를 하기전에 코드 품질을 자동으로 검사하고 포맷팅되지 않은 코드의 커밋을 방지하거나 잘못된 커밋 메시지를 막기 위해 husky를 많이 사용합니다. 하지만 husky는 Node.js 기반으로 동작하기 때문에 프로젝트 규모가 커질수록 커맨드 실행 속도가 느려지는 단점이 있습니다.

또한 husky는 각 훅마다 .husky/pre-commit, .husky/commit-msg처럼 별도의 스크립트 파일을 생성하고 관리해야 하지만 lefthook은 lefthook.yml 하나로 모든 훅을 설정할 수 있어 구성과 유지보수가 훨씬 간결합니다.
그리고 parallel: true 옵션을 통해 eslint, prettier 같은 작업을 병렬로 실행할 수 있어 전체 실행 시간도 줄일 수 있습니다

따라서 단순한 작은 프로젝트에는 둘 다 사용할 수 있지만, 속도, 설정 편의성, 유지보수, CI 최적화 측면에서 lefthook이 더 유리하다고 판단해 이번 프로젝트에서 lefthook을 도입하였습니다.

 

🔍 Husky vs Lefthook 비교

 

항목 Husky Lefthook
실행 기반 Node.js Go (단일 바이너리)
실행 속도 JS 런타임 로딩 필요 → 상대적으로 느림 매우 빠름 (컴파일 언어 기반)
병렬 실행 기본적으로 순차 실행 parallel 옵션으로 병렬 실행 가능
설정 방식 package.json 또는 .husky 스크립트 파일 lefthook.yml 하나로 선언적 관리
대규모 프로젝트 적합성 Hook 늘어나면 체감 속도 저하 파일 수 많아도 성능 영향 적음
CI 친화성 로컬 중심 설계 로컬 + CI 환경 모두 고려
의존성 Node 설치 필수 Node 없이도 동작 가능

 우리 프로젝트에서 Lefthook이 더 적합했던 이유!!

① 속도

Biome, commitlint, type-check까지 돌리면 Hook 작업이 적지 않다.
Husky는 Node 런타임 위에서 실행되기 때문에 Hook이 많아질수록 커밋 속도가 느려지는 체감이 있다.

👉 Lefthook은 Go 기반이라 실행 오버헤드가 거의 없고, 실제로 커밋 지연이 매우 적다.

코드 품질 장치가 개발 속도를 떨어뜨리면 결국 팀이 훅을 끄게 된다.
Lefthook은 이 문제를 구조적으로 방지해준다.


② 병렬 실행 — 검사 도구가 여러 개일 때 강력함

프로젝트의 품질을 지키기 위해 Biome, Type-check, Test 등 검사해야 할 항목은 계속 늘어납니다. 이때, 검사 도구를 어떻게 실행하느냐가 개발 생산성, 즉 속도를 결정합니다.

 

Husky: 순차 실행

Husky는 기본적으로 등록된 작업들을 하나씩 순서대로 실행합니다. 앞선 작업이 끝나야 다음 작업이 시작되므로, 검사 도구가 늘어날수록 대기 시간이 누적됩니다.


 
[Husky 실행 흐름]
START ──── [ Biome 검사 (3s) ] ──── [ Unit Test (5s) ] ──── [ 기타 검사 (2s) ] ──── END
# 총 소요 시간: 10초 + @ (Node.js 구동 시간)

Lefthook: 병렬 실행 

반면 Lefthook은 Go 언어의 동시성 기능을 활용해, 독립적인 작업들을 동시에 실행합니다. 설정 파일에 parallel: true 한 줄만 추가하면, 가장 오래 걸리는 작업이 끝나는 시간이 곧 전체 소요 시간이 됩니다. 


 
[Lefthook 실행 흐름]
START ┬── [ Biome 검사 (3s) ] ──────┐
      ├── [ Unit Test (5s) ] ───────┴── END
      └── [ 기타 검사 (2s) ] ───────┘
# 총 소요 시간: 5초 (가장 긴 작업 기준)

 설정 

저희 프로젝트에서는 pre-commit 단계에서 린트와 포맷팅, 스타일 검사 등이 동시에 이루어질 수 있도록 parallel 옵션을 적용했습니다. 덕분에 린트, 포맷, 기타 검사 시간이 겹쳐져 개발자의 커밋 대기 시간을 단축했습니다.


 
# lefthook.yml
pre-commit:
  parallel: true  #병렬 실행 
  commands:
    biome:
      run: pnpm exec biome check ...
    # 추후 테스트나 다른 검사 도구가 추가되어도 속도 저하가 거의 없음

③ 설정 구조가 선언적 → 유지보수에 유리

Husky의 구조 (분산됨, 스크립트)

Husky는 훅마다 파일을 따로 만들어야 합니다. 그리고 그 안에는 쉘 스크립트라는 리눅스 명령어가 들어갑니다.

 

폴더 구조:

.husky/
  ├── _/ (Husky 내부 파일들...)
  ├── pre-commit   <-- 파일 1
  └── commit-msg   <-- 파일 2

 


단점: 파일이 여러 개로 쪼개져 있어서, 우리 프로젝트에 어떤 규칙들이 있는지 보려면 파일을 하나하나 다 열어봐야 합니다. 그리고 쉘 스크
 
립트 문법을 모르면 수정하다가 에러를 내기 쉽습니다.
 

Lefthook의 구조 (중앙 집중, 설정)

Lefthook은 파일 하나에 모든 규칙을 다 적습니다. 그리고 YAML이라는 설정 파일 형식을 써서 훨씬 읽기 편합니다.

 

폴더 구조:

lefthook.yml    # 파일 하나

 

내용 (lefthook.yml):

# 한 파일 안에서 pre-commit, commit-msg 다 관리함
pre-commit:
  commands:
    lint:
      run: npm run lint

commit-msg:
  commands:
    check:
      run: npx commitlint --edit

 

장점: lefthook.yml 파일 하나만 열면 커밋 전에 린트 검사를하고, 메시지 검사도 한다는 것을 알 수 있고 유지보수에도 유리한 방식임을 알 수 있다. 

 


④ Node 환경 의존성 제거

Husky는 Node 환경이 깨지면 Hook도 같이 깨진다.
Lefthook은 바이너리라 이런 영향이 없다.

특히:

  • CI 환경
  • Docker 컨테이너
  • 다양한 OS 환경

에서 더 안정적이다.

 

Husky는 가장 널리 쓰이는 Git Hook 도구이지만, 실행 속도와 병렬 처리 측면에서 대규모 또는 품질 검사 도구가 많은 프로젝트에서는 한계를 느낄 수 있다.
이번 프로젝트는 Biome + commitlint + type-check 등 여러 검사를 로컬 단계에서 수행해야 했고, 이 과정이 개발 흐름을 방해하지 않는 것이 중요했다.
Lefthook은 Go 기반의 빠른 실행 속도, 병렬 실행 지원, 선언형 설정 구조 덕분에 코드 품질을 지키면서도 개발 속도를 유지할 수 있는 선택지였다.

 

4-2.  Lefthook 설정 코드 설명

 

① pre-commit: 커밋 직전, 코드 품질 검사

 

가장 자주 실행되는 단계입니다. 개발자가 git commit을 입력하면, Biome이 코드를 검사하고 수정합니다.

pre-commit:
  parallel: true
  commands:
    biome:
      glob: "*.{js,ts,jsx,tsx,json,md,yml,yaml,css}"
      run: pnpm exec biome check --write --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}
      stage_fixed: true

 

 

  • 효율성 극대화 ({staged_files})
    • 프로젝트 전체를 매번 검사하면 커밋 속도가 매우 느려집니다. 따라서 이번에 수정한(Staged) 파일만 검사하도록 범위를 제한했습니다.
  • 자동 수정 및 반영 (--write & stage_fixed: true)
    •  Biome이 린트 에러나 포맷팅을 자동으로 고쳐주더라도(--write), 이를 다시 git add하지 않으면 수정 전 파일이 커밋되는 문제가 발생합니다.
    • stage_fixed: true 옵션은 Biome이 수정한 코드를 즉시 다시 Staging 영역에 올려, 올바른 코드가 커밋되도록 보장해 줍니다.
  • 유연한 파일 처리 (glob & --no-errors...)
    • JS/TS 파일뿐만 아니라 CSS 파일도 포함했다. Tailwind를 쓰더라도 기본적인 CSS 문법 오류를 방지하기 위해서입니다.
    •  

 

② commit-msg: 커밋 메시지 규칙 검사

협업할 때 사람마다 커밋 메시지의 형식이 다르면 히스토리를 파악하기 어렵습니다. 이를 방지하기 위해 메시지 작성 규칙을 검사합니다.

commit-msg:
  commands:
    commitlint:
      run: pnpm exec commitlint --edit {1}
  • 규칙의 표준화 (commitlint)
    • feat:, fix:와 같은 표준 접두어(Conventional Commits) 사용을 강제합니다.
  • 우리 팀만의 룰 적용
    • 소문자 강제: Feat 대신 feat만 허용하여 일관성을 유지합니다.
    • 본문 필수: 제목만 있는 불친절한 커밋을 방지하기 위해, 본문 작성을 의무화하여 변경 이유를 남기도록 했습니다.

③ pre-push: 푸시 전에 타입 체크

로컬에서는 문제없었는데, CI에 올리자마자 빌드가 깨지는것을 방지하기 위한 마지막 안전장치입니다.

pre-push:
  commands:
    type-check:
      run: pnpm run type-check

   

    타입 안정성 확보 

  • tsc(TypeScript Compiler)를 실행하여 타입 에러가 있는지 검사합니다.
  • 이 과정은 시간이 조금 걸릴 수 있어, 자주 일어나는 commit 단계 대신 push 직전에 한 번만 수행하도록 배치하여 개발 속도와 안정성의 균형을 맞췄습니다.

 

5.  실행결과


- vs code에서 커밋 메시지를 컨벤션에 맞지 않게 작성했을 때, 이런 GUI 가 나옵니다.



- Biome + commitlint + lefthook이 모두 정상 작동되어 pre-commit → commit-msg 검사 후 커밋에 성공한 화면입니다