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>
</>
);
}
완성!

'✨FRONTEND > 📍React' 카테고리의 다른 글
| React Native와 Expo로 간편하게 앱 개발하기 (1) | 2025.02.04 |
|---|---|
| 페이지네이션 초간단 구현하기 (React) (2) | 2024.12.23 |
| Toast 공통 컴포넌트 만들기 (feat.react-toastify) (0) | 2024.08.08 |
| react-hook-form + yup = 회원가입/로그인(feat.쿠키) (0) | 2024.08.02 |
| input 공통 컴포넌트로 만들기 (feat. react-hook-form + yup) (0) | 2024.08.01 |