린트는 스타일 검사가 아닙니다

여기 있는 규칙 대부분은 컴파일되고, 실행되고, 멀쩡해 보이는데 조용히 아무 일도 하지 않는 코드를 잡습니다. 이런 코드는 린터만 알아챕니다.
badge에 bg-blue-500을 적었다고 해 봅시다. 페이지는 그려지고 class도 DOM에 있지만 badge에는 색이 없고, 빌드도 브라우저도 아무 말이 없습니다.
CSS가 없는 class
Tailwind 기본 팔레트는 스타일시트에서 빠져 있어서, badge가 색 없이 그려집니다.
className="bg-blue-500"
에이전트가 닿지 못하는 필드
setter를 화살표 함수로 감싸면 가려져서, 그 필드는 에이전트 툴로 공개되지 않습니다.
onChange={(v) => st.do.setTitleOnTicket(v)}
아무도 받지 못하는 반환값
store 액션은 void로 호출되므로, 호출한 쪽은 그 값을 받지 못합니다.
return ticket;
모든 방문자가 내려받는 메모
느낌표 주석은 minify를 거쳐도 남아서 브라우저 번들에 그대로 실립니다.
//! remove before launch
이 페이지에서 쓰는 말
Biome
이 워크스페이스가 쓰는 포매터이자 린터입니다. akan lint가 대신 실행합니다.
grit 플러그인
Akan을 위해 GritQL로 쓴 린트 규칙이며, Biome이 실행합니다. 모두 28개입니다.
진단
Biome이 출력하는 검사 결과 한 건입니다. 파일, 줄, 규칙 이름, 메시지가 들어 있습니다.
안전한 수정
클래스 정렬이나 안 쓰는 import 삭제처럼, Biome이 알아서 적용하는 수정입니다.
색 어휘
시맨틱 색 토큰만 모은 닫힌 집합입니다. Tailwind 기본 팔레트는 여기에 없습니다.
적용 범위
규칙이 들여다보는 경로입니다. 범위 밖의 파일은 그 규칙에 걸리지 않습니다.

가장 먼저 만날 여섯 가지

진단에는 걸린 규칙의 이름이 찍힙니다. 아래에서 그 이름을 찾으면 고치는 방법은 정해져 있습니다.
색 어휘에 없는 색
no-raw-palette-class
  • 이유: 컴파일된 스타일시트에서 Tailwind 기본 팔레트가 빠지므로 bg-blue-500 뒤에는 CSS가 없습니다. DOM에는 class가 그대로 보이는데 badge는 스타일 없이 그려집니다.
  • 고치는 법: bg-primary 같은 시맨틱 토큰을 씁니다. style 안의 hex 색도 함께 없앱니다(no-inline-color).
apps/myapp/ui/StatusBadge.tsx
그냥 던진 Error
no-throw-raw-error
  • 이유: 그냥 던진 Error는 메시지도 번역도 없이 Internal Server Error로 호출자에게 도착합니다.
  • 고치는 법: key를 지정한 Err를 던지고, 그 key를 모듈 dictionary에 [en, ko] 쌍으로 등록합니다.
apps/myapp/lib/ticket/ticket.service.ts
그리고 같은 모듈의 dictionary에 key를 등록합니다.
apps/myapp/lib/ticket/ticket.dictionary.ts
화살표 함수로 감싼 setter
no-unpublished-form-setter
  • 이유: 두 줄은 똑같이 실행되지만, 화살표 함수는 이름 없는 closure입니다. 그러면 control에 data-akan-action이 붙지 않고, 필드도 에이전트 툴로 공개되지 않습니다.
  • 고치는 법: setter를 참조로 넘깁니다. 값을 다듬으려면 control의 transform prop을, 여러 값을 함께 쓰려면 store의 _postSet<Field> 메서드를 씁니다.
apps/myapp/lib/ticket/Ticket.Template.tsx
store 액션이 돌려주는 값
no-return-in-store-action
  • 이유: store의 모든 메서드는 st.do.<action>()으로 호출되고, 이 호출은 void입니다. 돌려준 값은 어느 호출 지점에도 닿지 않습니다.
  • 고치는 법: 값은 this.set({ ... })으로 state에 씁니다. 조건 탈출용 return;은 그대로 써도 됩니다.
apps/myapp/lib/ticket/ticket.store.ts
클라이언트에서 부른 hydration 호출
no-init-fetch-in-client
  • 이유: fetch.init<Model><Suffix>는 Load.Units가 store를 채울 스냅샷을 만듭니다. hydration 뒤에 부르면, 브라우저가 이미 그린 화면을 위해 왕복을 두 번 더 합니다.
  • 고치는 법: 첫 바이트 전에 끝나도록 라우트에서 시작하고, 그 promise를 Zone의 init으로 넘깁니다. 클라이언트에서 다시 불러올 때는 st.do.init<Model><Suffix>()를 부릅니다.
고치기 전에는 Zone이 마운트될 때 목록을 불러옵니다.
apps/myapp/lib/ticket/Ticket.Zone.tsx
고친 뒤에는 라우트가 불러오기를 시작하고 그 promise를 넘깁니다.
apps/myapp/page/project/[projectId]/_index.tsx
Zone은 받은 것을 그리기만 합니다.
apps/myapp/lib/ticket/Ticket.Zone.tsx
네 가지 파일에서 쓴 #private
no-js-private-class-method
  • 이유: 프레임워크는 constant, document, service, store 클래스의 메서드를 다른 클래스로 복사해 합칩니다. 복사된 메서드가 # 멤버를 부르면 에러가 납니다.
  • 고치는 법: 밑줄로 시작하는 TypeScript private 메서드를 씁니다. srvkit/를 포함한 나머지 파일에서는 #private이 그대로 기본 스타일입니다.
apps/myapp/lib/ticket/ticket.service.ts

빌드를 깨는 규칙 전체

스물여덟 개는 Akan을 위해 쓴 grit 플러그인이고, 표에 경고라고 적힌 것 말고는 모두 error입니다. 경고는 출력되지만 빌드를 깨지 않습니다. 나머지는 Biome 자체 규칙입니다. 플러그인은 자기 적용 범위만 보므로, pkgs/ 아래의 평범한 패키지는 모듈 규칙에 걸리지 않습니다.
색 어휘
다섯 규칙 모두 테스트를 뺀 apps/, libs/의 .ts, .tsx 파일 전체를 봅니다.
no-raw-palette-class
bg-blue-500 같은 Tailwind 기본 팔레트 class는 CSS가 생기지 않습니다. bg-primary 같은 토큰을 씁니다.
no-arbitrary-color
bg-[#3b82f6] 같은 색 값은 data-theme을 무시합니다. var() 참조는 괜찮습니다.
no-daisyui-legacy-class
btn-primary, card-body, bg-base-100 같은 daisyUI class는 제거되어 스타일 없이 그려집니다.
no-inline-color
style={{ ... }}이나 <style> 본문의 색 리터럴은 토큰과 테마 전환을 건너뜁니다. SVG 색 속성(fill, stroke, stopColor 등)이나 el.style 대입에 쓴 같은 리터럴은 경고입니다.
no-interpolated-arbitrary-class
min-h-[${n}px]처럼 런타임에 만든 arbitrary 값은 CSS가 생기지 않습니다. style이나 고정된 class를 씁니다.
daisyUI에서 빠진 색 슬롯은 아래 토큰으로 옮깁니다. black과 white는 어휘에 그대로 남아 있습니다.
base-100
background
base-200
muted
base-300
border
base-content
foreground
<colour>-content
<colour>-foreground
error
destructive
에러, 로그, 주석
no-throw-raw-error
던진 Error입니다. new Err("<module>.error.<key>")를 던지고 key를 등록합니다. 그 밖에서 만든 Error(reject, return, 보관)는 경고입니다.적용 범위: apps/** libs/** (테스트, *.constant.ts, common/**, env/** 제외)
no-deprecated-log-level
logger.log()는 별도 레벨처럼 보이지만 info로 찍힙니다. .info()를 씁니다.적용 범위: apps/** libs/**
no-bang-comment-in-client
//!나 /*! 주석은 minify 뒤에도 남아 배포됩니다. 대신 // FIXME:를 씁니다.적용 범위: ui/ webkit/ common/ page/, *.constant.ts *.store.ts, 모듈 컴포넌트
no-document-cookie
iOS·macOS·Linux의 app:// 페이지는 쿠키를 보관하지 않아 앱에서 document.cookie가 비어 있습니다. akanjs/client의 getCookie / setCookie / removeCookie를 씁니다.적용 범위: apps/** libs/** (테스트 제외)
no-web-storage
localStorage / sessionStorage는 akanjs가 플랫폼별로 고르는 저장소를 우회하고 SSR에서 예외가 납니다. akanjs/client의 storage를, 자격 증명은 secretStorage를 씁니다.적용 범위: apps/** libs/** (테스트 제외)
no-web-only-api-outside-webkit
navigator.share, navigator.serviceWorker, Notification.*, navigator.geolocation, navigator.vibrate는 일부 앱 WebView에 없습니다. isNativeApp()으로 분기하는 webkit/ 훅 안에 둡니다.적용 범위: webkit/ 밖의 apps/** libs/** (테스트 제외) — 경고
  • 서버에서는 //!를 써도 됩니다. 서버 파일, srvkit/, CLI 코드는 브라우저에 가지 않습니다.
  • no-bang-comment-in-client는 언제나 1행을 가리킵니다. 파일 단위 진단이므로, 표식은 파일에서 직접 찾아야 합니다.
store, form, 모듈 파일
no-return-in-store-action
st.do.<action>()은 void입니다. 값은 this.set({ ... })으로 state에 씁니다.적용 범위: *.store.ts
no-unpublished-form-setter
form setter로 값만 넘기는 화살표 함수입니다. st.do.setXOnY를 참조로 넘깁니다.적용 범위: apps/ libs/의 모든 .tsx
no-init-fetch-in-client
클라이언트에서 부른 fetch.init<Model><Suffix>나 fetch.get<Model>Init<Suffix>입니다. 라우트에서 불러옵니다.적용 범위: "use client" 파일과 *.store.ts
no-model-type-in-util-zone
항상 클라이언트인 파일의 prop 타입에 쓴 cnst 모델입니다. 대신 id를 받습니다.적용 범위: *.Util.tsx *.Zone.tsx
no-redeclare-predefined-endpoint
create<Model>, view<Model>처럼 생성된 CRUD 이름을 다시 쓴 엔드포인트입니다.적용 범위: *.signal.ts
no-unguarded-endpoint
guards가 없거나 guards: []인 엔드포인트와 slice의 init(), 그리고 guards나 root가 없는 slice입니다. 모두 누구에게나 열립니다. 의도한 것이라면 guards: [Public]을 씁니다.적용 범위: *.signal.ts — 경고
no-double-brace-placeholder
.error()·.translate() 항목의 {{name}}은 값이 채워지지 않고 적힌 그대로 보입니다. {name}으로 씁니다.적용 범위: *.dictionary.ts
no-js-private-class-method
#private 메서드입니다. 대신 TypeScript private _method()를 씁니다.적용 범위: *.constant.ts *.document.ts *.service.ts *.store.ts
no-static-in-object-light-model
XObject나 LightX에 쓴 static이며, 전체 모델에 닿지 않습니다. XInput, X, XInsight에 선언합니다.적용 범위: *.constant.ts
  • 생성된 CRUD 이름은 이미 쓰이고 있습니다. <model>, light<Model>, create<Model>, update<Model>, remove<Model>, view<Model>, edit<Model>, merge<Model>가 이미 있으니, 직접 만드는 엔드포인트에는 다른 이름을 붙입니다.
서버 컴포넌트
page, Unit, View는 언제나 서버 컴포넌트입니다. 이 규칙들이 그 안에 클라이언트 코드가 들어오지 않게 막습니다.
no-import-client-functions
서버 컴포넌트에 import한 React hook이나 st입니다. 상호작용 부분을 떼어냅니다.적용 범위: page/** *.Unit.tsx *.View.tsx
no-use-client-in-server
항상 서버 컴포넌트인 파일에 붙은 "use client"입니다. 상호작용 부분만 떼어냅니다.적용 범위: page/** *.Unit.tsx *.View.tsx
non-scalar-props-restricted
서버 컴포넌트에서 prop으로 넘긴 함수입니다. loader, render, of만 함수를 받습니다.적용 범위: page/** *.Unit.tsx *.View.tsx
no-async-component-in-ui
async ui/ 컴포넌트는 클라이언트 부모 아래에서 깨집니다. page에서 await합니다.적용 범위: ui/**/*.tsx
import
no-deep-internal-import
<name>/<entry>보다 깊은 @apps·@libs 경로, 모듈 파일의 ../../, 모듈 .tsx의 ../입니다.적용 범위: 모듈 파일, page/**, barrel
no-import-external-library
서드파티 패키지입니다. 먼저 lib의 common/, webkit/, ui/에서 re-export합니다.적용 범위: 모듈 파일, page/**, barrel
no-import-server-in-client
*.document·*.dictionary·*.service·*.signal 파일, srvkit/, server 엔트리를 import합니다.적용 범위: ui/ webkit/ page/ common/, *.store.ts *.constant.ts, 모든 .tsx
no-import-client-in-server
*.store, 모듈 컴포넌트, ui/, webkit/, client 엔트리, client barrel을 import합니다.적용 범위: *.document *.dictionary *.service *.signal srvkit/ common/ *.constant
  • import type은 언제나 허용됩니다. 번들 전에 지워지기 때문입니다. 값과 타입을 섞은 import는 예외가 아닙니다.
  • common/과 *.constant.ts는 두 방향을 모두 지킵니다. 그래서 공유 코드는 어느 쪽에도 닿지 않습니다.
  • barrel도 마찬가지입니다. 클라이언트 파일은 db, srv, sig, dict, option, useServer를, 서버 파일은 st, store, useClient를 import할 수 없습니다.
  • Akan 자체 패키지는 통과합니다. 상대 경로, akanjs, @akanjs/*, @apps/*, @libs/*, @pkgs/*, react*, @playwright/*, bun:test는 서드파티 import로 보지 않습니다.
  • 모듈 사이의 constant 참조는 예외입니다. .ts 모듈 파일은 ../map/map.constant를 import할 수 있습니다.
Biome 자체 규칙
이 규칙들은 모든 파일에 적용됩니다. 꺼 둔 두 규칙은 일부러 끈 것입니다.
nursery/useSortedClasses
cn()의 문자열 인자까지 Tailwind class를 정렬합니다. 손으로 정렬하거나 결과를 되돌리지 않습니다.수준: error, 안전한 수정
suspicious/noConsole
console.log와 console.debug입니다. assert, error, info, warn만 허용됩니다.수준: error
correctness/noUnusedImports
akan lint가 수정 단계에서 안 쓰는 import를 보고하는 대신 지웁니다.수준: error, 안전한 수정
suspicious/noArrayIndexKey
일부러 껐습니다. 자기 id가 없는 embedded scalar의 key={idx}는 의도된 것입니다.수준: off
correctness/useExhaustiveDependencies
일부러 껐습니다. 이 워크스페이스의 짧은 dependency 배열은 의도된 것입니다.수준: off

규칙 하나 끄기

고정된 색이 맞을 때도 있습니다. OS 창 목업, 데이터 시각화 색 척도, 벤더의 브랜드 색 같은 경우입니다. 그 자리만 규칙을 끄고, 이유는 반드시 적습니다.
// biome-ignore lint/plugin: <reason>
다음 줄이나 문장에서 플러그인 규칙을 끕니다.
{/* biome-ignore lint/plugin: <reason> */}
JSX 안에서 같은 일을 합니다. 요소 하나 바로 위에 둡니다.
// biome-ignore-all lint/plugin: <reason>
파일 맨 위에 두면 파일 전체에서 플러그인 규칙을 끕니다.
// biome-ignore lint/suspicious/noConsole: <reason>
Biome 자체 규칙은 그룹과 규칙 이름으로 지정합니다.
JSX 형태는 실제 컴포넌트에서 이렇게 씁니다.
apps/myapp/ui/BrowserChrome.tsx
  • 모든 예외에는 이유가 붙습니다. 이 워크스페이스에는 이유 없는 disable 블록이 하나도 없습니다.
  • 범위는 좁게 둡니다. 파일 단위 형태는 데이터 시각화 팔레트 파일처럼 파일 안의 모든 위반이 같은 이유일 때만 씁니다.

명령어와 설정

akan lint는 app, lib, package 하나를 포맷하고 고칩니다. bunx biome check는 보고만 합니다.
Terminal
  • 진단은 200개까지 출력합니다. Biome 기본값은 전체 개수 없이 20개만 보여 주므로, 문제의 종류만 바뀌어도 줄어든 것처럼 보입니다.
  • --fix false로 돌려도 akan lint의 검사는 모두 거칩니다. Biome의 수정만 건너뛰고 아래 검사는 그대로 합니다. Biome만 돌리려면 bunx biome check를 씁니다.
akan lint가 Biome 다음에 확인하는 것
page/styles.css
테마마다 글자와 배경 토큰 쌍이 WCAG 대비 기준을 넘어야 합니다.
ui/Recipe/*
고를 variant가 없는 recipe는 실패합니다. 컴포넌트나 class 상수로 바꿉니다.
AGENTS.md
## Recipes In Scope 블록이 오래되면 실패합니다. akan sync <name>으로 다시 만듭니다.
설정이 있는 곳
biome.json
워크스페이스 루트에 있고 @akanjs/devkit/biome.base.json을 확장합니다.
@akanjs/devkit/biome.base.json
모든 규칙의 수준을 정하고, grit 플러그인마다 적용 경로를 지정합니다.
@akanjs/devkit/lint/*.grit
플러그인 원본이며 규칙마다 파일 하나입니다. 대부분은 그 규칙이 없으면 무엇이 깨지는지 설명하는 주석으로 시작합니다.

MIT 라이선스 하에 배포되었습니다.

내 AI에 이 문서 연결하기

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman