React Router와 react-scroll로 부드러운 스크롤링 네비게이션 구현하기

ZENMA·2024년 12월 10일

React

목록 보기
1/3
post-thumbnail

웹 개발하다 보면 특정 섹션으로 부드럽게 스크롤링하는 기능이 필요할 때가 있음. 특히 React로 SPA(Single Page Application) 만들다 보면 이런 기능 하나로 사용자 경험(UX)이 확 좋아질 수 있음. 최근 프로젝트에서 React Routerreact-scroll 써서 부드러운 스크롤링 네비게이션 구현해봤는데, 삽질 좀 하다가 해결해서 공유해봄.


문제 상황

React로 웹 앱 만들던 중에 이런 요구사항이 나옴:

  1. 네비게이션 탭 클릭하면 해당 섹션으로 부드럽게 이동해야 함.
  2. 현재 보이는 섹션에 따라 네비게이션 탭 상태가 동적으로 변경되어야 함.
  3. 고정 헤더 때문에 섹션이 헤더에 가려지지 않아야 함.

이거 구현하다 보니까 다음 같은 문제들이 터짐:

  • React Router랑 scroll 충돌: 특정 경로로 이동하면 스크롤링 기능이 제대로 안 됨.
  • 탭 동기화 문제: 보이는 섹션이랑 탭 상태가 어긋남.
  • 헤더 오프셋 문제: 고정 헤더 때문에 섹션 일부가 잘림.

해결 과정

1. 라이브러리 설정

일단 react-router-dom이랑 react-scroll 설치함. 각각 라우팅이랑 스크롤 애니메이션 기능 제공해줌.

npm install react-router-dom react-scroll

2. 스크롤링 기능 구현

react-scrollscroller 사용해서 특정 섹션으로 부드럽게 이동하도록 구현함.

import { scroller } from 'react-scroll';

const handleScrollTo = (sectionId) => {
  scroller.scrollTo(sectionId, {
    duration: 800,
    delay: 0,
    smooth: 'easeInOutQuart',
    offset: -80, // 고정 헤더 높이만큼 오프셋
  });
};

3. React Router랑 통합

SPA 특성상 다른 페이지에서 네비게이션 클릭하면 메인 페이지로 돌아가 특정 섹션으로 스크롤해야 했음. React Router의 navigate를 써서 상태 전달함.

import { useNavigate } from 'react-router-dom';

const navigate = useNavigate();

const handleNavigation = (sectionId) => {
  navigate('/', { state: { scrollTo: sectionId } });
};

메인 페이지에서는 useEffect로 상태 기반 스크롤 처리했음.

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { scroller } from 'react-scroll';

const MainPage = () => {
  const location = useLocation();

  useEffect(() => {
    if (location.state?.scrollTo) {
      scroller.scrollTo(location.state.scrollTo, {
        duration: 800,
        delay: 0,
        smooth: 'easeInOutQuart',
        offset: -80,
      });
    }
  }, [location]);

  return (
    <div>
      {/* 섹션들 */}
      <section id="section1">...</section>
      <section id="section2">...</section>
      <section id="section3">...</section>
    </div>
  );
};

4. 현재 섹션이랑 탭 동기화

IntersectionObserver 써서 현재 보이는 섹션 감지하고, 해당 섹션에 맞는 탭을 활성화함.

import { useEffect, useState } from 'react';

const useActiveSection = (sections) => {
  const [activeSection, setActiveSection] = useState(null);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          setActiveSection(entry.target.id);
        }
      });
    }, { threshold: 0.6 });

    sections.forEach((id) => {
      const element = document.getElementById(id);
      if (element) observer.observe(element);
    });

    return () => {
      sections.forEach((id) => {
        const element = document.getElementById(id);
        if (element) observer.unobserve(element);
      });
    };
  }, [sections]);

  return activeSection;
};

useActiveSection 훅으로 활성화된 섹션 감지해서 탭 상태 업데이트함.


최종 코드 구조

MainPage.js

const MainPage = () => {
  const activeSection = useActiveSection(['section1', 'section2', 'section3']);

  return (
    <div>
      <header>
        <Tabs activeTab={activeSection} />
      </header>
      <section id="section1">섹션 1</section>
      <section id="section2">섹션 2</section>
      <section id="section3">섹션 3</section>
    </div>
  );
};

Tabs.js

const Tabs = ({ activeTab }) => (
  <nav>
    <button onClick={() => handleScrollTo('section1')} className={activeTab === 'section1' ? 'active' : ''}>
      섹션 1
    </button>
    <button onClick={() => handleScrollTo('section2')} className={activeTab === 'section2' ? 'active' : ''}>
      섹션 2
    </button>
    <button onClick={() => handleScrollTo('section3')} className={activeTab === 'section3' ? 'active' : ''}>
      섹션 3
    </button>
  </nav>
);

배운 점

이 작업 하면서 몇 가지 깨달은 점:

  1. React에서 상태 관리랑 DOM 조작의 균형: useEffect랑 라이브러리 조합으로 문제 해결 가능.
  2. IntersectionObserver 활용: 사용자 인터랙션 없이도 뷰포트 기반 상태 동기화 가능.
  3. 라이브러리 통합: react-scroll이랑 React Router 조합으로 UX 크게 개선 가능.

이렇게 부드러운 스크롤링 네비게이션 구현하는 과정을 정리해봤음. 혹시 질문이나 피드백 있으면 댓글로 남겨주셈! 😊

profile
개발하면서 습득한 지식 끄적끄적✍️

0개의 댓글