조직 식별 정보, 호스트·계정, 티켓 번호, 회원 데이터는 제외했다. 매물번호는 테스트용 합성값이다. 구조와 코드 패턴은 실제 그대로다.
가드는 이렇게 생겼었다.
if (filters.isLIVE !== 'all') params.isLIVE = filters.isLIVE;
서버가 받는 이름은 is_live 다. 이 API 는 snake_case 와 camelCase 를 둘 다
받는데, is_live 의 camelCase 는 isLive 다. isLIVE 는 둘 다 아니다.
git log -S 로 보니 최초 구현 커밋부터 그 이름이었다. 163일.
아무도 몰랐던 이유
목록 쿼리에서 값을 대입하는 지점이 19개인데, 이 하나만 관례에서 벗어나 있었다.
searchUserType contractType buildingType roomType naverStatus
houseCDate saleNumber hidxNumber … ← 정상 camelCase
verification_type ← snake_case, 서버 문서와 동일
isLIVE ← 어느 쪽도 아님
한 파일 안에서 두 관례가 섞여 있는 것은 읽으면 보인다. 세 번째 관례는 안 보인다. 오타가 아니라 하나의 일관된 스타일처럼 생겼기 때문이다.
그런데 이름이 틀렸다는 사실만으로는 163일이 설명되지 않는다. 화면이 조용했던 이유가 따로 있다.
이 서버는 모르는 쿼리 파라미터를 400 으로 거절하지 않고 조용히 무시한다. 그러면 응답은 필터 없는 전체 목록이다. 그리고 화면에서 "필터가 안 걸림"과 "조건에 맞는 게 전부임"은 구별되지 않는다.
그래서 테스트를 화면이 아니라 전선에 걸었다.
const q = new URLSearchParams(listQueries[listQueries.length - 1] ?? '');
expect(q.get('is_live')).toBe('NOT_LIVE');
expect(q.has('isLIVE')).toBe(false); // 옛 이름이 남아 있지 않은지도 본다
요청을 가로채 쿼리스트링을 쌓아두고 직접 읽는다. 화면은 보지 않는다.
두 번째 줄이 첫 번째 줄보다 중요하다. 새 이름이 나가는 것만 확인하면, 옛 이름을 지우지 않고 추가해도 통과한다. 서버가 모르는 파라미터를 무시하니 그 상태로도 동작하고, 다음 사람은 두 이름을 다 유지해야 한다고 배운다.
테스트는 실패를 본 뒤에야 테스트다
오늘 고친 것마다 수정을 일부러 되돌려 테스트가 실패하는지 먼저 확인했다. 일곱 번.
Expected 1 Received 0 상세 표에 이전 매물번호 행이 없음
Expected "2000-01-01" Received "2026-08-04" 등록일이 1개월로 좁혀짐
Expected "9100" Received null 클릭 경로에서 식별자가 안 나감
Expected "9100" Received null 값이 입력칸으로 안 들어감
Expected "created" Received "hidx" 검색이 정렬을 되돌림
Expected disabled Received enabled 허용 밖 상태값이 눌림
Expected "NOT_LIVE" Received null 필터가 파라미터를 안 보냄
이 습관이 오늘 두 번 값을 했다.
한 번은 남의 버그를 잡았다. Expected "2000-01-01" Received "2026-08-04" —
처음 받은 URL 에 그 날짜가 박혀 있었는데 그때는 연결하지 못했다. 되돌려
돌리니 같은 값이 그대로 나왔다.
한 번은 내 테스트의 버그를 잡았다. 정렬 테스트를 되돌렸는데 예상과 다른 지점에서 실패했다.
await expect.poll(() => listQueries.length).toBeGreaterThan(1); // 틀렸다
초기 로드가 요청을 2건 내면 이 폴링은 클릭 직후 즉시 통과하고 낡은 마지막 쿼리를 읽는다. 검색 부분은 클릭 전 개수를 캡처해뒀는데 정렬 부분만 빠뜨렸다.
통과하는 테스트는 아무것도 증명하지 않는다. 실패하는 걸 본 테스트만 증명한다. 그리고 그 확인 과정이 테스트 자신의 결함도 드러낸다.
기능보다 오래 살아남은 테스트는 틀린 편을 든다
"링크로 들어오면 필터에 값이 안 보인다"는 지적을 받고 필터 패널에 식별자 입력칸을 추가했다. 통과, 커밋.
사이드이펙트 점검 중에 이걸 발견했다.
2026-06-01 전체 목록 검색에서 식별자 입력 필드 제거
3개월 전에 의도적으로 지운 필드를 되살린 것이었다. 이유는 단순했다 — 매물번호가 곧 그 식별자다. 같은 값을 두 칸으로 받고 있었고, 그 커밋이 중복을 정리한 것이었다.
내가 놓친 데는 저장소 쪽 사정도 있었다. e2e 스펙에 그 필드가 표시되는지 확인하는 테스트가 아직 남아 있었다. 제거 때 함께 정리되지 않았다. 코드는 "없다"고 하고 테스트는 "있어야 한다"고 하는 모순 상태였고, 그 스위트가 배선 누락으로 안 돌던 기간이라 아무도 못 봤다.
최종 해결은 입력칸을 만들지 않는 쪽이었다. 링크 진입 값을 기존 매물번호 필터에 넣으면 그 칸에 그대로 표시되고 지울 수도 있다. 칸을 늘리지 않고 문제가 사라졌다.
무언가를 추가하기 전에 그게 삭제된 적 있는지 본다.
git log -S "<정확한 심볼>" -- <파일>한 줄이면 된다.
센티넬은 그게 특별한 값인 줄 모르는 코드에서 터진다
등록일 필터의 "전체" 가 이렇게 구현돼 있었다.
export const DATE_RANGE_OPTIONS = [
{ value: '2000-01-01', label: '전체' }, // 아무 건보다 과거인 날짜를 하한으로
{ value: '1m', label: '1개월 이내' },
];
옵션 값에 상대 토큰과 절대 날짜가 섞여 있고, 변환 함수 첫 줄이 이 값만
예외 처리한다. 형제 화면 둘은 같은 자리에 value: '' 를 두고 파라미터를 아예
안 보낸다. 이 화면만 예외였다.
이 저장소는 반대 방향 센티넬도 쓴다 — 기간 제한 없음을 9999-12-31 로
저장한다. 그리고 그쪽에서 이미 사고가 났던 흔적이 코드에 남아 있다. 타임라인
주석이 무제한 종료일을 진짜 종료일로 믿어 축 범위가 깨진 문제를 기록하고 있다.
센티넬의 비용은 그게 특별한 값인 줄 아는 코드에서는 0 이고, 모르는 코드에서 전액 청구된다. "전체"는 매직 값이 아니라 파라미터의 부재여야 한다.
이건 링크 진입 버그의 절반이기도 했다. 식별자로 들어올 때 등록일 기본값
1m 이 그대로 따라가서, 오래된 건이 날짜 조건에서 걸러져 0건이 됐다. 대상이
정의상 오래된 항목이라 거의 항상 터지는 조합이었다.
표시 문자열이 그대로 전선에 실릴 때
상태 변경 API 는 code 로 숫자 코드가 아니라 한글 라벨을 받는다.
POST …/naver-status body { "code": "검증실패" }
허용값 등록완료 / 등록취소 / 등록실패 / 검증실패
그 외는 400 INVALID_INPUT_VALUE
그런데 코드→라벨 표가 두 곳에 따로 있었다. 상수 파일의 필터 옵션과, 컬럼 정의 파일 안의 지역 맵. 값이 같아서 지금까지는 문제가 없었다.
표시용 라벨이 갈라지면 보통은 화면 글자만 달라진다. 그런데 이 라벨은 API 로 나가는 값이다. 한쪽만 고치면 400 을 맞는다. 지역 맵을 지우고 상수 하나로 합쳤다.
어떤 문자열이 표시 값이면서 전선 값이면 정의는 정확히 하나여야 한다.
눌릴 것처럼 보이지만 아무 일도 안 하던 버튼
상태 변경 UI 는 새로 만들 게 없었다. 섹션과 버튼 컴포넌트가 이미 있었고
onChange prop 도 받게 되어 있었다. 아무도 넘기지 않아서 표시 전용이었다.
그런데 모든 값이 <button> 이고 cursor-pointer 와 hover 스타일까지 있었다.
눌러도 아무 일이 없다.
배선하면서 changeable 을 추가해 서버가 받는 값만 활성으로 두고 나머지는
disabled 로 내렸다. 서버가 받는 값이 표기 값보다 좁다는 게 요점이다 —
노출상태는 표기 8종 중 3종만, 네이버는 9종 중 4종만 변경 가능하다. 현재 값도
비활성이다. 같은 값을 다시 보내면 서버 왕복만 하고 바뀌는 게 없다.
화면은 정상이라고 말했다
오늘 고친 일곱 개가 한 가지를 공유했다. 에러도, 빈 화면도, 스피너도 없었다. 컴파일도 되고 콘솔도 깨끗했다. 화면은 정상이라고 말했고, 매번 그 말이 맞아 보였다.
그래서 남기는 규칙은 전부 "무엇을 증거로 삼을 것인가"에 관한 것이다.
- 필터·검색은 나가는 쿼리로 검증한다. 모르는 파라미터를 조용히 무시하는 서버에서는 렌더 결과가 증거가 못 된다
- 수정을 되돌려 테스트가 실패하는 걸 본다. 안 보면 그 테스트가 무엇을 지키는지 모른다
- 추가 전에 삭제 이력을 본다. 3개월 전 결정을 되살리는 건 30초면 막을 수 있었다
- 기능을 지울 때 그 기능의 테스트도 지운다. 남은 테스트는 나중에 틀린 편을 든다
- "전체"는 파라미터 부재로 표현한다
- 표시 문자열이 전선 값이면 정의를 하나로 유지한다
아직 증거가 없는 것
mocked 테스트는 프런트 경로만 증명한다. 실호출로 가려야 하는 게 남았다.
| 항목 | 근거 상태 |
|---|---|
| 노출상태 변경 허용값 3종 | 서버 문서 없음. body 샘플뿐 |
| 필터의 세 번째 상태값 | 문서상 허용값 목록에 없음 |
| 매물번호 파라미터가 식별자를 받는지 | 문서 설명뿐 |
| 목록 응답의 이전 매물번호 필드 | 응답 필드표에 없음 — 서버 추가가 필요할 수 있다 |
상태 변경 하나만 문서에 허용값과 400 응답까지 명시돼 있어 근거가 확실하다. 나머지 넷은 지금 동작하지만 왜 동작하는지 문서로 확인하지 못한 것이다. 그것도 화면은 정상이라고 말하는 상태의 한 종류다.