Next.js
React 검색창에서 이전 응답이 덮어쓴 결과를 AbortController로 막은 기록
빠르게 검색어를 바꿀 때 늦게 도착한 응답이 화면을 망가뜨린 원인과 취소 기준을 정리했다.
증상이 재현된 순서
관리자 화면에 제목 검색창을 붙인 뒤 r을 입력하고 곧바로 re를 입력했다. 네트워크 탭에서는 두 요청이 정상적으로 200을 반환했는데, 가끔 화면에는 더 짧은 r의 결과가 남았다. 처음에는 API 정렬 문제라고 생각했지만, 느린 요청을 흉내 내니 원인이 분명해졌다. 먼저 보낸 요청이 늦게 끝나면서 최신 상태를 덮어쓰고 있었다.
useEffect(() => {
fetch(`/api/posts?q=${encodeURIComponent(query)}`)
.then((res) => res.json())
.then(setResults);
}, [query]);
이 코드는 요청을 시작한 순서만 보장할 뿐, 응답이 도착하는 순서는 보장하지 않는다.
취소와 무시를 나눠서 처리하기
브라우저가 아직 처리하지 않은 이전 요청은 AbortController로 취소했다. 다만 서버가 이미 응답을 보낸 뒤일 수도 있으므로, 취소 오류를 일반 오류 화면으로 보여주지 않는 조건도 넣었다.
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(
`/api/posts?q=${encodeURIComponent(query)}`,
{ signal: controller.signal }
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setResults(await res.json());
} catch (error) {
if (error.name !== 'AbortError') setError(error);
}
}
load();
return () => controller.abort();
}, [query]);
검색어가 비어 있을 때 요청하지 않는 조건은 별도로 두었다. 여기서 헷갈린 지점은 abort()가 서버 작업까지 반드시 중단한다는 뜻은 아니라는 점이다. 이 화면에서 필요한 것은 이전 응답을 소비하지 않는 것이었고, 그 기준에는 클라이언트 취소만으로 충분했다.
다시 확인한 테스트 항목
- 느린 응답과 빠른 응답의 순서를 바꿔도 최신 검색어가 남는다.
- 새 검색어를 입력하면 이전 요청의 오류가 화면에 나타나지 않는다.
- 컴포넌트를 빠르게 이동해도 상태 업데이트 경고가 없다.
- 빈 검색어에서 불필요한 네트워크 요청을 만들지 않는다.
이번에는 요청 개수를 줄이는 것보다 화면에 반영할 응답의 기준을 먼저 정했다. 그 뒤에 디바운스를 추가하면 서버 부담도 낮출 수 있지만, 디바운스만으로 경합 문제가 사라지는 것은 아니므로 두 처리를 따로 보는 편이 안전했다.