Next.js 뒤로가기 로직 구현하기

seoyeonpp·2025년 7월 16일

Frontend

목록 보기
11/13
post-thumbnail

어느날 뒤로가기에 대한 이슈들이 나왔다..

이슈는 바로..

🚨 1. 상품리스트 무한스크롤 페이지에서 스크롤을 내린 후 상품상세 갔다가 뒤로가기 하면 스크롤위치가 유지가 되지않고 이상한 곳으로 간다.

🚨 2. 인증이 필요한 페이지에서 로그인으로 보낸 후, 카카오 로그인 완료 하면 해당 페이지로 router.replace 시키는데, 그 페이지에서 뒤로가기하면 로그인페이지가 다시 나오는? 이상한 버그

🚨 3. 탭 ui에서 A,B 탭이 있으면 B탭 클릭해서 상세페이지 들어간 후 뒤로가면 A탭이 보이는 상태 저장이 안된다


각각 서로 다른 이슈여서 어떻게 처리하지 고민을 해봤다.

먼저 1번 이슈!!!!!

상품리스트 무한스크롤 페이지에서 스크롤을 내린 후 상품상세 갔다가 뒤로가기 하면 스크롤위치가 유지가 되지않고 이상한 곳으로 간다.

Next.js의 scrollRestoration 이라는 기능이 있다길래 한번 시도를 해봤다.
next.config.js에 이렇게 써주면 된다는데, experimental???? 실험적 기능이라고 해서 뭔가 찝찝했다.
그리고 결론적으로 스크롤위치 저장이 안된다. 😡

module.exports = {
  experimental: {
    scrollRestoration: true
  }
}

그래서 여러 무한리스트 구현기를 보면서 도대체 다들 어떻게 한건지를 찾아봤다..
그래서 힌트를 얻은게 커스텀훅이다!

페이지를 저장해야하는 무한스크롤 페이지에서는 usePageSaveScroll() 한방이면 session-storage에 해당 페이지의 pathname 으로 스크롤 위치를 0.3초마다 저장시키고 다시 그 페이지 돌아올때 session-storage에 있다면 해당 위치로 scrollTo 시키는 로직으로 구현했다.

더 좋은 방법이 있을 수도 있는데 이거밖에 생각이 안났다..

근데 또 문제 발생!!!!🚨

상품리스트가 느리게 로드될때 스켈레톤을 보여주게 되어있는데, 이 스켈레톤은 그냥 고정값으로 랜더링을 시키고 있었다.

그러니까 실제로 페이지로 돌아와도 스켈레톤이 보이면서 스크롤 길이가 줄어들게 되고, scrollTo가 먹히지 않는 현상이 발생했다.

좋은방법이 없을까 고민하다가 그냥 스켈레톤 개수를 스크롤 한 위치만큼 랜더링 시키기로 했다!

그래서 아까 작성한 커스텀 훅에 isSkeleton 인자를 추가하고, true라면 session-storage에 srollHeight가 저장되도록 했다.

그래서 탄생한 최종 훅!

export const usePageSaveScroll = (isSkeleton?: boolean) => {
  const pathname = usePathname()
  const isRestore = useRef(false)

  // scroll 저장
  useEffect(() => {
    const restoreScrollPosition = async () => {
      try {
        await new Promise<void>((resolve) => {
          setTimeout(() => {
            window.scrollTo(0, Number(sessionStorage.getItem(`scroll-${pathname}`) || 0))
            resolve()
          }, 100)
        })

        isRestore.current = false
      } catch (error) {
        console.error('Error restoring scroll position:', error)
      }
    }

    if (sessionStorage.getItem(`scroll-${pathname}`)) {
      isRestore.current = true
      restoreScrollPosition()
    }

    const saveScrollPosition = throttle(() => {
      if (isRestore.current) return
      sessionStorage.setItem(`scroll-${pathname}`, String(window.scrollY))
      if (isSkeleton) {
        // loading 시 skeleton 길이를 맞추기위해서 height를 저장함
        sessionStorage.setItem(`scrollHeight-${pathname}`, String(document.body.scrollHeight))
      }
    }, 300)

    window.addEventListener('scroll', saveScrollPosition)

    return () => {
      window.removeEventListener('scroll', saveScrollPosition)
    }
  }, [])
}

이제 이걸 호출한 페이지에서 스켈레톤을 해당 scrollHeight만큼 계산해서 보여주기만 하면 된다..
default를 20개로 보여주고, scrollHeight가 있으면 스켈레톤 컴포넌트 크기만큼 계산하고,

  // 무한스크롤 후 다시 페이지로 넘어올때, 스크롤 위치로 이동시키기 위해 skeleton 동적으로 개수 세팅
  const expectedHeight = Number(sessionStorage.getItem(`scrollHeight-${pathname}`))
  // skeleton의 높이 254px + y gap 24px => 278px
  const skeletonCount = expectedHeight > 0 ? Math.ceil(expectedHeight / 278) * 2 : 20

jsx 안에서는 이런식으로 보여줬다.

  {Children.toArray(Array.from(Array(skeletonCount).keys()).map(() => <ProductSkeleton />))}

그러니까 useSaveScroll 훅이 정상적으로 동작하게 되었다!
그래서 일단 1번 이슈 해결 ♨️👏


2번 이슈!!!!!

인증이 필요한 페이지에서 로그인으로 보낸 후, 카카오 로그인 완료 하면 해당 페이지로 router.replace 시키는데, 그 페이지에서 뒤로가기하면 로그인페이지가 다시 나오는? 이상한 버그

아 이걸 해결하려니까 수정할곳이 너무 많은 문제가 있었다.
뭐냐면 인증이 필요한 페이지에서 login으로 리다이렉트 될때, 최근경로를 쿠키에 저장하는데 그 저장하는게 로그인으로 router.push 처리하는 모~ 든 페이지에 작성되어있었다. 으악

그래서 일단 /login으로 보내기 전 경로를 쿠키에 저장하는 것을 layout에서 하면 한방에 처리하기 좋을 것 같았다.

그러면 이전 경로와 현재 경로를 알아야하는데, Next14버전에는 그런걸 탐지하는게 없다고 한다. next/navigation에는 없음..

그래서 RouterWatcher라는 컴포넌트를 만들었다.

export default function RouterWatcher() {
  const pathname = usePathname()
  const searchParams = useSearchParams()
  const prevPathRef = useRef<string | null>(null)
  const prevQueryRef = useRef<string | null>(null)

  useEffect(() => {
    if (pathname === '/login' && prevPathRef.current && prevPathRef.current !== pathname) {
      const isExcludedPath = pathname.includes('join') || pathname.includes('login')

      if (!isExcludedPath) {
        const recentRoute = prevQueryRef.current
          ? `${prevPathRef.current}?${prevQueryRef.current}`
          : prevPathRef.current
        setCookie('recent-route', recentRoute)
      }
    }


    prevPathRef.current = pathname
    prevQueryRef.current = searchParams?.toString() || null
  }, [pathname, searchParams])

  return null
}

만든 컴포넌트를 layout에 import하면 끝!

그리고, 로그인 페이지에서 로그인이 완료되면router.replace(recentRoute) 해주는 부분에, 뒤로가기하면 다시 로그인 페이지로 올 경우를 대비해서 accessToken이 url에 없으면 router back 해주는 로직만 추가했더니 잘 됐다!


마지막 3번 이슈!!!!!

탭 ui에서 A,B 탭이 있으면 B탭 클릭해서 상세페이지 들어간 후 뒤로가면 A탭이 보이는 상태 저장이 안된다

이건 login으로 보내기 전에 RouterWatcher에서 최근경로에 router query를 저장하기때문에, 해당 탭의 페이지 코드에서 router query에 tab정보가 있을 때, 해당 탭을 활성화 하도록 해서 해결했다.

// 예시
      if (searchParams.get('tap')) {
        const tap = searchParams.get('tap')
        onTabSelected(tap)
      }

글로 적으니까 별로 없어보이긴하는데, 이거 하면서 여러 이슈가 많았다.
middleware를 통한 redirect와 Link를 통한 이동이 router query 유무가 다른 이슈도 있었다. 근데 어쩌다 보니 해결 ..

이번 이슈를 통해서 뒤로가기 했을때의 UX도 개발할때 중요하게 봐야겠다는 경험이 생겼다!

2개의 댓글

comment-user-thumbnail
2025년 7월 16일

답글 달기
comment-user-thumbnail
2025년 7월 16일

트러블슈팅 관리하시는 개발 습관 멋져요👏👏

답글 달기