검색어를 빠르게 바꿨더니 이전 결과가 나중에 도착해 화면을 덮은 이유
문제 발생
검색어가 바뀔 때마다 결과를 가져왔습니다.
watch(query, async (value) => {
results.value = await search(value);
});빠르게 타이핑하면 이전 검색어의 결과가 나중에 도착해 최신 결과를 덮었습니다. 화면에는 방금 지운 단어의 결과가 남았습니다.
원인 분석
경쟁 상태입니다. 요청이 시작된 순서와 응답이 도착하는 순서는 다를 수 있습니다. 늦게 도착한 오래된 응답이 마지막에 results를 덮어씁니다.
Vue는 이 문제를 위해 정리 콜백을 제공합니다. 문서 설명대로 watch와 watchEffect 모두 콜백에 onCleanup 인자를 주고, 등록한 정리 함수는 다음 번 이펙트가 재실행되기 직전에 호출되며 watcher가 정지될 때도 호출됩니다.
문서가 드는 대표 사례가 정확히 이 상황입니다 — id가 바뀌면 cancel이 호출되어, 아직 완료되지 않은 이전 요청을 취소합니다. 그 목적도 함께 적혀 있습니다 — 의존성이 바뀌기 전에 시작된 오래된 비동기 요청이 상태를 덮어쓰는 것을 막습니다.
Vue 3.5+에서는 같은 일을 하는 onWatcherCleanup을 import해서 쓸 수도 있습니다.
해결 방안
- 정리 콜백에서 이전 요청을 취소합니다.
watch(query, async (value, _old, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
results.value = await search(value, { signal: controller.signal });
});- 취소가 불가능하면 최신 여부를 확인합니다. 외부 SDK처럼 중단할 수 없는 API라면 플래그로 판별합니다.
watch(query, async (value, _old, onCleanup) => {
let stale = false;
onCleanup(() => { stale = true; });
const data = await search(value);
if (!stale) results.value = data;
});-
디바운스를 함께 겁니다. 취소와 디바운스는 목적이 다릅니다 — 디바운스는 요청 수를, 정리는 순서를 다룹니다.
-
watchEffect에서도 같습니다. 콜백의 첫 인자가onCleanup이고, 3.5+에서는onWatcherCleanup(cancel)을 직접 부를 수 있습니다. -
정리 함수는 watcher 정지 시에도 불립니다. 컴포넌트가 언마운트될 때 자동으로 정리되므로, 별도
onUnmounted를 만들 필요가 없습니다. -
이 문제를 테스트로 고정합니다. 응답 지연을 뒤집은 케이스(첫 요청이 두 번째보다 늦게 도착)를 넣으면, 나중에 정리 코드가 사라져도 바로 드러납니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.