본문 바로가기
✨FRONTEND/📍React

path를 따라가는 로딩화면 구현하기

by 짱돌보리 2024. 8. 16.
728x90

✈️ path를 따라가는 로딩화면 구현하기

Next.js에서 페이지 전환 시 로딩 화면을 띄워주고 싶을 때가 있다.
특히 초기 로딩이 느릴 때 사용자에게 "로딩 중입니다"라는 신호를 주는 건 UX적으로 꽤 중요하니까!

 

근데 문제는... 모든 페이지 이동마다 로딩 화면이 보이면 오히려 거슬린다.
CSR 기반 라우팅이라 한 번 방문한 페이지는 정말 빨리 열리는데, 그 짧은 순간에도 로딩이 보이면 되려 방해됨 😅

 

그래서 처음 방문한 페이지에서만 로딩이 나오고, 이후에는 로딩 없이 자연스럽게 이동되도록 구현해봤다.

비행기 애니메이션 SVG 컴포넌트

import React from 'react';

type LoadingProps = {
  width?: number;
};

export default function Loading({ width = 1200 }: LoadingProps) {
  return (
    <div className="flex items-center justify-center">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width={width}
        viewBox="0 0 400 200"
      >
        <defs>
          <path
            id="flightPath"
            d="M 0 100 C 100 50, 300 150, 400 100"
            fill="none"
            stroke="lightgrey"
            strokeWidth="2"
          />
        </defs>

        {/* 비행 경로를 따라 선을 그리기 위한 경로 */}
        <path
          id="line"
          d="M 0 100 C 100 50, 300 150, 400 100"
          fill="none"
          stroke="lightblue"
          strokeWidth="2"
        >
          <animate
            attributeName="stroke-dasharray"
            from="0,400"
            to="440,0"
            dur="3s"
            repeatCount="indefinite"
          />
        </path>

        {/* 비행기 모양 */}
        <path
          id="plane"
          d="M 27,3 H 21 L 13,15 H 9 L 12,3 H 5 L 3,7 H -1 L 1,0 -1,-7 H 3 L 5,-3 H 12 L 9,-15 H 13 L 21,-3 H 27 C 33,-3 33,3 27,3 Z"
          fill="white"
          stroke="black"
          strokeWidth="1.5"
        >
          <animateMotion
            rotate="auto"
            begin="0s"
            dur="3s"
            repeatCount="indefinite"
          >
            <mpath xlinkHref="#flightPath" />
          </animateMotion>
        </path>
      </svg>
    </div>
  );
}

useLoading 커스텀 훅

Next.js의 Router.events를 활용해서 페이지 전환 감지하고,
한 번이라도 방문한 페이지는 기록해서 이후에는 로딩 안 뜨도록 처리한다.

  • routeChangeStart(url, { shallow }) - 라우트가 변경되기 시작할때 트리거됨.
  • routeChangeComplete(url, { shallow }) - 라우트가 완전히 변경되었을 때 트리거됨.
  • routeChangeError(err, url, { shallow }) - 라우트 변경 중에 에러가 발생했거나, 취소되었을 때 트리거됨.
import { Router } from 'next/router';
import { useEffect, useState } from 'react';

export const useLoading = () => {
  const [showLoading, setShowLoading] = useState(true); // 기본 true로 시작
  const [hasLoadedPages, setHasLoadedPages] = useState(new Set<string>());

  useEffect(() => {
    const start = (url: string) => {
      if (hasLoadedPages.has(url)) return; // 이미 로딩했던 페이지면 무시
      setShowLoading(true);
    };

    const end = (url: string) => {
      setShowLoading(false);
      setHasLoadedPages((prev) => new Set(prev).add(url)); // 방문 기록 추가
    };

    Router.events.on('routeChangeStart', start);
    Router.events.on('routeChangeComplete', end);
    Router.events.on('routeChangeError', end);

    return () => {
      Router.events.off('routeChangeStart', start);
      Router.events.off('routeChangeComplete', end);
      Router.events.off('routeChangeError', end);
    };
  }, [hasLoadedPages]);

  return showLoading;
};

Layout 컴포넌트에 적용하기

export default function Layout({
  children,
  showHeader = true,
  showFooter = true,
}: LayoutProps) {
  const pathname = usePathname();
  const router = useRouter();

  const loading = useLoading();

  return (
    <>
      {showHeader && <Header />}
      <AnimatePresence mode="wait">
        <motion.div
          key={router.pathname}
          initial={isMyPage ? undefined : { opacity: 0, y: -50 }}
          animate={isMyPage ? undefined : { opacity: 1, y: 0 }}
          exit={isMyPage ? undefined : { opacity: 0, y: -50 }}
          transition={{ duration: 0.3 }}
        >
          {loading ? <Loading /> : children}
          {!loading && showFooter && <Footer />}
        </motion.div>
      </AnimatePresence>
    </>
  );
}

 

완성!