Notice
Recent Posts
Recent Comments
Link
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

likeornament 님의 블로그

콘텐츠 표시 완료 시간 58% 감소, 로딩 스피너 100% 제거 — prefetch가 바꾼 UX 본문

공부

콘텐츠 표시 완료 시간 58% 감소, 로딩 스피너 100% 제거 — prefetch가 바꾼 UX

likeornament 2025. 9. 5. 18:07

예전에 prefetch 없이 클라이언트에서 useQuery만으로 데이터를 불러왔을 때도 충분히 빠르다고 아래의 글을 쓴 적이 있다.
https://likeornament.tistory.com/17

 

하지만 실제 배포 환경에서 확인해보니, 페이지 전환 시 민감한 사용자라면 거슬릴 정도의 로딩 지연이 존재했다.


Prefetch 적용 전

좌측: word/page.tsx, 우측: word/learn/page.tsx

 

좌측의 첫 번째 페이지(word/page.tsx)에서 버튼을 클릭 후

우측의 두 번째 페이지(word/learn/page.tsx)로 이동할 때, useQuery로 데이터를 불러오는 동안 로딩 스피너가 나타난다.

 

즉, 페이지 전환은 되었지만 실제 콘텐츠는 늦게 표시된다.
→ 예민한 사용자라면 충분히 거슬릴 수 있는 지점이다.

 

이를 개선하기 위해 prefetch를 적용하기로 했다.

 

 

위 그림을 보면, 버튼 클릭 후 페이지 전환 → 페이지 마운트 후 useQuery fetch → 로딩 스피너 표시 → 데이터 로드 완료 후 콘텐츠 렌더링 순서다.
결국 스피너가 화면에 노출되는 시간이 문제였다.


Prefetch 적용 시점 고민

prefetch는 “사용자가 실제로 이동할 가능성이 높은 시점”에 해야 한다.
너무 일찍 불러오면 불필요한 메모리 낭비가 되기 때문이다.

 

그래서 나는 버튼에 마우스를 올린 순간을 prefetch 시점으로 정했다.

  • 첫 페이지에는 설명 텍스트와 상단 뒤로가기 버튼, 하단 이동 버튼뿐이다.
  • 학습을 원하지 않는 사용자는 하단 버튼에 hover조차 하지 않고 뒤로 가기를 누를 것이다.
  • 따라서 hover 순간이 다음 페이지로 이동할 확률이 가장 높은 시점이라고 판단했다.
const prefetch = async () => {
  if (!studyId) return;
  await Promise.all([
    queryClient.prefetchQuery({ queryKey: ["word", "learn", studyId], queryFn: getWords }),
    queryClient.prefetchQuery({ queryKey: ["word", "validation", studyId], queryFn: getValidationWordResult }),
  ]);
};

const handleClick = async () => {
  if (studyId && wordTotal) {
    // 모바일에서는 hover가 없으니 클릭 직전 prefetch
    await prefetch();
    router.replace(`/word/learn?studyId=${studyId}`);
  } else {
    router.replace("/home");
  }
};

return (
  <button
    onMouseEnter={prefetch} // 데스크톱 hover prefetch
    onClick={handleClick}   // 모바일 fallback
  >
    학습 시작하기
  </button>
);

 

모바일에서는 hover 이벤트가 없기 때문에 클릭 직후에라도 prefetch를 하기 위해
데스크탑에서는 onMouseEnter로, 모바일에서는 onClick 직후에 prefetch가 실행되도록 분리했다.


Prefetch 적용 효과

 

 

이 방식대로 적용하면, 버튼 hover → prefetch 실행 → 버튼 클릭 → 캐시 데이터 즉시 렌더 순서가 된다.
덕분에 로딩 스피너가 뜨기도 전에 콘텐츠가 그려져 훨씬 즉각적인 페이지 전환 경험을 제공할 수 있다.

 

하지만 단순히 “빠르다”로 끝내기보다 실제 수치로 검증해보기로 했다.


성능 측정 환경

https://likeornament.tistory.com/26

현재 프로젝트는 7월 회고록에서 언급했다시피 백엔드의 사정으로 인해

MSW(Mock Service Worker) 기반이므로 기본 응답 속도가 너무 빨라 비교가 어렵다.
따라서 현실적인 지연을 시뮬레이션했다.

 

1. MSW 핸들러에 delay 추가

import { http, HttpResponse, delay } from 'msw';

const jitter = (p50: number, p95: number) => {
  const p = Math.random();
  return p < 0.9
    ? p50 + Math.random() * (p95 - p50) * 0.3 // 보통 케이스
    : p95 + Math.random() * (p95 * 0.3);      // 느린 꼬리
};

export const handlers = [
  http.get('/api/words', async () => {
    await delay(jitter(250, 800)); // ms
    return HttpResponse.json({ /* ... */ });
  }),
  http.get('/api/validation', async () => {
    await delay(jitter(300, 900));
    return HttpResponse.json({ /* ... */ });
  }),
];

 

2. 브라우저 DevTools 네트워크 속도 Slow 4G 설정

 

3. 마킹 지점 기록

// A. 버튼 클릭 시점
performance.mark('nav-click');

// B. /word/learn 진입해서 "스피너가 보이기 시작" 순간
useEffect(() => {
  if (isLoading) performance.mark('spinner-start');
}, [isLoading]);

// C. 데이터가 모두 준비되어 "스피너가 사라진" 순간(렌더 직전/직후)
useEffect(() => {
  if (words && (isReview || validationResult)) {
    performance.mark('content-ready');
    const n = performance.measure('TTCR', 'nav-click', 'content-ready').duration;
    const s = performance.getEntriesByName('spinner-start')[0]
      ? performance.measure('SpinnerVisible', 'spinner-start', 'content-ready').duration
      : 0;
    console.log('TTCR(ms):', Math.round(n), 'SpinnerVisible(ms):', Math.round(s));
  }
}, [words, validationResult, isReview]);

 


측정 결과

각각 5회씩 측정 후 평균을 비교했다. (매번 캐시 초기화 후 진행)

 

(prefetch 적용 전)

  • TTCR: 549, 426, 450, 433, 397ms
  • SpinnerVisible: 485, 403, 415, 403, 368ms

(prefetch 적용 후)

  • TTCR: 195, 223, 156, 214, 166ms
  • SpinnerVisible: 모두 0ms

위의 내용의 값을 평균을 내서 그래프로 확인해 보자.

 


결과 해석

  • TTCR(노랑 막대, 콘텐츠 표시 완료 시간)
    • prefetch 전: 약 451ms
    • prefetch 후: 약 191ms
      약 58% 개선
  • SpinnerVisible(주황 막대, 로딩 스피너 표시 시간)
    • prefetch 전: 약 415ms
    • prefetch 후: 0ms
      스피너 완전 제거 (100% 개선)

즉, prefetch 적용으로 페이지 전환 시 콘텐츠 표시 속도가 절반 이상 빨라졌고,
스피너는 아예 보이지 않게 되어 UX가 크게 개선되었다.

 

마지막으로, 이 수치는 실서버와 절대적으로 동일하진 않다.
다만 중요한 점은 “이 서비스에서 prefetch 전략이 체감 UX를 확실히 개선한다”는 점을 실험으로 증명했다는 것이다.