카드 한쪽 액센트 바를 테스트로 차단하기: no-one-sided-accent
디자인 금지 규칙을 문서 대신 테스트로 강제하는 법. 한 변 액센트 바를 정규식 3종으로 잡아 CI에서 fail-closed로 막는 과정을 정리했어요.
AI한테 카드 UI를 맡기면 거의 매번 같은 모양이 돌아와요. 왼쪽에 4px짜리 색 띠를 두른 카드요. DESIGN.md에 금지라고 적어 두면 다음엔 띠가 위로 올라가요. border-l-4를 막았더니 border-t-2가 왔고, 그것도 막았더니 절대배치로 3px 가짜 띠를 그렸어요. 문장으로 적은 규칙은 우회당해요.
백엔드에서는 익숙한 상황이에요. 입력 검증을 핸들러마다 손으로 쓰면 하나는 꼭 빠지니까 결국 DB에 CHECK 제약을 걸잖아요. 디자인 규칙도 같아요. 리뷰어 눈에 기대는 문장이 아니라, 어기면 빌드가 멈추는 제약으로 바꿔야 지켜져요.
테스트 파일 1개면 끝나요
플랜비 레포에 넣어 둔 차단 테스트는 소스를 훑는 Vitest 테스트 하나예요. 직접 만들어 보니 세 단계면 충분했어요.
const BANNED = [
/\bborder-[tblr]-[2-9]\b/,
/border(Top|Bottom|Left|Right):\s*['"]?[2-9]px/,
/inset-x-0 (top|bottom)-0 h-\[[2-9]px\]/,
];
const ALLOW = 'allow-one-sided-accent:';
test('no one-sided accent', async () => {
const hits: string[] = [];
for (const file of await glob('src/**/*.{tsx,css}')) {
const lines = (await readFile(file, 'utf8')).split('\n');
lines.forEach((line, i) => {
if (line.includes(ALLOW)) return;
if (BANNED.some((re) => re.test(line))) hits.push(`${file}:${i + 1}`);
});
}
expect(hits).toEqual([]);
});패턴은 3종이에요. Tailwind 한 변 보더, 인라인 스타일, 절대배치 가짜 띠. 1px은 구조용 구분선이라 통과시키고 2px부터 막아요. 색이 다르든 방향이 다르든 한 변에만 두꺼운 띠면 전부 걸려요.
예외는 같은 줄 주석으로만
우회 경로가 없는 제약은 사람이 끄게 돼요. 그래서 예외는 해당 줄에 allow-one-sided-accent: 사유 주석 1개를 남기는 방식만 열어 뒀어요. 이렇게 해 보니 테스트를 skip하거나 패턴을 지우는 우회는 diff에 바로 드러나서 리뷰에서 잡혀요. 사유를 강제하는 건 마이그레이션 파일에 why를 적게 하는 것과 같은 이유예요.
CI에서 fail-closed로
따로 린트 플러그인을 만들지 않고 pnpm test에 그대로 넣었어요. 적용해 봤더니 수백 개 파일을 정규식으로 훑는 데 1초가 안 걸려서 pre-commit에 올려도 체감 지연이 없었어요. 돌려 보니 기존 코드에서 몇 건이 바로 걸렸고, 틴트 배경이나 색 점으로 바꾸는 데 파일당 2분 안쪽이면 끝났어요.
테스트해 보면 좋은 점이 하나 더 있어요. AI가 새 우회 패턴을 들고 올 때마다 정규식 한 줄만 추가하면 돼요. 문서를 고치면 다음 세션에서 잊히지만 테스트에 들어간 패턴은 안 잊혀요.
DESIGN.md 금지 문장 vs 차단 테스트 — 강제력·우회 비용·예외 추적
막힌 건수는 테스트 출력에 그대로 남아요. 몇 번 잡혔는지 세어 보면 규칙이 실제로 일하는지도 보여요. 강제되고 측정될 때만 규칙이에요.
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.