lifecarelog
백엔드

카드 한쪽 액센트 바를 테스트로 차단하기: no-one-sided-accent

디자인 금지 규칙을 문서 대신 테스트로 강제하는 법. 한 변 액센트 바를 정규식 3종으로 잡아 CI에서 fail-closed로 막는 과정을 정리했어요.

5분 읽기

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 차단 테스트 — 강제력·우회 비용·예외 추적

막힌 건수는 테스트 출력에 그대로 남아요. 몇 번 잡혔는지 세어 보면 규칙이 실제로 일하는지도 보여요. 강제되고 측정될 때만 규칙이에요.

#Vitest#CI게이트#디자인시스템

라이프케어로그 서비스가 궁금하신가요?

AI 기반 건강·일정·재활 관리 앱을 직접 써보세요.

서비스 살펴보기

관련 글

댓글

아직 댓글이 없어요. 첫 댓글을 남겨주세요.