CI는 초록불인데 화면은 깨져 있었어요 — 프론트 훅이 조용히 건너뛰는 문제
로컬에선 잡히던 프론트엔드 검사가 자동 실행 환경에서만 통과하는 false-green 현상을 추적하고, 화면 단에서 증거를 남기는 구조로 바꾼 기록이에요.
컴포넌트를 수정하고 커밋했는데, 자동 검사 로그에는 전부 통과 표시가 떠 있었어요. 그런데 배포된 화면을 열어 보니 버튼 정렬이 무너져 있더라고요. 로컬에서 같은 명령을 돌려 보니 바로 실패가 잡혔어요. 같은 코드, 같은 명령인데 결과가 달랐어요.
왜 로컬에서만 잡혔을까
원인은 코드가 아니라 실행 경로였어요. 제가 로컬에서 쓰던 검사는 커밋 직전에 걸리는 훅에 붙어 있었는데, 이건 사람이 터미널 앞에 앉아서 명령을 칠 때만 동작하는 구조였어요. 자동화 스크립트가 무인으로 돌 때는 이 훅 자체가 실행되지 않았고요.
훅이 실패한 게 아니라 아예 호출되지 않았다는 게 핵심이에요. 실패하면 빨간불이 뜨지만, 호출되지 않으면 아무 로그도 안 남아요. 그래서 "검사 없음"이 "검사 통과"처럼 보였어요.
프론트엔드에서 특히 위험한 이유
백엔드는 API가 500을 뱉으면 어디선가 알람이 울려요. 그런데 프론트엔드는 깨져도 서버 응답은 200이에요. 레이아웃이 무너지거나 폼 검증이 빠져도, 화면을 직접 열어 보지 않으면 아무 신호가 없어요.
제가 놓쳤던 것들을 정리해 보면 이래요.
- 타입 오류가 있는데 번들은 그대로 생성됨
- 사용하지 않는 import가 남아 번들 용량만 커짐
- 접근성 속성이 빠졌는데 렌더링은 정상
- 다국어 텍스트가 길어져 버튼을 밀어냈는데 에러는 0건
전부 "돌아가긴 하는" 상태예요. 그래서 검사가 유일한 방어선인데, 그 검사가 조용히 빠져 있었던 거죠.
검사를 스크립트 안으로 옮겼어요
해결 방향은 단순했어요. 검사를 훅에 얹지 말고, 빌드 스크립트 자체에 넣는 것이에요. 훅은 환경에 따라 건너뛰지만, 스크립트 본문은 누가 실행하든 그대로 돌아가니까요.
package.json의 빌드 명령을 이렇게 바꿨어요.
{
"scripts": {
"verify": "tsc --noEmit && eslint . && vitest run",
"build": "pnpm verify && next build"
}
}&&로 연결했기 때문에 앞 단계가 실패하면 뒤가 실행되지 않아요. 훅에 의존할 때는 4개 경로 중 1개에서만 검사가 돌았는데, 스크립트로 옮기고 나니 전부 동일하게 돌더라고요. 바꾸는 데 10분쯤 걸렸어요.
화면 증거를 하나 남기기
여기에 하나 더 붙였어요. 명령 결과가 통과여도, 실제 화면 상태를 확인하는 단계를 하나 넣는 거예요. Playwright로 주요 페이지 2곳을 열어 스크린샷을 저장하게 했어요.
test('메인 화면이 정상 렌더링돼요', async ({ page }) => {
await page.goto('/')
await expect(page.getByRole('button', { name: '시작하기' })).toBeVisible()
await page.screenshot({ path: 'artifacts/home.png' })
})로그의 "통과"보다 스크린샷 1장이 더 믿을 만해요. 지표와 실제 화면이 어긋날 때는 화면을 믿는 쪽이 맞았어요.
- 훅 대신 스크립트
- 명령 연결로 중단 보장
- 화면 증거 1개 필수
정리하면
- 훅은 환경에 따라 건너뛰어요. 검사는 스크립트에 넣으세요
- 명령은
&&로 이어서 실패가 뒤로 전파되게 하세요 - 통과 로그만 믿지 말고, 화면 캡처를 1개는 남기세요
무료 금융계산기인 플랜씨(plan-c)를 만들 때도 같은 패턴을 썼어요. 로그인 없이 쓰는 계산기라 입력 폼이 깨지면 바로 이탈로 이어지거든요. 그래서 계산 결과 화면을 캡처하는 검사를 빌드에 붙여 뒀어요. 이 구조를 적용해 본 뒤로는 "통과했는데 깨졌다"는 상황이 안 나오고 있어요.
검사가 실패하는 건 괜찮아요. 검사가 실행조차 안 되는 게 문제예요. 🙂
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.