스모크 테스트가 다시 중요해진 이유: 타입체크를 통과하는 결함들
며칠 전 공개 문서 세 편을 발행했습니다. 타입 체크가 통과했고, 테스트도 통과했고, 배포도 성공했습니다. 그리고 화면에서는 문장 하나가 가로줄로 그어진 채 나갔습니다.
원인은 마크다운 렌더러였습니다. GFM 은 물결표 하나로 감싼 것도 취소선으로 읽는데, 한국어로 숫자 범위를 쓰면 물결이 반드시 짝을 이룹니다. "5~10페이지 회사 홈페이지가 260~570만원"이라고 쓰면 그 사이가 통째로 지워진 것처럼 보입니다. 검사는 전부 초록색이었고, 잡아낸 것은 화면을 본 사람이었습니다.
검사가 통과한 다섯 건
이런 일이 부쩍 늘었다는 느낌이 있어서 기록을 뒤져 봤습니다. AI 자동화 플랫폼에 패턴 모듈을 대량으로 붙이던 때의 작업 노트에 이렇게 적혀 있었습니다.
표본을 실제로 실행하라. 결함 5건 중 5건이 실행에서만 드러났다(타입체크·린트는 전부 통과했다).
다섯 건이 어떤 것이었냐면, 스키마의 description 만 믿고 프롬프트에 필드별 지시를 안 썼더니 모델이 조항명 자리에 "5" 를 적어 넣었고, 개수 상한만 주고 하한을 안 줬더니 표를 한 행만 만들었고, 점수 구간을 숫자로 안 박았더니 모델이 곧바로 0점을 골랐습니다. 전부 타입이 맞고 문법이 맞고 규칙에 안 걸리는 코드입니다. 실행해야만 틀린 게 보입니다.
저는 이 비율이 우연이 아니라고 봅니다. 타입체크와 린트는 형태를 봅니다. 이 함수가 저 타입을 받는가, 이 변수가 쓰이는가. 그런데 AI 가 쓴 코드는 형태가 거의 항상 맞습니다. 모델이 잘하는 것이 정확히 그 부분이라서입니다. 틀리는 쪽은 의도입니다. 문법이 아니라 "이 프롬프트가 정말 저 결과를 내는가"이고, 그건 돌려 봐야 압니다.
로컬에서 재현되지 않는 결함
형태 검사가 못 잡는 것이 하나 더 있습니다. 환경입니다.
8월 27일에 문서 상세 페이지가 통째로 500 을 냈습니다. 마크다운을 정제하는 라이브러리가 내부적으로 jsdom 을 부르는데, 그 의존성 사슬에 ESM 전용 패키지가 들어와 있어 서버리스 함수가 require 로 로드하지 못했습니다.
이상한 것은 로컬에서 멀쩡했다는 점입니다. 로컬 Node 24 는 require(ESM) 을 허용하기 때문에 거기서는 재현되지 않습니다. 타입 체크도 빌드도 로컬 테스트도 전부 통과합니다. 배포된 함수에서만 죽습니다.
목록 페이지가 멀쩡했던 것이 진단을 늦췄습니다. 목록은 마크다운을 렌더하지 않아 그 경로를 지나지 않습니다. "일부만 죽는다"는 증상은 대개 코드가 아니라 경로를 가리킵니다.
고친 뒤에 테스트를 하나 넣었습니다. 그 조건을 일부러 재현하는 것입니다.
node --no-experimental-require-module -e "require('isomorphic-dompurify').sanitize('<b>x</b>')"로컬 Node 의 관대함을 끄고 서버리스 함수와 같은 조건으로 한 번 불러 봅니다. 이 한 줄이 타입 시스템 전체가 못 잡은 것을 잡습니다. 검사의 정교함이 아니라 실행 조건이 같은가가 갈랐습니다.
스모크가 보는 것, 「떴다」가 아니라 「돈다」
스모크 테스트라는 말은 하드웨어에서 왔습니다. 회로에 전원을 넣고 연기가 나는지 본다는 뜻입니다. 정교한 검증이 아니라 최소한 켜지는가를 확인하는 단계입니다.
소프트웨어에서 이 단계가 자주 형식적으로 취급됩니다. 컨테이너가 떴는지, 프로세스가 살아 있는지만 보고 넘어갑니다. 그건 전원이 들어왔다는 확인이지 회로가 돈다는 확인이 아닙니다.
전자서명 서비스의 스모크 스크립트 첫 줄에 이렇게 적어 두었습니다.
API 왕복 확인 - "컨테이너가 떴다"가 아니라 "앱 → DB 가 실제로 돈다"를 검증한다.
헬스 엔드포인트가 200 을 주는 것과, 그 요청이 데이터베이스까지 갔다 오는 것은 다릅니다. 앞은 프로세스가 살아 있다는 뜻이고 뒤는 시스템이 이어져 있다는 뜻입니다. 연결 문자열이 틀렸거나 방화벽이 막혔거나 스키마가 안 맞으면 앞은 통과하고 뒤는 실패합니다.
막혀 있어야 할 것의 확인
같은 스크립트에서 제일 마음에 드는 부분은 통과를 보지 않고 차단을 보는 쪽입니다.
서명 서비스는 고객이 계정 없이 링크로 들어와야 해서 통째로 인증 뒤에 둘 수가 없습니다. 그래서 무엇이 열려 있고 무엇이 닫혀 있는지가 설계의 전부입니다. 스모크는 그걸 봅니다. 관리 API 가 인증 없이 열리지 않는지, 접근코드를 넣기 전에 문서 내용이 나가지 않는지를 매번 확인합니다.
보안 헤더도 같은 자리에 있습니다. 한동안 운영 응답에 CSP 헤더가 아예 빠져 있었습니다. 고치는 것은 설정 한 줄이었지만, 고친 뒤에 검사를 넣지 않으면 다음에 또 조용히 사라집니다. 없어진 것을 알아채기가 어렵기 때문입니다. 있는 것이 잘못되면 에러가 나지만, 없어야 할 것이 없거나 있어야 할 것이 없으면 아무 일도 일어나지 않습니다.
이 종류의 결함은 사용자가 신고해 주지 않습니다. 화면이 멀쩡히 뜨니까요.
데이터를 만들지 않는 스모크
같은 스크립트에 이런 제약도 적혀 있습니다.
이 서비스의 스모크는 데이터를 만들지 않는다. 서명 요청은 문서를 얼려야 만들어지고, 그건 실제 문서가 대상이라 검증용으로 찍을 수 있는 것이 아니다.
스모크를 붙이려다 막히는 지점이 대개 여기입니다. 제대로 확인하려면 데이터를 만들어야 하는데, 운영 데이터에 검증용 찌꺼기를 남길 수는 없습니다.
그때 답은 "그러면 스모크를 포기한다"가 아니라 만들지 않고도 볼 수 있는 것을 고르는 것입니다. 위의 차단 검사가 그렇습니다. 아무것도 생성하지 않고, 열려 있으면 안 되는 문을 밀어 보기만 합니다. 실패하면 그것이 통과입니다.
화면을 보는 사람이 마지막 검사인 자리
맨 앞의 취소선 이야기로 돌아가면, 그건 스모크로도 안 잡혔을 것입니다. 페이지는 200 을 주고 제목도 맞고 표도 렌더됩니다. 글자에 줄이 그어졌다는 것만 다릅니다.
고친 뒤에는 테스트를 넣었습니다. 물결 하나는 취소선이 아니고 둘은 취소선이라는 두 줄입니다. 이제 이 결함은 다시 나가지 않습니다. 다만 그건 알고 난 뒤에 넣은 것이고, 처음 발견한 것은 사람의 눈이었습니다.
저는 이 순서가 정상이라고 봅니다. 자동 검사는 이미 아는 실패를 막는 장치이지 모르는 실패를 찾아 주는 장치가 아닙니다. 새로운 결함은 늘 사람이 먼저 봅니다. 그 뒤에 그것을 검사로 바꿔 두면 같은 것이 두 번 나가지 않습니다.
그래서 스모크의 값은 검사 항목 수가 아니라 거기 쌓인 사고의 목록에 있습니다. 저 스크립트의 CSP 검사 줄은 CSP 를 알아서 넣은 것이 아니라 한 번 빠뜨렸기 때문에 있습니다. 재현 조건을 끄고 한 번 불러 보는 그 한 줄도 마찬가지입니다.
그래서 무엇을 넣는가
지금 기준으로 저는 세 가지를 봅니다.
끝에서 끝까지 한 번. 프로세스가 아니라 요청 하나가 저장소까지 갔다 오는 것을 봅니다. 연결이 끊어지는 지점은 대개 층과 층 사이입니다.
닫혀야 할 문 한두 개. 인증이 필요한 경로를 인증 없이 열어 보고 막히는지 봅니다. 이건 통과가 아니라 실패를 기대하는 검사라 잊기 쉽고, 그래서 조용히 무너집니다.
한 번 당한 것. 사고가 날 때마다 한 줄씩 늘립니다. 처음부터 완전한 목록을 설계하려 들면 아무것도 못 넣습니다.
셋 다 정교하지 않습니다. 정교한 검사는 타입 시스템과 단위 테스트가 이미 맡고 있고, 그것들이 못 보는 자리가 따로 있다는 것이 이 글의 요지입니다. 그 자리는 실제로 돌려 봐야만 보이는 곳이고, AI 가 코드를 더 많이 쓸수록 그 자리가 넓어집니다.
자동 검사가 언제 어떻게 도는지는 git push 후 자동 검사가 시작되는 이유에 정리해 두었습니다.
함께 읽기
- AI 코딩의 함정: 실패하지 않는 실수랜딩 페이지의 앱 소개 화면에 QR 코드를 붙이는 작업을 클로드에게 맡겼습니다. 결과물은 잘 돌아갔습니다. 타입 체크를 통과했고 프리뷰 배포도 깨끗했습니다.
- git push 후 자동 검사가 시작되는 이유: Continuous Integration(CI) 입문코드를 수정하고 로컬에서 잘 동작하는 것까지 확인했습니다. 이제 아래처럼 GitHub에 올립니다.
- 중복처럼 보이는 코드를 함부로 합치면 안 되는 이유오래 운영한 코드베이스를 점검하다 보면 비슷하게 생긴 코드 뭉치를 발견하고 "이거 중복이네, 합치자"라는 충동이 듭니다. 중복 제거는 리팩토링의 기본이라고 배우기도 했고, 지우는 작업은 손도 빠릅니다. 저는 얼마 전 그 충동대로 갔다가 큰일 날 뻔했습니다.
- 반복 코드 131줄을 정규식으로 한 번에 바꿔도 괜찮았던 이유혼자 운영하는 서비스는 점검을 자꾸 미루게 됩니다. 기능이 돌아가고 있으면 코드를 다시 들여다볼 이유가 생기지 않고, 그 사이에 같은 패턴이 계속 복사됩니다. 저도 얼마 전 하루를 잡고 운영 중인 서비스 코드를 전체 점검했는데, 관리자 API 라우트 파일 70개에 완전히 똑같은 인증 가드 한 줄이 131번 반복되고 있었…
- AI 도입 전에 물어야 할 것: ERP 를 바꿀 일인가, 앞뒤를 붙일 일인가AI 기능이 들어갔다는 업무 시스템으로 갈아탔습니다. 반년이 지났는데 현장에서는 여전히 거래처 카톡을 읽어 엑셀에 옮겨 적고 있습니다.