실패 재현과 회귀 방지
200분 안팎
학습 목표
실패하는 사용자 테스트를 먼저 만들고 원인을 고칩니다.
개념
포커스 회귀를 실패로 고정합니다
공용 카드 렌더링을 정리한 뒤 목록 복귀에서 포커스가 검색어로 튄다는 문제가 생겼습니다. 클릭 사용자는 다시 원하는 카드를 누를 수 있어 결함을 놓치기 쉽습니다. 이 레슨에서는 이미 존재하는 상세 복귀 계약을 실패 테스트로 재현하고 최소한의 수정으로 되살립니다. 수정 완료를 판단하는 근거는 코드 인상이 아니라 같은 재현 조건에서의 기대 동작입니다.
재현 기록부터 구체화합니다. URL은 results.html?q=책®ion=busan이며 id=2 링크로 상세에 들어간 후 목록으로 돌아갑니다. 기대는 현재 카드 링크의 포커스, 실제는 검색어 입력의 포커스입니다. 제목과 검색 조건은 맞으므로 데이터 계산보다 복귀 전환을 조사합니다. 화면 폭이나 브라우저에 따른 차이가 있으면 함께 적고 최소 fixture로 문제를 좁힙니다.
제공 starter는 앞 모듈의 동작하는 앱에서 results.mjs의 복귀 분기를 바꾼 버전입니다. link를 찾고도 query.focus만 실행해 선택 위치를 잃습니다. 기존 회귀와 새 flow-test.mjs가 이 문제를 잡습니다. 실패가 새 검사에만 나오는지 이전 검사에도 나오는지 확인하면 결함이 새로운 명세 때문인지 이미 보존하던 계약을 깨뜨렸기 때문인지 구별할 수 있습니다.
검사는 먼저 검색 완료를 기다리고 현재 링크 참조를 저장합니다. 목록의 click을 상세 진입으로 전달한 뒤 상세 제목 포커스를 확인합니다. 다음으로 목록 복귀를 실행하고 렌더가 완료된 뒤 새로운 카드 링크를 얻습니다. activeElement가 그 새 링크인지 비교합니다. 제거된 옛 링크와 새 링크가 다르다는 단언도 있어 우연히 낡은 참조를 활성화하는 수정을 막습니다.
브라우저 회귀는 Tab과 Enter로 같은 전환을 반복합니다. 마우스로 복귀 링크를 누르면 활성 요소가 달라지는 조건이 생길 수 있으므로 키보드 경로를 따로 유지합니다. 이름이 같은 링크가 다시 렌더링돼도 locator는 현재 요소를 찾아 검사합니다. 실제 제목 전환과 포커스와 카드 내용이 함께 맞는지를 기다려 비동기 완료 전에 성공을 판정하지 않습니다.
원인은 결과 목록의 수명과 연결됩니다. loading 상태에서 list.replaceChildren이 기존 노드를 제거하고 응답이 오면 카드가 새로 만들어집니다. 검색 조건과 선택 id는 살아 있지만 링크 객체는 계속 사용할 수 없습니다. 따라서 오래 보관할 값은 DOM 노드 대신 id입니다. 현재 DOM에서 그 id와 일치하는 링크를 찾아야 의미 있는 사용자 위치를 복구할 수 있습니다.
수정 위치는 render의 목록 복귀 분기입니다. focusId가 null이 아니면 a[data-event-id] 목록에서 같은 숫자 문자열을 찾고, 링크가 있으면 focus하고 없으면 query를 선택합니다. 카드 생성 함수마다 전역 포커스를 건드리면 검색 결과 렌더 중에도 임의 카드로 이동합니다. 포커스 정책은 이동 이유를 아는 상위 흐름이 결정하고 카드는 화면 내용을 생성하는 책임을 유지합니다.
실패 사례만 맞추는 수정에는 반례가 필요합니다. 상세를 보는 사이 목록 응답에서 id=2가 사라지면 복귀 대상은 존재하지 않습니다. 이때 null.focus를 호출하면 TypeError가 나고 사용자 위치도 잃습니다. 제공 검사는 두 번째 목록 응답을 []로 바꿔 검색어가 대체 위치가 되는지 확인합니다. 정상 복귀와 대상 소실을 함께 검사해야 변경의 범위를 설명할 수 있습니다.
직접 상세 링크로 접속한 경우도 기존 동작을 보존합니다. 이전 목록 히스토리가 없으면 history.back을 무작정 호출하지 않고 조건이 포함된 목록 URL을 사용합니다. 이번 수정은 그 경로가 완료된 뒤 현재 링크를 찾는 정책을 공유합니다. 이전 페이지가 외부 사이트였을 때 사용자를 뜻밖에 보내는 변경은 필요하지 않습니다. 기존 상세 진입 테스트를 삭제하지 않는 이유입니다.
첫 실행의 실패 메시지는 관찰 근거로 남깁니다. “복귀는 제거된 옛 노드가 아닌 새 링크” assertion이 실패하면 actual과 expected의 활성 요소를 확인합니다. TypeError가 먼저 발생하면 목표한 정책 실패까지 도달했는지 조사합니다. 모듈 import 오류나 브라우저 설치 실패는 red 단계의 유효한 근거가 아닙니다. 검사 환경이 준비되고 목표 계약에서 실패했는지를 구분합니다.
수정 후에는 좁은 검사부터 실행합니다. node --test flow-test.mjs로 재현과 대상 소실과 검색 조건을 확인하고 npm test로 기존 HTML·CSS·API·요청 순서 검사를 다시 실행합니다. 브라우저가 허용되는 환경에서는 npm run test:e2e를 이어서 실행합니다. 한 번 통과한 숫자를 복사하지 않고 실제 실행한 명령과 종료 결과를 변경 기록에 적습니다.
검사 기대값을 query로 바꾸면 현재 starter도 통과할 수 있지만 사용자 계약을 포기하는 변경입니다. 모든 카드에 자동 focus를 넣으면 마지막 카드가 선택돼 재현만 우연히 가려질 수 있습니다. 시간 지연을 늘리는 방법도 잘못된 대상 선택을 고치지 못합니다. 코드 수정이 재현 원인과 연결되는지 설명하고 불필요한 상태 추가나 공용 카드 변경을 피합니다.
재시도 포커스 정책은 복귀와 다른 전환입니다. 현재 앱은 다시 시도 버튼에 있던 의도를 로딩 동안 기억하고 성공 시 검색어로 옮깁니다. 상세 복귀에서는 선택한 카드가 목적지입니다. 모든 완료에서 하나의 공통 위치로 이동시키면 한 정책은 맞아도 다른 정책을 깨뜨립니다. 상태 전환의 출발 행동과 기대 다음 행동을 함께 적으면 서로 다른 목적지를 혼동하지 않습니다.
회귀 기록은 문제·재현·원인·수정·검증·남은 범위의 순서로 씁니다. “목록 응답 뒤 노드가 교체되는데 검색어만 선택했다”처럼 원인을 코드의 생명주기와 연결합니다. 변경 파일은 results.mjs이며 현재 id 링크 선택과 소실 시 대체 규칙을 복구했다고 적습니다. 브라우저가 PENDING이면 모델 통과와 실제 사용자 화면의 확인 범위를 별도 문장으로 남깁니다.
미션 starter는 m07 solution 전체에 검사 누락과 모바일 계획 누락과 포커스 회귀를 함께 넣었습니다. 먼저 assertSearch에서 조건과 카드가 같은지 확인하고, audit-plan에서 검사 환경을 채우고, render의 복귀 분기를 고칩니다. 기존 자료와 mock 데이터는 출발점을 유지합니다. 제공 MANUAL.md에는 아직 실행하지 않은 항목이 미확인으로 되어 있으며 학습자가 실제 관찰 뒤 갱신합니다.
사람이 보는 완료 기준도 있습니다. 수정 이유를 테스트 이름과 연결해 말할 수 있는지, 실패를 숨기는 기대값 변경이 없는지, 검색 조건과 링크 동작이 보존됐는지 설명합니다. 자동 검사에 사람이 채운 수동 기록의 진실성을 판정하게 하지 않습니다. 넓은 성능 개선이나 디자인 변경은 이 결함 수정의 근거가 아니므로 다음 모듈에서 별도로 측정합니다.
마지막으로 원래 제보 순서대로 재확인합니다. 부산의 책 검색에서 상세로 갔다가 돌아오고, 해당 카드에서 다시 다른 탐색을 시작할 수 있는지 봅니다. 사라진 행사에서는 입력으로 돌아가 새 조건을 넣을 수 있어야 합니다. 이 두 문장이 회귀 검사의 업무적 의미입니다. 더 읽기에서는 디버깅 도구를 확장하되 이번 수정의 재현 조건과 검사 근거는 프로젝트 파일에 남깁니다.
따라하기
노드 대신 id로 복귀 위치를 찾습니다
아래는 DOM 없이 선택 정책만 확인하는 독립 예제입니다. 현재 목록에 선택 id가 있는 경우와 없는 경우를 비교합니다.
function target(ids,selected){return ids.includes(selected)?'event-'+selected:'query';}console.log(target([2,3],2));console.log(target([3],2));실행 결과
event-2 query
회귀를 먼저 실패시킵니다
starter 루트에서 실행합니다. 복귀 검사에서 actual 입력과 expected 현재 카드가 다른 assertion을 찾습니다. 실행·import 오류라면 환경부터 해결합니다.
node --test flow-test.mjs복구 분기를 수정하고 전체 모델을 검사합니다
assets/results.mjs에서 focusId의 현재 링크를 찾은 뒤 링크 또는 query에 focus합니다. 기존 테스트를 유지하고 실행 근거를 기록합니다.
npm test실제 키보드 재현을 마무리합니다
외부 환경에서 같은 실패 흐름과 대상 소실 정책을 확인합니다. 브라우저 미실행은 PENDING으로 남기고 MANUAL.md에 수정 원인과 다음 확인을 적습니다.
bash check.sh확인 문제
실습
results.mjs의 복귀 포커스 TODO를 먼저 실패 테스트로 재현합니다. 현재 id의 링크 또는 query로 포커스를 복구하고 대상 소실 반례와 전체 회귀를 유지합니다. npm test로 모델을 먼저 검사합니다. 브라우저 설치 후 bash check.sh는 모델과 Playwright를 모두 실행합니다. 서버는 harness가 열고 close로 닫으며 이 샌드박스의 브라우저 판정은 PENDING입니다.
실행 명령
bash check.sh
기대 결과
npm test의 의도된 starter assertion 실패·solution 모델 전부 통과; 설치 후 Playwright starter 실패·solution 전부 통과는 외부 검증 PENDING
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 키보드만으로 폼을 사용하는 흐름을 설명합니다.
- 컴포넌트의 상태 위치를 정한 사례를 설명합니다.