요청 취소와 입력 지연
180분 안팎
학습 목표
AbortController와 디바운스로 불필요한 검색을 줄입니다.
개념
요청 취소와 입력 지연
검색창에 지역 행사명을 한 글자씩 입력할 때 매번 요청하면 대부분의 응답은 사용자가 이미 버린 조건에 대한 결과입니다. 디바운스는 연속 입력이 멈춘 뒤 마지막 조건만 실행하는 예약 정책입니다. 취소는 이미 시작한 요청을 멈추도록 신호를 보내는 정책입니다. 둘을 함께 쓰면 시작 전 낭비와 시작 후 낭비를 각각 줄일 수 있지만 어느 쪽도 최신 응답 검사 자체를 대신하지는 않습니다.
AbortController는 새 요청을 만들 때 함께 생성합니다. fetch에 controller.signal을 전달하고 새 조건이나 화면 이탈에서 abort를 호출합니다. 취소된 controller를 다음 요청에 재사용하면 signal이 이미 aborted이므로 새 작업도 취소된 상태로 시작합니다. controller는 요청마다 새로 만들고 해당 요청의 지역 변수로도 보관합니다. 공용 변수는 현재 취소할 요청을 가리키는 역할만 맡깁니다.
일반적인 fetch 취소는 AbortError로 거부될 수 있습니다. 이 요청은 사용자 선택 때문에 중단한 것이므로 연결 오류나 서버 오류 문구로 바꾸지 않습니다. catch에서는 signal.aborted와 오류 name을 확인하고 최신 번호도 검사합니다. 이번 모의 요청은 중단에 협조하지 않는 경우도 포함합니다. 그래서 signal을 보냈다는 검사와 늦은 결과를 반영하지 않는 검사를 별도로 둡니다.
디바운스는 timer 변수 하나를 클로저에 보관합니다. 새 입력마다 기존 timer를 clearTimeout으로 제거하고 마지막 조건을 캡처한 새 타이머를 등록합니다. 입력 이벤트 안에서 매번 별도의 디바운스 함수를 만들면 이전 타이머를 알 수 없어 모든 글자 검색이 실행됩니다. 생명 주기는 화면 session과 같게 두고 리스너에서는 동일한 schedule 함수를 반복 호출합니다.
이 실습은 trailing 방식으로 250ms 후 한 번 실행합니다. 맨 처음 글자를 즉시 검색하는 leading 동작은 없습니다. 시간 값은 예제 정책이며 제품 전체의 최적값이라고 주장하지 않습니다. 연속 입력 사이가 100ms라면 두 번째 입력 후 다시 250ms를 세므로 처음 입력 기준의 250ms에 실행하지 않습니다. 테스트는 실제 벽시계 대기 대신 주입된 clock을 앞으로 이동시켜 이 규칙을 확인합니다.
중요한 경계는 새 입력 시점과 실제 새 요청 시작 시점 사이입니다. A가 진행 중일 때 B 입력을 예약하면 B 요청은 250ms 뒤 시작합니다. 그 사이 A 응답이 와도 A는 이미 오래된 의도입니다. schedule은 입력을 받은 즉시 번호를 증가시키고 이전 controller를 취소합니다. 실제 요청이 시작할 때만 번호를 바꾸는 구현은 디바운스 대기 동안 A 카드가 잠깐 나타나는 문제를 남깁니다.
가짜 clock에는 setTimeout·clearTimeout·tick이 있습니다. 실제 타이머 API의 필요한 부분만 흉내 내며 레이아웃이나 네트워크 속도는 재현하지 않습니다. tick(249)에서 요청 횟수 0, 이어 tick(1)에서 요청 횟수 1이 되어야 합니다. 새 입력 뒤에는 남은 timer가 한 개여야 합니다. 테스트를 1초 sleep으로 바꾸면 느려질 뿐 취소 경계가 더 정확해지지 않습니다.
Enter로 검색을 제출하면 사용자가 기다림을 끝내고 실행하겠다는 뜻이므로 run을 즉시 호출합니다. run은 대기 타이머를 제거하고 현재 조건을 실행합니다. 그렇지 않으면 Enter 검색과 250ms 뒤 예약 검색이 중복됩니다. 지역 선택 change는 즉시 검색하며 텍스트 input만 지연합니다. 상세 진입과 목록 복귀도 지연하지 않습니다. 서로 다른 UI 행동을 한 가지 입력 정책으로 덮지 않습니다.
한글 조합 입력에서는 input 이벤트가 최종 문자열 전에도 발생할 수 있습니다. 미션은 compositionstart에서 조합 중 플래그를 켜고 조합 중 input을 건너뜁니다. compositionend에서 최종 값을 예약하고 뒤따르는 input은 같은 타이머를 교체하므로 한 건으로 모입니다. 브라우저마다 이벤트 관찰이 필요하므로 Node 모델 통과로 실제 IME 동작을 증명했다고 쓰지 않습니다.
화면 이탈은 새 검색보다 강한 종료입니다. dispose는 disposed를 설정하고 대기 타이머를 제거하며 현재 요청에 취소 신호를 보냅니다. 이후 run·schedule·retry 호출도 무시합니다. dispose를 두 번 호출해도 오류가 없어야 합니다. 리스너 해제는 DOM 어댑터가 담당하고 요청 정리는 controller가 담당합니다. 타이머만 지우고 이미 시작된 Promise의 완료를 방치하지 않습니다.
local 과제의 controller.cjs는 createSearch를 내보냅니다. run은 Promise를 반환하고 schedule은 예약만 합니다. retry는 마지막 URL과 options를 저장해 재사용하되 busy일 때 중복 실행하지 않습니다. options.detail처럼 호출마다 달라지는 값도 복사합니다. 채점 테스트는 URL·signal.aborted·발행 상태·timer 개수·늦은 쓰기를 확인하며 실제 네트워크나 외부 행사 서비스에는 접속하지 않습니다.
starter에서는 schedule의 타이머 정리와 즉시 무효화가 빠져 있습니다. 테스트 일부가 이미 통과하므로 전체 함수를 새로 지우지 말고 실패한 경계를 찾아 수정합니다. 249ms 경계 테스트의 요청 횟수가 1이면 첫 타이머가 살아 있는지 확인합니다. 대기 중 옛 완료 테스트에서 success가 나오면 번호 증가가 새 입력이 아닌 새 요청 시점에만 있는지 확인합니다. solution 파일은 비교할 때 펼쳐 봅니다.
오류 화면으로 순간 이동하는 경우에는 error.name과 signal 상태를 함께 살펴봅니다. AbortError를 단순 문자열 message 비교로 판별하면 런타임 문구 차이에 의존합니다. 반대로 모든 오류를 취소라고 삼키면 현재 요청의 500이나 계약 오류가 사라집니다. 취소는 조용히 종료하지만 실제 실패는 재시도할 수 있는 상태로 남기는 분기를 테스트의 기대 출력으로 확인합니다.
실습 완료 뒤 npm test의 통과 개수만 적지 않고 새 입력 시점에 이전 작업을 무효화한 이유를 설명합니다. 가짜 clock 테스트는 시간 정책의 논리를 검증하며 실제 브라우저 입력 지연 체감이나 서버 처리량은 측정하지 않습니다. 미션에서 npm start를 사용해 입력·Enter·지역 변경·화면 이탈을 관찰하고 사용한 터미널을 닫기 전 Ctrl+C로 직접 종료합니다. 이번 작성 과정의 자동 테스트는 서버를 시작하지 않습니다.
취소 API의 동작은 MDN AbortController 안내에서 확인할 수 있습니다. 성능 장에는 디바운스와 다른 최적화 방법이 함께 설명되어 있지만 여기서는 예약 하나의 수명과 요청 한 건의 수명에 집중합니다. 자원을 정리하는 코드와 결과 반영 권한을 확인하는 코드가 서로 다른 이유를 말할 수 있으면 이번 목표를 달성한 것입니다.
따라하기
취소 신호의 수명을 확인합니다
새 요청에는 새 controller를 준비합니다. 서버나 fetch 없이 signal 상태만 관찰합니다.
const c=new AbortController();console.log(c.signal.aborted);c.abort();console.log(c.signal.aborted);const next=new AbortController();console.log(next.signal.aborted);실행 결과
false true false
예약 하나만 유지합니다
작은 가짜 clock으로 타이머 교체와 마지막 조건을 확인합니다.
let next=0;const jobs=new Map();let timer=null;const clock={setTimeout(fn){jobs.set(++next,fn);return next;},clearTimeout(id){jobs.delete(id);}};function schedule(q){if(timer!==null)clock.clearTimeout(timer);timer=clock.setTimeout(()=>console.log(q));}schedule('책');schedule('책 모임');console.log(jobs.size);for(const fn of jobs.values())fn();실행 결과
1 책 모임
취소와 실패를 나눕니다
실제 오류 전달은 local 테스트에서 검사합니다. 이 코드는 표시 정책의 분기를 확인합니다.
for(const name of ['AbortError','TypeError'])console.log(name,name==='AbortError'?'안내 없음':'오류 안내');실행 결과
AbortError 안내 없음 TypeError 오류 안내
TODO와 가짜 시간 테스트를 연결합니다
controller.cjs의 schedule을 완성하고 npm test를 실행합니다. order-test.cjs의 249ms 경계·새 입력 즉시 무효화·Enter·dispose 검사를 각각 읽습니다. Expected와 Actual의 요청 횟수·상태 차이를 확인하고 README에 수정 근거를 적습니다.
확인 문제
실습
controller.cjs의 schedule TODO를 완성합니다. 이전 타이머 제거, 입력 즉시 무효화, 마지막 조건 실행을 구현합니다. npm ci 후 npm test로 확인하며 9개 테스트를 수정하지 않습니다. starter는 디바운스 관련 일부 검사에 실패합니다.
실행 명령
npm test
기대 결과
Node 테스트 9개 통과, 실패 0개
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 검색 요청이 순서와 다르게 도착할 때의 처리 방식을 설명합니다.