개발하고 있는 프로덕트에서는 페이지 간 이동을 위해 Next.js의 <Link>태그를 사용하고 있었습니다. 이는 클라이언트 사이드 라우팅을 통해 전체 페이지를 새로고침하지 않고도 빠르고 부드러운 화면 전환이 가능하다는 장점이 있기 때문이다. 하지만 개발 도중 한가지 예상치 못한 문제를 발견했습니다. 바로 페이지를 이동할 때마다 이전 페이지의 스크롤 위치가 기억되어, 새로운 페이지가 렌더링된 후에도 스크롤이 맨위가 아닌 중간위치에 머무는 현상이 발생한 것이였습니다. 사용자의 입장에서는 갑자기 페이지가 어중간한 위치에서 열리는 듯한 인상을 받을 수 있어서, 이 문제를 무시하기는 어려웠습니다.
문제 상황 분석
Next.js의 태그는 전체 페이지를 새로고침하지 않고, React 클라이언트 사이드 라우팅을 통해 필요한 컴포넌트만 업데이트합니다. 내부적으로는 history.pushState()를 사용해 URL만 변경하고, 브라우저는 전체 페이지가 새로 고쳐진 것이 아니라고 판단합니다. 따라서 스크롤 위치도 유지가 됩니다. 이는 빠르고 부드러운 UX를 제공하기 위한 설계지만, 사용자가 페이지 전환 시 콘텐츠를 맨 위에서 보기를 기대하는 경우에는 문제가 될 수 있습니다.
왜 이렇게 동작하는걸까
이러한 동작은 의도된 된 것입니다. 무한 스크롤, 탭 전환, 리스트-상세 페이지 흐름에서는 이전 위치를 유지하는 것이 자연스러운 UX이기 때문입니다. 하지만 콘텐츠를 처음부터 읽어야 하는 페이지라면, 이전 스크롤 위치가 유지되면 오히려 혼란스러운 경험을 줄 수 있습니다. 따러서 상황에 맞는 스크롤 동작 제어가 필요합니다.
기존의 DOM 구조는 대부분 유지되고, React는 새로운 페이지 컴포넌트만 Virtual DOM을 통해 업데이트합니다. 이는 사용자가 빠르고 부드럽게 페이지를 전환할 수 있게 해주는 장점이 있습니다.
router.push로 해결할 수 있을까?
처음에는 next/navigation의 router.push()를 사용해 라우팅을 수동 제어를 해보았습니다. 하지만 이 방식에는 여러 한계가 있었습니다. <Link>는 prefetch 기능, 접근성(A11y), 시멘틱 구조, Next.js 최적화 기능과 자연스럽게 통합되는 반면에, router.push()는 단순 경로 변경만 수행하고 이런 기능들과 분리되어 있습니다. 특히 성능 저하와 스크린 리더 지원 측면에서 단점이 뚜렷했습니다. 따라서 일반적인 페이지 전환에는 여전히 <Link> 사용이 권장되고 있는 것 같습니다.
그럼에도 불구하고 스크롤이 유지된다면?
<Link scroll={true}>를 사용했음에도 스크롤이 초기화되지 않는 경우가 있습니다. (디폴트로 Link는 scroll={true}로 설정되어있음.) 대표적인 원인은 다음과 같습니다.
- 레이아웃 유지 (layout.tsx)
- App Router는 layout.tsx를 유지한 채 페이지 일부만 교체합니다. 이 경우 전체 DOM이 새로 생성되지 않기 때문에 스크롤 초기화 타이밍이 사라집니다. 저의 경우에도 이 레이아웃 유지가 문제의 원인이였습니다. 상위 레이아웃이 유지되면서 스크롤 상태가 그대로 전달되어 문제가 발생했습니다.
- 고정 헤더
- position : fixed나 sticky 요소가 상단에 있을 경우, 브라우저가 "이미 상단이 보인다"고 판단할 수 있습니다.
- 커스텀 스크롤 컨테이너
- overflow:auto가 걸린 div를 사용하는 경우, Next.js는 그 안의 스크롤 위치는 제어하지 못합니다.
- 스타일 시트 간섭
- html, body에 scroll-behavior: smooth나 overflow:hidden이 설정되어 있는 경우 스크롤 이동이 비정상적으로 작동할 수 있습니다.
해결 방안(시도한 방법)
1. 기본 동작 활용
Next.js의 컴포넌트는 기본적으로 scroll={ture} 설정이 적용되어 있어, 페이지 이동 시 자동으로 스크롤을 최상단으로 이동시키는 기능이 내장되어 있습니다. 이 설정만으로도 간단한 페이지 전환이라면 충분히 기대하는 스크롤 초기화가 이루어질 수 있습니다.
하지만 저의 문제 상황에서는 레이아웃이 유지되는 구조의 경우라서 스크롤 초기화가 무시되었습니다.
2. 특정페이지에서 적용 가능한 스크롤 초기화 훅 사용
Next.js의 useEffect() 훅을 활용하여 스크롤 초기화가 필요한 페이지에서만 스크롤 위치를 (0,0)으로 이동시키는 방식을 사용했습니다. 전역으로 Scroll 컴포넌트를 두기보다는, 상세 페이지나 특정 섹션에만 적용이 필요하므로 유용한 방법이였습니다.
✅ 장점
- 불필요한 전역 스크롤 이동 방지를 통해 UX 간섭 해소
- 실제로 스크롤 초기화가 필요한 곳에서만 직접적으로 제어 가능
- 간단한 커스텀 훅으로 재사용성 확보 가능
❌ 단점
- 페이지마다 해당 훅을 일일이 호출해야 함.
- 복잡한 상태 기반 네비게이션에서는 스크롤 타이밍 제어가 까다로움.
'use client';
import { useEffect } from 'react';
export function useScrollTopOnMount() {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
}
export default function TeamViewDetail() {
useScrollTopOnMount();
return (...);
}
3. experimental scrollRestoration 사용
Next.js의 실험적 기능인 scrollRestoration을 활성화하면, 브라우저 세션 히스토리와 함께 스크롤 위치를 자동 저장하고 복원할 수 있습니다. 주로 '뒤로 가기' 시 유용하게 작동하며, 앞으로 이동 시에는 상단으로 초기화됩니다.
// next.config.js
experimental: {
scrollRestoration: true,
}
✅ 장점
- 브라우저의 기대 동작과 일치
- 별도의 로직 없이 Next.js 설정만으로 적용 가능
- UX 향상
❌ 단점
- 항상 스크롤을 상단으로 이동시키고 싶은 경우에는 불필요
- App Router의 레이아웃 유지 구조와 완벽히 맞물리지 않아 예외 케이스 발생 가능
4. 서드파티 라이브러리 사용
스크롤 위치 초기화 및 복원을 위해 커뮤니티에서 개발된 라이브러리를 사용하는 방법도 있습니다. 대표적으로 next-scroll-restorer, next-scroll-restoration 등이 있으며, 라우팅 이벤트에 따라 스크롤을 제어하고 복잡한 레이아웃도 대응 가능합니다.
✅ 장점
- 여러 스크롤 컨테이너, 고정 헤더, 레이아웃 등 복잡한 상황을 자동 처리함.
- scroll 상태 저장/복원이 자동으로 됨.
- 별도의 구현 없이 컴포넌트 삽입만으로 적용 가능
❌ 단점
- 외부 라이브러리에 의존해야 함.
- Next.js 업데이트 시 호환성 문제가 발생할 수 있음.
선택한 해결 방안
여러가지 방법들이 존재했지만, 구미가 당기는 방안을 찾지는 못했다. App Router + layout 유지 구조에서는 페이지 전환시에도 레이아웃이 유지되기 때문에 스크롤 초기화가 자동으로 발생하지 않는다. 이 경우 상세 페이지 컴포넌트 내부에서 명시적으로 window.scrollTo(0,0)을 호출하는 것이 가장 실용적인 확실한 해결 방법(2. 특정페이지에서 적용 가능한 스크롤 초기화 훅 사용)이라고 생각했다.
결론
Next.js App Router 환경에서는 자동 스크롤 초기화가 항상 보장되지 않으며, 특히 layout.tsx를 유지하는 구조에서는 의도하지 않는 스크롤 유지가 발생할 수 있습니다. 다양한 방법들을 검토하고 검색해보았지만, 현재로서는 클라이언트 컴포넌트 내에서 직접 스크롤을 초기화하는 방법 외에는 확실한 대안을 찾지 못했습니다.
이번 문제를 경험하면서 느낀 점은, 단순히 겉으로 드러난 증상을 해결하는 것이 아니라 Next.js의 내부 구조와 동작 원리에 대한 깊은 이해가 있어야 보다 효율적인 해결책을 설계할 수 있다는 것 이였습니다. 앞으로도 에상치 못한 문제를 만났을 때, 그 기반이 되는 프레임워크 자체를 더 잘 이해하는 노력이 필요한 것 같습니다.
'프론트엔드 기록 > 리액트' 카테고리의 다른 글
React Query와 Intersection Observer를 활용한 무한스크롤 구현 (0) | 2025.03.05 |
---|---|
빈 화면 없이 자연스럽게 데이터 갱신하여 SSR UX 개선하기 (0) | 2025.03.03 |
리액트 Flux 패턴이란?( vs MVC 모델 ) (2) | 2024.10.29 |
[Next.js 14] FSD 기능 관점 폴더 아키텍처에 대한 생각 (2) | 2024.10.04 |
리액트의 동작 원리, Virtual DOM (1) | 2024.09.12 |