왜 화면만 매번 사람 손을 타는가
코드 품질에는 이미 자동 장치가 있다. 린트가 규칙 위반을 막고, 타입 검사가 형태를 맞추고, 테스트가 동작을 지킨다. 이 셋은 사람이 보지 않아도 돌아가고, 어기면 병합이 막힌다.
화면은 그렇지 않다. 대비가 무너졌는지, 카드가 두 겹인지, 모바일에서 글자가 잘리는지는 대개 리뷰어가 눈으로 본다. 그래서 리뷰어가 바쁘면 통과하고, 사람이 바뀌면 기준도 바뀐다. 같은 지적이 몇 달 뒤에 다시 올라온다.
AI에게 화면을 맡기기 시작하면 이 문제가 커진다. 사람이 하루에 만들던 화면을 에이전트가 한 시간에 만들어 내는데, 검토는 여전히 사람 속도이기 때문이다. 검토를 자동으로 돌릴 수 있는 부분과 사람이 봐야 하는 부분을 갈라 두지 않으면 병목이 그대로 남는다.
기계가 볼 수 있는 항목은 생각보다 넓다. 대비 비율, 줄 간격, 터치 영역 크기, 제목 단계 건너뜀, 카드 중첩, 색 조합처럼 값으로 판정되는 것들이다. 이런 항목을 CI로 내리고, 사람은 위계와 의미 전달에 집중하면 된다.
종료코드 하나로 게이트가 된다
Impeccable의 detect는 결과를 종료코드로 알려준다. 지적이 있으면 0이 아닌 값으로 끝나고, 없으면 0으로 끝난다. CI는 명령이 0이 아닌 값으로 끝나면 그 단계를 실패로 보므로, 별도의 액션이나 플러그인 없이 워크플로에 명령 한 줄만 넣으면 게이트가 된다.
실제로 확인해 보면 이렇다. 안티패턴을 일부러 넣은 HTML 한 장에 돌리면 17건이 나오고 종료코드는 2다. 지적받은 항목만 고치고 다시 돌리면 0건, 종료코드 0이다. 이 차이 하나로 파이프라인이 갈린다.
붙이는 자리는 두 곳이 좋다. 하나는 소스 폴더를 대상으로 하는 단계이고, 다른 하나는 배포 미리보기 주소를 대상으로 하는 단계다. 뒤에서 설명하듯 둘이 잡는 항목이 다르기 때문이다.
LLM을 부르지 않는다는 점이 CI에서 특히 중요하다. API 키를 비밀값으로 넣을 필요가 없고, 같은 입력에 같은 결과가 나오며, 실행 시간과 비용이 예측 가능하다. 모델 응답에 따라 통과 여부가 흔들리는 검사는 게이트로 쓸 수 없다.
- name: Design check
run: npx impeccable detect src/
# 배포 미리보기까지 함께 보고 싶다면
- name: Design check (deployed)
run: npx impeccable detect ${{ steps.deploy.outputs.preview-url }}대상에 따라 검사 방식이 갈린다
같은 명령이지만 무엇을 주느냐에 따라 안에서 하는 일이 다르다. HTML 파일은 연결된 CSS까지 함께 읽어 정적으로 분석한다. JSX, TSX, CSS 같은 나머지 파일은 패턴 매칭으로 훑는다. 주소를 주면 실제 브라우저로 페이지를 띄워 렌더가 끝난 화면을 본다.
이 차이가 실무에서 헷갈리는 지점을 만든다. 컴포넌트 소스만 스캔하면 값으로 드러나는 항목은 잡히지만, 실제로 겹쳐 놓았을 때만 드러나는 항목은 지나간다. 예컨대 제목 바로 위에 붙은 작은 라벨이나 최종 대비 값은 화면이 조립된 뒤에야 판정된다. 소스 스캔이 0건인데도 배포 주소 스캔에서 지적이 나오는 이유가 이것이다.
그래서 게이트를 하나만 둔다면 배포 미리보기 주소 쪽이 더 많이 잡는다. 다만 주소 검사는 브라우저를 띄우므로 소스 스캔보다 느리다. 소스 스캔은 모든 푸시에, 주소 스캔은 미리보기가 준비된 뒤에 거는 조합이 무난하다.
옵션은 넷만 알면 충분하다. json은 기계 판독용 출력이라 리포트 생성이나 PR 코멘트로 넘기기 좋고, scope는 타입이나 레이아웃처럼 특정 영역만 보게 하며, viewport는 주소 검사에서 화면 폭을 바꿔 모바일로 한 번 더 돌리게 한다. no-advisory는 참고용으로 분류된 항목을 아예 숨긴다.
npx impeccable detect --json src/ > findings.json # 리포트용
npx impeccable detect --scope type,layout src/ # 영역 한정
npx impeccable detect --viewport 390x844 <URL> # 모바일 폭 재검사
npx impeccable detect --no-advisory src/ # 참고 항목 숨김예외는 이유와 함께 코드에 남긴다
게이트를 걸면 반드시 예외가 필요해진다. 브랜드가 정한 폰트가 흔한 목록에 들어 있거나, 특정 페이지에서만 의도적으로 규칙을 벗어나는 경우가 생긴다. 이때 검사를 통째로 끄는 대신 두 가지 방법을 쓴다.
첫째는 파일 안 주석이다. impeccable-disable 뒤에 규칙 이름과 이유를 적으면 그 파일에서만 면제된다. 한 줄만 면제하는 형태도 있다. 예외가 코드 옆에 붙어 있으므로 파일을 옮기거나 지우면 예외도 함께 사라진다.
둘째는 저장소 설정이다. ignores 명령으로 규칙, 파일 경로, 특정 값을 예외로 등록하면 저장소 전체에 적용된다. 이유를 함께 남길 수 있어 나중에 왜 열어 두었는지 추적된다.
두 방법 모두 공통점이 있다. 예외가 대화가 아니라 파일에 남는다는 것이다. 리뷰에서 구두로 합의한 예외는 다음 사람에게 전달되지 않지만, 저장소에 적힌 예외는 전달된다.
<!-- impeccable-disable overused-font -- 브랜드 지정 서체 -->
/* impeccable-disable-line dark-glow */
npx impeccable ignores add-value overused-font Inter --reason "Brand font"
npx impeccable ignores list커밋 전에 먼저 걸리게 하려면
CI는 마지막 그물이다. 이미 만들어 놓은 다음에 걸리면 되돌리는 비용이 든다. 그래서 지원되는 도구에는 편집 시점에 도는 훅이 함께 설치된다. UI 파일을 고치는 순간 검사기가 돌아 결과를 에이전트 쪽으로 되돌려 준다.
도구마다 개입 시점이 다르다. 커서는 잘못된 수정이 반영되기 전에 막는 쪽이고, 나머지는 수정이 끝난 뒤 결과를 알려주는 쪽이다. 어느 쪽이든 사람이 리뷰를 열기 전에 한 번 걸러진다는 점은 같다.
훅을 쓸지는 설치 과정에서 묻는다. 자동화가 편집 흐름을 끊는 걸 싫어한다면 훅은 빼고 CI만 걸어도 된다. 반대로 에이전트에게 화면을 많이 맡기는 팀이라면 훅 쪽이 더 자주 값을 한다. 만든 직후에 지적이 돌아와야 에이전트가 같은 맥락에서 고칠 수 있기 때문이다.
