본문으로 건너뛰기
개발 머꼬
개발 노트Vue
hohyeon.dev24

검색어를 빠르게 바꿨더니 이전 결과가 나중에 도착해 화면을 덮은 이유

  • #Engineering Note
  • #Vue

문제 발생

검색어가 바뀔 때마다 결과를 가져왔습니다.

watch(query, async (value) => {
  results.value = await search(value);
});

빠르게 타이핑하면 이전 검색어의 결과가 나중에 도착해 최신 결과를 덮었습니다. 화면에는 방금 지운 단어의 결과가 남았습니다.

원인 분석

경쟁 상태입니다. 요청이 시작된 순서와 응답이 도착하는 순서는 다를 수 있습니다. 늦게 도착한 오래된 응답이 마지막에 results를 덮어씁니다.

Vue는 이 문제를 위해 정리 콜백을 제공합니다. 문서 설명대로 watchwatchEffect 모두 콜백에 onCleanup 인자를 주고, 등록한 정리 함수는 다음 번 이펙트가 재실행되기 직전에 호출되며 watcher가 정지될 때도 호출됩니다.

문서가 드는 대표 사례가 정확히 이 상황입니다 — id가 바뀌면 cancel이 호출되어, 아직 완료되지 않은 이전 요청을 취소합니다. 그 목적도 함께 적혀 있습니다 — 의존성이 바뀌기 전에 시작된 오래된 비동기 요청이 상태를 덮어쓰는 것을 막습니다.

Vue 3.5+에서는 같은 일을 하는 onWatcherCleanup을 import해서 쓸 수도 있습니다.

해결 방안

  1. 정리 콜백에서 이전 요청을 취소합니다.
watch(query, async (value, _old, onCleanup) => {
  const controller = new AbortController();
  onCleanup(() => controller.abort());
  results.value = await search(value, { signal: controller.signal });
});
  1. 취소가 불가능하면 최신 여부를 확인합니다. 외부 SDK처럼 중단할 수 없는 API라면 플래그로 판별합니다.
watch(query, async (value, _old, onCleanup) => {
  let stale = false;
  onCleanup(() => { stale = true; });
  const data = await search(value);
  if (!stale) results.value = data;
});
  1. 디바운스를 함께 겁니다. 취소와 디바운스는 목적이 다릅니다 — 디바운스는 요청 수를, 정리는 순서를 다룹니다.

  2. watchEffect에서도 같습니다. 콜백의 첫 인자가 onCleanup이고, 3.5+에서는 onWatcherCleanup(cancel)을 직접 부를 수 있습니다.

  3. 정리 함수는 watcher 정지 시에도 불립니다. 컴포넌트가 언마운트될 때 자동으로 정리되므로, 별도 onUnmounted를 만들 필요가 없습니다.

  4. 이 문제를 테스트로 고정합니다. 응답 지연을 뒤집은 케이스(첫 요청이 두 번째보다 늦게 도착)를 넣으면, 나중에 정리 코드가 사라져도 바로 드러납니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.