본문 바로가기
💻 개발/toy project

자연스러운 UX: 뒤로 가기에서 스크롤 위치 유지하기

by llddang 2025. 2. 4.

웹 애플리케이션을 사용하다보면 리스트 페이지에서 특정 아이템의 상세 페이지로 이동했다가 다시 돌아올 때, 스크롤 위치가 맨 위로 초기화되는 경험을 하곤한다.
이는 사용자 경험을 지해하는 요소 중 하나로, 오늘은 React-router-dom과 window.scrollTo 메서드를 활용해 이 문제를 해결한 방법에 대해 작성해보겠다!

 

상황 구성하기

먼저 세로 스크롤이 긴 / 페이지와 특정 아이템의 상세 내용을 볼 수 있는 /item가 있다고 하자.

// Home.tsx

import { Link } from "react-router-dom";

export default function Home() {
  const items = Array.from({ length: 30 }, (_, i) => i);

  return (
    <div>
      <h1>Home</h1>
      {items.map((itemId) => (
        <Link
          key={itemId}
          to="/item"
          style={{ display: "block", height: "40px", border: "1px soild red" }}
        >
          item으로 이동하기 {itemId}
        </Link>
      ))}
    </div>
  );
}
// Item.tsx

import { Link } from "react-router-dom";

export default function Item() {
  return (
    <div>
      <h1>Detailed description of the item...</h1>
      <Link to="/">home으로 돌아가기</Link>
    </div>
  );
}

 

 

Home 컴포넌트에서는 스크롤을 만들기 위해 30개의 /item으로 이동할 수 있는 링크를 만들어주었다.

이후 Item 컴포넌트에서는 /으로 이동할 수 있는 링크를 만들었다.

뒤로 가기 시, 최상위 scroll 로 이동하는 gif

 

 

이 상태에서 Item으로 이동 후, 링크를 통해 / 으로 이동하면 이전 스크롤 위치가 아닌 최상위 스크롤로 이동하는 것을 볼 수 있다!

 

위에서도 말했지만 이는 상당한 불편함을 초래하는 UX 문제이다.

해결해보자!

 

뒤로 이동 시, 스크롤 유지하기

 

먼저, react-router-dom의 useNavigation을 활용할 것이기 때문에 아래 명령어를 이용하여 react-router-dom 패키지를 설치해주길 바란다.

# if use npm
$ npm install react-router-dom
# if use yarn
$ yarn add react-router-dom

 

 

해결하는 방법에 대해 잠시 설명하겠다.

 

먼저 item 링크 요소를 클릭하였을 때, 현재 화면의 window.scorllY값을 React Router의 state로 /item 페이지로 넘겨준다.

 

Item 페이지 컴포넌트에서는 useEffect의 cleanup 함수로 window.scrollTo 메서드를 활용한 스크롤 위치 이동함수를 정의한다.

이후 Item 페이지 컴포넌트가 언마운트될 떄(뒤로 가기 시), cleanup 함수가 실행되며 해당 위치로 이동하게 된다.

 

그럼 이제 구현한 코드를 살펴보자!

import { Link, useNavigate } from "react-router-dom";
// Home.tsx

export default function Home() {
  const items = Array.from({ length: 30 }, (_, i) => i);

  // navigate 선언
  const navigate = useNavigate();

  function handleLinkClick(e: React.MouseEvent<HTMLAnchorElement>) {
    e.preventDefault(); // 기본 Link의 이벤트 막기

    const scrollPosition = window.scrollY; // 현재 위치 기록
    navigate(`/item`, {
      state: { scrollPosition }, // React Router의 State로 이전 scroll 위치 전달
    });
  }

  return (
    <div>
      <h1>Home</h1>
      {items.map((itemId) => (
        <Link
          key={itemId}
          to="/item"
          style={{ display: "block", height: "40px", border: "1px soild red" }}
          onClick={handleLinkClick}
        >
          item으로 이동하기 {itemId}
        </Link>
      ))}
    </div>
  );
}

 

// Item.tsx

import { useEffect } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";

export default function Item() {
  // 이전 scroll 위치가 전달된 state 받기
  const { state } = useLocation();
  const navigate = useNavigate();

  const handleBack = (e: React.MouseEvent<HTMLAnchorElement>) => {
    e.preventDefault(); // Link의 기본 이벤트 막기
    navigate(-1); // 이전 페이지로 이동
  };

  useEffect(() => {
    // cleanup 함수 정의 - 컴포넌트가 DOM에서 사라지면 렌더링 이후 값으로 실행됨
    return () => {
      if (state?.scrollPosition) {
        window.scrollTo(0, state.scrollPosition); // 스크롤 위치 이동
      }
    };
  }, [state]);

  return (
    <div>
      <h1>Detailed description of the item...</h1>
      <Link to="/" onClick={handleBack}>
        home으로 돌아가기
      </Link>
    </div>
  );
}

 

 

 

장점과 주의사항

장점

  • 사용자가 이전에 보던 위치를 기억하고 있어 자연스러운 UX 제공
  • React Router의 기본 기능만으로 구현 가능
  • 추가 라이브러리 없이 간단히 구현 가능

주의사항

  • 브라우저의 history를 벗어나는 경우(예: 직접 URL 입력, 다른 사이트에서 진입)에는 이전 스크롤 위치 정보가 없음
  • 모바일 기기에서의 스크롤 위치 계산 시 viewport 크기 고려 필요
  • 뒤로 가기가 아닌 다른 경로로 이동 후 돌아오는 경우는 포함되지 않음

 

 

 

후기

이번에는 복잡한 요소가 없어서 간단하게 구현할 수 있었다.

하지만 만약에 Item의 요소에서 리렌더링을 유발하는 상태가 있고, Item의 길이가 길다면 의도치 않은 스크롤 이동이 발생할 수도 있다.

 

따라서 여러가지 상황에서 이 방법을 쓸 수 있을 것이라고는 생각되지 않는다.

 

다음에는 무한 스크롤에서 스크롤의 위치를 유지하는 방법을 작성해보면서 코드를 개선해보겠다!

 

해당 내용은 포켓몬 도감을 구현하는 프로젝트에서 사용되었다.