본문으로 건너뛰기

스크롤 복원

Hash/페이지 상단 스크롤

TanStack Router는 별도 구성 없이 hash 스크롤페이지 상단 스크롤을 모두 지원합니다.

상단으로 스크롤하기 및 중첩된 스크롤 영역

기본적으로 상단으로 스크롤하기는 브라우저 동작을 따르므로, 탐색이 성공한 후 window 자체만 상단으로 스크롤됩니다. 하지만 고급 레이아웃을 사용하는 많은 앱에서는 주요 스크롤 영역이 중첩된 div 또는 유사한 요소인 경우가 일반적입니다. TanStack Router가 이러한 주요 스크롤 영역도 스크롤하게 하려면 routerOptions.scrollToTopSelectors를 사용해 해당 영역을 대상으로 하는 선택자를 추가할 수 있습니다.

const router = createRouter({
scrollToTopSelectors: ['#main-scrollable-area'],
})

document.querySelector(selector)로 간단히 확인할 수 없는 복잡한 선택자의 경우 routerOptions.scrollToTopSelectors에 HTML 요소를 반환하는 함수를 전달할 수 있습니다.

const selector = () =>
document
.querySelector('#shadowRootParent')
?.shadowRoot?.querySelector('#main-scrollable-area')

const router = createRouter({
scrollToTopSelectors: [selector],
})

이러한 선택자는 현재 비활성화할 수 없는 window와 함께 처리됩니다.

스크롤 복원

스크롤 복원은 사용자가 페이지로 다시 탐색할 때 페이지의 스크롤 위치를 복원하는 과정입니다. 일반적으로 표준 HTML 기반 웹사이트에는 기본 제공되는 기능이지만, SPA 애플리케이션에서는 다음과 같은 이유로 재현하기 어려울 수 있습니다.

  • SPA는 일반적으로 탐색에 history.pushState API를 사용하므로 브라우저가 스크롤 위치를 기본적으로 복원해야 한다는 것을 알지 못합니다.
  • SPA는 때때로 콘텐츠를 비동기적으로 렌더링하므로 브라우저가 렌더링이 끝날 때까지 페이지 높이를 알 수 없습니다.
  • SPA는 특정 레이아웃과 기능을 구현하기 위해 중첩된 스크롤 컨테이너를 사용할 수도 있습니다.

또한 애플리케이션에는 body뿐 아니라 앱 내부에 여러 스크롤 영역이 있는 경우가 많습니다. 예를 들어 채팅 애플리케이션에는 스크롤 가능한 사이드바와 스크롤 가능한 채팅 영역이 있을 수 있습니다. 이 경우 두 영역의 스크롤 위치를 서로 독립적으로 복원해야 합니다.

TanStack Router는 이 문제를 해결하기 위해 스크롤 위치의 모니터링, 캐싱, 복원을 처리하는 스크롤 복원 컴포넌트와 훅을 제공합니다.

다음과 같은 방식으로 처리합니다.

  • 스크롤 이벤트를 확인하도록 DOM 모니터링
  • 스크롤 복원 캐시에 스크롤 영역 등록
  • 스크롤 위치를 캐시하고 복원할 시점을 알기 위해 적절한 라우터 이벤트 수신
  • 각 스크롤 영역의 스크롤 위치를 캐시에 저장(windowbody 포함)
  • 탐색 성공 후 DOM이 그려지기 전에 스크롤 위치 복원

복잡해 보일 수 있지만 사용 방법은 다음처럼 간단합니다.

const router = createRouter({
scrollRestoration: true,
})

[!NOTE] <ScrollRestoration /> 컴포넌트는 여전히 작동하지만 더 이상 권장되지 않습니다.

사용자 지정 캐시 키

Remix 자체의 스크롤 복원 API와 마찬가지로 getKey 옵션을 사용해 특정 스크롤 영역의 스크롤 위치를 캐시하는 데 사용하는 키를 사용자 지정할 수도 있습니다. 예를 들어 사용자의 브라우저 기록과 관계없이 같은 스크롤 위치를 사용하도록 강제할 수 있습니다.

getKey 옵션은 TanStack Router에서 관련 Location 상태를 받고, 해당 상태의 스크롤 측정값을 고유하게 식별하는 문자열을 반환해야 합니다.

기본 getKey(location) => location.state.__TSR_key!이며, __TSR_key는 history의 각 항목에 대해 생성되는 고유 키입니다.

v1.121.34 이전의 이전 버전에서는 state.key를 기본 키로 사용했지만, 이제 state.__TSR_key 사용을 권장합니다. 현재는 호환성을 위해 location.state.key를 계속 사용할 수 있지만 다음 메이저 버전에서 제거됩니다.

예시

pathname에 스크롤을 동기화할 수 있습니다.

const router = createRouter({
getScrollRestorationKey: (location) => location.pathname,
})

일부 경로만 조건부로 동기화하고 나머지에는 키를 사용할 수 있습니다.

const router = createRouter({
getScrollRestorationKey: (location) => {
const paths = ['/', '/chat']
return paths.includes(location.pathname)
? location.pathname
: location.state.__TSR_key!
},
})

스크롤 복원 방지

스크롤 복원이 수행되지 않도록 해야 하는 경우가 있습니다. 다음 API에서 제공하는 resetScroll 옵션을 사용하면 됩니다.

  • <Link resetScroll={false}>
  • navigate({ resetScroll: false })
  • redirect({ resetScroll: false })

resetScrollfalse로 설정하면 다음 탐색에서 스크롤 위치가 복원되지 않으며(스택의 기존 history 항목으로 탐색하는 경우) 상단으로 초기화되지도 않습니다(스택에 새 history 항목을 추가하는 경우).

수동 스크롤 복원

대부분의 경우 스크롤 복원을 작동시키기 위해 특별히 할 일은 없습니다. 하지만 스크롤 복원을 수동으로 제어해야 하는 경우도 있으며, 가장 일반적인 예는 가상화된 목록입니다.

브라우저 전체 창에서 가상화된 목록의 스크롤 복원을 수동으로 제어하려면 다음과 같이 합니다.

function Component() {
const scrollEntry = useElementScrollRestoration({
getElement: () => window,
})

// Let's use TanStack Virtual to virtualize some content!
const virtualizer = useWindowVirtualizer({
count: 10000,
estimateSize: () => 100,
// We pass the scrollY from the scroll restoration entry to the virtualizer
// as the initial offset
initialOffset: scrollEntry?.scrollY,
})

return (
<div>
{virtualizer.getVirtualItems().map(item => (
...
))}
</div>
)
}

특정 요소의 스크롤 복원을 수동으로 제어하려면 useElementScrollRestoration 훅과 data-scroll-restoration-id DOM 속성을 사용할 수 있습니다.

React

function Component() {
// We need a unique ID for manual scroll restoration on a specific element
// It should be as unique as possible for this element across your app
const scrollRestorationId = 'myVirtualizedContent'

// We use that ID to get the scroll entry for this element
const scrollEntry = useElementScrollRestoration({
id: scrollRestorationId,
})

// Let's use TanStack Virtual to virtualize some content!
const virtualizerParentRef = React.useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: 10000,
getScrollElement: () => virtualizerParentRef.current,
estimateSize: () => 100,
// We pass the scrollY from the scroll restoration entry to the virtualizer
// as the initial offset
initialOffset: scrollEntry?.scrollY,
})

return (
<div
ref={virtualizerParentRef}
// We pass the scroll restoration ID to the element
// as a custom attribute that will get picked up by the
// scroll restoration watcher
data-scroll-restoration-id={scrollRestorationId}
className="flex-1 border rounded-lg overflow-auto relative"
>
...
</div>
)
}

Solid

function Component() {
// We need a unique ID for manual scroll restoration on a specific element
// It should be as unique as possible for this element across your app
const scrollRestorationId = 'myVirtualizedContent'

// We use that ID to get the scroll entry for this element
const scrollEntry = useElementScrollRestoration({
id: scrollRestorationId,
})

// Let's use TanStack Virtual to virtualize some content!
let virtualizerParentRef: any
const virtualizer = createVirtualizer({
count: 10000,
getScrollElement: () => virtualizerParentRef,
estimateSize: () => 100,
// We pass the scrollY from the scroll restoration entry to the virtualizer
// as the initial offset
initialOffset: scrollEntry?.scrollY,
})

return (
<div
ref={virtualizerParentRef}
// We pass the scroll restoration ID to the element
// as a custom attribute that will get picked up by the
// scroll restoration watcher
data-scroll-restoration-id={scrollRestorationId}
class="flex-1 border rounded-lg overflow-auto relative"
>
...
</div>
)
}

스크롤 동작

페이지 사이를 탐색할 때 스크롤 동작을 제어하려면 scrollRestorationBehavior 옵션을 사용할 수 있습니다. 이 옵션을 사용하면 페이지 전환을 부드러운 스크롤 대신 즉시 수행할 수 있습니다. 스크롤 복원 동작의 전역 구성에는 브라우저가 지원하는 smooth, instant, auto 옵션이 동일하게 있으며, 자세한 내용은 MDN을 참고하세요.

const router = createRouter({
scrollRestorationBehavior: 'instant',
})