Skip to content

Commit 9b7746c

Browse files
committed
fix: 페이지 전환 스크롤 정책 통일
1 parent e9d83db commit 9b7746c

3 files changed

Lines changed: 110 additions & 0 deletions

File tree

src/app/layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { Suspense } from "react";
2+
13
import type { Metadata } from "next";
24
import { Alata } from "next/font/google";
35
import localFont from "next/font/local";
46

57
import Navbar from "@/components/common/Navbar";
8+
import ScrollManager from "@/components/common/ScrollManager";
69

710
import Providers from "./providers";
811

@@ -36,6 +39,9 @@ export default function RootLayout({
3639
{/* <body className="flex min-h-full flex-col"> */}
3740
<body className="mx-auto min-h-screen w-full max-w-97.5 bg-white">
3841
<Providers>{children}</Providers>
42+
<Suspense fallback={null}>
43+
<ScrollManager />
44+
</Suspense>
3945
<Navbar />
4046
</body>
4147
</html>

src/components/common/Navbar.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export default function Navbar() {
5656
<Link
5757
key={menu.href}
5858
href={menu.href}
59+
onClick={() => {
60+
if (isActive) {
61+
window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
62+
}
63+
}}
5964
className="flex h-full flex-1 flex-col items-center justify-center gap-1"
6065
>
6166
<Icon active={isActive} />
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"use client";
2+
3+
import { useEffect, useMemo, useRef } from "react";
4+
5+
import { usePathname, useSearchParams } from "next/navigation";
6+
7+
interface ScrollPosition {
8+
x: number;
9+
y: number;
10+
}
11+
12+
const TOP_POSITION: ScrollPosition = { x: 0, y: 0 };
13+
14+
function getScrollPosition(): ScrollPosition {
15+
return { x: window.scrollX, y: window.scrollY };
16+
}
17+
18+
function scrollToPosition(position: ScrollPosition) {
19+
window.scrollTo({
20+
left: position.x,
21+
top: position.y,
22+
behavior: "auto",
23+
});
24+
}
25+
26+
export default function ScrollManager() {
27+
const pathname = usePathname();
28+
const searchParams = useSearchParams();
29+
const routeKey = useMemo(() => {
30+
const queryString = searchParams.toString();
31+
return queryString ? `${pathname}?${queryString}` : pathname;
32+
}, [pathname, searchParams]);
33+
const routeKeyRef = useRef(routeKey);
34+
const positionsRef = useRef<Record<string, ScrollPosition>>({});
35+
const isPopNavigationRef = useRef(false);
36+
const frameRef = useRef<number | null>(null);
37+
38+
useEffect(() => {
39+
if (!("scrollRestoration" in window.history)) return;
40+
41+
const previousScrollRestoration = window.history.scrollRestoration;
42+
window.history.scrollRestoration = "manual";
43+
44+
return () => {
45+
window.history.scrollRestoration = previousScrollRestoration;
46+
};
47+
}, []);
48+
49+
useEffect(() => {
50+
const handlePopState = () => {
51+
isPopNavigationRef.current = true;
52+
};
53+
54+
window.addEventListener("popstate", handlePopState);
55+
return () => window.removeEventListener("popstate", handlePopState);
56+
}, []);
57+
58+
useEffect(() => {
59+
const positions = positionsRef.current;
60+
61+
const handleScroll = () => {
62+
if (frameRef.current !== null) return;
63+
64+
frameRef.current = window.requestAnimationFrame(() => {
65+
positions[routeKeyRef.current] = getScrollPosition();
66+
frameRef.current = null;
67+
});
68+
};
69+
70+
window.addEventListener("scroll", handleScroll, { passive: true });
71+
return () => {
72+
window.removeEventListener("scroll", handleScroll);
73+
if (frameRef.current !== null) {
74+
window.cancelAnimationFrame(frameRef.current);
75+
}
76+
positions[routeKeyRef.current] = getScrollPosition();
77+
};
78+
}, []);
79+
80+
useEffect(() => {
81+
const previousRouteKey = routeKeyRef.current;
82+
if (previousRouteKey === routeKey) return;
83+
84+
positionsRef.current[previousRouteKey] ??= getScrollPosition();
85+
routeKeyRef.current = routeKey;
86+
87+
const shouldRestore = isPopNavigationRef.current;
88+
const nextPosition = shouldRestore
89+
? (positionsRef.current[routeKey] ?? TOP_POSITION)
90+
: TOP_POSITION;
91+
isPopNavigationRef.current = false;
92+
93+
window.requestAnimationFrame(() => {
94+
window.requestAnimationFrame(() => scrollToPosition(nextPosition));
95+
});
96+
}, [routeKey]);
97+
98+
return null;
99+
}

0 commit comments

Comments
 (0)