PromleeBlog
sitemapaboutMe

posting thumbnail
React Router 원리와 활용법 - React, 알고 쓰자 6일차
React Router Principles and Usage - React Explained Day 6

📅

🚀

들어가기 전에🔗

우리는 지금까지 컴포넌트를 만들고, 상태를 관리하고, 화면을 업데이트하는 방법에 대해 배웠습니다.
하지만 대부분의 웹 애플리케이션은 여러 개의 '페이지'로 구성되어 있고, 사용자는 이 페이지들 사이를 자유롭게 이동할 수 있어야 합니다.
전통적인 웹사이트는 페이지를 이동할 때마다 서버에서 새로운 HTML 문서를 받아왔지만, React와 같은 프레임워크로 만드는
싱글 페이지 애플리케이션(Single Page Application, SPA)
에서는 조금 다른 방식으로 페이지 전환을 처리합니다.
SPA는 처음에 하나의 HTML 페이지만 로드하고, 이후 페이지 이동 요청이 있을 때는 서버에 전체 페이지를 다시 요청하는 대신, JavaScript를 사용하여 동적으로 화면의 내용만 바꿔줍니다.
이러한 SPA의 페이지 이동, 즉
라우팅(Routing)
을 구현하는 데 가장 널리 사용되는 라이브러리가 바로 React Router입니다.
오늘은 React Router의 최신 버전인 v6를 중심으로, 단순히 사용하는 방법을 넘어
어떻게 내부적으로 동작하는지
그 원리까지 함께 파헤쳐 보고, 다양한 기능을 활용하는 방법을 자세히 알아보겠습니다.

🚀

React Router v6 기본 설정하기🔗

React Router를 사용하기 위한 첫 단계는 라이브러리를 설치하고 기본적인 라우팅 구조를 설정하는 것입니다.

설치🔗

먼저 프로젝트에 react-router-dom 패키지를 설치해야 합니다.
npm 또는 yarn과 같은 패키지 매니저를 사용합니다.
npm install react-router-dom
 # 또는
yarn add react-router-dom

기본 구조: BrowserRouter, Routes, Route🔗

가장 기본적인 라우팅 설정은 다음과 같은 컴포넌트들을 사용합니다.
import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; // Link 추가
 
// 페이지 컴포넌트 예시
function HomePage() {
  return <h2>홈 페이지</h2>;
}
 
function AboutPage() {
  return <h2>소개 페이지</h2>;
}
 
// 네비게이션 컴포넌트 예시
function Navigation() {
  return (
    <nav>
      <ul>
        <li><Link to="/">홈</Link></li>
        <li><Link to="/about">소개</Link></li>
      </ul>
    </nav>
  );
}
 
 
function App() {
  return (
    <BrowserRouter>
      <h1>나의 앱</h1>
      <Navigation /> {/* 네비게이션 링크 */}
      <Routes>
        {/* path="/"는 홈페이지 경로 */}
        <Route path="/" element={<HomePage />} />
        {/* path="/about"은 소개 페이지 경로 */}
        <Route path="/about" element={<AboutPage />} />
        {/* 정의되지 않은 다른 모든 경로 처리 (예: 404 페이지) */}
        <Route path="*" element={<h2>페이지를 찾을 수 없습니다 (404)</h2>} />
      </Routes>
    </BrowserRouter>
  );
}
 
export default App;
이제 브라우저 주소창에 /를 입력하면 HomePage가, /about을 입력하면 AboutPage가 보이게 됩니다.
path="*"는 와일드카드 경로로, 위에 정의된 다른 경로들과 일치하지 않는 모든 경로를 처리합니다. 보통 404 Not Found 페이지를 보여주는 데 사용됩니다.

🚀

React Router는 어떻게 동작할까요?🔗

우리가 설정한 대로 React Router가 URL 변경에 따라 적절한 컴포넌트를 보여주는 마법은 어떻게 일어날까요?
그 핵심에는 브라우저의 History API와 React Router 내부의 매칭 로직이 있습니다.

History API: 브라우저 기록 조작하기🔗

BrowserRouter는 브라우저에 내장된
History API
를 사용하여 동작합니다.
History API는 개발자가 JavaScript를 통해 브라우저의 세션 기록(방문 기록 스택)을 조작할 수 있게 해주는 기능입니다.
주요 기능은 다음과 같습니다.
즉, BrowserRouter는 History API를 통해 URL 변경을
감지
하고, 변경된 URL에 따라 어떤 컴포넌트를 보여줄지 결정하는 역할을 합니다.

URL 매칭: 어떤 Route를 보여줄까?🔗

사용자가 Link를 클릭하거나 URL이 변경되면, React Router는 현재 URL 경로를 가지고 Routes 컴포넌트 내의 Route 자식들과 비교하는
매칭 과정
을 수행합니다.
이 매칭 과정 덕분에 현재 URL에 해당하는 정확한 UI를 사용자에게 보여줄 수 있습니다.

렌더링과 Outlet의 역할🔗

URL 매칭을 통해 선택된 Route의 element가 화면에 렌더링됩니다.
만약 중첩 라우트 구조라면, 부모 Route의 element가 먼저 렌더링되고, 그 안의 Outlet 컴포넌트 위치에 매칭된 자식 Route의 element가 렌더링됩니다.
Outlet은 마치 '이 자리에 자식 라우트의 내용을 넣어주세요'라고 React Router에게 알려주는 표시와 같습니다.
이 과정을 통해 복잡한 레이아웃 구조도 깔끔하게 관리하고 렌더링할 수 있습니다.

🚀

주요 컴포넌트와 훅 살펴보기🔗

이제 주요 기능들을 다시 한번 동작 원리 관점에서 살펴보겠습니다.
사용자가 클릭하여 다른 페이지로 이동할 수 있는 링크를 만들 때는 일반적인 HTML <a> 태그 대신 React Router의 Link 컴포넌트를 사용해야 합니다.
➡️

동작 원리🔗

사용자가 Link 컴포넌트를 클릭하면,
  1. 기본 <a> 태그의 동작(페이지 새로고침)을 막습니다(preventDefault).
  2. History API의 pushState 함수를 호출하여 브라우저 주소창의 URL을 to prop에 지정된 값으로 변경하고, 세션 기록 스택에 새 항목을 추가합니다.
  3. React Router는 변경된 URL을 감지하고, 새로운 URL에 맞는 컴포넌트를 렌더링하기 위해 리렌더링 과정을 시작합니다.
<a> 태그와의 차이점
: 페이지 전체를 서버에서 다시 로드하지 않으므로 훨씬 빠르고 부드러운 사용자 경험을 제공합니다.
// Navigation 컴포넌트 예시 (기존과 동일)
import { Link } from 'react-router-dom';
 
function Navigation() {
  return (
    <nav>
      <ul>
        <li>
          {/* to prop에 이동할 경로 지정 */}
          <Link to="/">홈</Link>
        </li>
        <li>
          <Link to="/about">소개</Link>
        </li>
      </ul>
    </nav>
  );
}
Q:"Link 컴포넌트와 HTML의 <a> 태그의 차이점은 무엇인가요?"
A: "내부 동작 원리(History API 사용, 페이지 새로고침 방지)"를 포함하여 답변.

프로그래밍 방식 이동: useNavigate 훅🔗

버튼 클릭 후 특정 로직을 처리한 다음 페이지를 이동시키는 등, 프로그래밍 방식으로 페이지를 이동해야 할 때가 있습니다.
이때는 useNavigate 훅을 사용합니다.
➡️

동작 원리🔗

useNavigate 훅이 반환하는 navigate 함수를 호출하면, React Router는 내부적으로 History API의 pushState (기본 동작) 또는 replaceState (옵션 지정 시)를 호출하여 URL을 변경하고 리렌더링을 유발합니다.
navigate 함수에 전달된 경로나 숫자에 따라 적절한 History API 메서드를 사용합니다.
import { useNavigate } from 'react-router-dom';
 
function LoginPage() {
  const navigate = useNavigate();
 
  const handleLogin = () => {
    const isLoggedIn = true;
    if (isLoggedIn) {
      // 내부적으로 history.pushState('/') 와 유사하게 동작
      navigate('/');
      // 로그인 페이지 기록을 남기지 않으려면 replace 사용
      // navigate('/', { replace: true }); // history.replaceState('/') 와 유사
    } else {
      alert('로그인 실패!');
    }
  };
 
  return (
    <div>
      <h2>로그인</h2>
      <button onClick={handleLogin}>로그인 하기</button>
    </div>
  );
}

URL 파라미터 처리: useParams 훅🔗

동적인 경로 세그먼트(URL 파라미터) 값을 가져올 때 useParams 훅을 사용합니다.
➡️

동작 원리🔗

React Router는 URL 매칭 과정에서 현재 URL과 Route의 path를 비교하면서 동적 세그먼트에 해당하는 값을 추출합니다.
useParams 훅은 이렇게 추출된 파라미터들을 객체 형태로 반환하여 컴포넌트에서 쉽게 사용할 수 있도록 제공합니다.
import { useParams } from 'react-router-dom';
 
// Route 설정
// <Route path="/users/:userId" element={<UserProfilePage />} />
 
function UserProfilePage() {
  // URL이 '/users/alice' 라면, params는 { userId: 'alice' } 가 됨
  const params = useParams<{ userId: string }>();
 
  return (
    <div>
      <h2>사용자 프로필</h2>
      <p>사용자 ID: {params.userId}</p>
    </div>
  );
}

기타 유용한 훅들🔗


🚀

중첩 라우트와 Outlet: 페이지 레이아웃 관리 (원리 재강조)🔗

중첩 라우트는 공통 레이아웃을 효율적으로 관리하는 방법입니다.
➡️

동작 원리🔗

URL 매칭 시, React Router는 현재 경로와 가장 잘 맞는 중첩된 Route 경로를 찾습니다.
예를 들어 URL이 /dashboard/analytics 라면, 먼저 / (MainLayout)에 매칭되고, 다음으로 dashboard (Outlet), 마지막으로 analytics (DashboardAnalytics)에 매칭됩니다.
렌더링 시에는 부모 Route(MainLayout)의 element가 렌더링되고, 그 안의 Outlet 위치에 가장 깊게 매칭된 자식 Route(DashboardAnalytics)의 element가 렌더링되는 방식으로 동작합니다.
// 중첩 라우트 설정 예시 (기존과 동일)
import React from 'react'; // React 임포트
import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom'; // 필요한 모듈 임포트
 
// 공통 레이아웃 컴포넌트
function MainLayout() { /* ... Outlet 포함 ... */ }
// 페이지 컴포넌트들
function DashboardHome() { return <h3>대시보드 홈</h3>; }
function DashboardAnalytics() { return <h3>대시보드 분석</h3>; }
function SettingsPage() { return <h2>설정</h2>; }
 
function AppNested() { // 컴포넌트 이름 변경 (App 중복 방지)
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<MainLayout />}>
          <Route index element={<h2>홈페이지 컨텐츠</h2>} />
          <Route path="dashboard" element={<Outlet />}>
            <Route index element={<DashboardHome />} />
            <Route path="analytics" element={<DashboardAnalytics />} />
          </Route>
          <Route path="settings" element={<SettingsPage />} />
        </Route>
        <Route path="*" element={<h2>페이지를 찾을 수 없습니다 (404)</h2>} />
      </Routes>
    </BrowserRouter>
  );
}

🚀

동적 라우팅과 코드 스플리팅: 성능 최적화🔗

코드 스플리팅은 React.lazy와 Suspense를 사용하여 구현합니다.
➡️

동작 원리🔗

React.lazy로 감싸진 컴포넌트는 해당 Route가 처음 매칭되어 렌더링될 때 비동기적으로 로드됩니다.
로딩이 진행되는 동안 Suspense 컴포넌트가 fallback UI를 보여줍니다.
로딩이 완료되면 Suspense는 로드된 컴포넌트를 렌더링합니다.
이는 JavaScript 번들링 도구(Webpack, Vite 등)의 동적 import() 구문과 함께 작동하여 코드를 작은 청크(chunk)로 분할하고 필요할 때만 로드하는 방식으로 구현됩니다.
import React, { lazy, Suspense } from 'react'; // lazy, Suspense 임포트
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; // 필요한 모듈 임포트
 
// 동적 임포트
const LazyHomePage = lazy(() => import('./pages/HomePage'));
const LazyAboutPage = lazy(() => import('./pages/AboutPage'));
 
function AppLazy() { // 컴포넌트 이름 변경 (App 중복 방지)
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">홈</Link> | <Link to="/about">소개</Link>
      </nav>
      <Suspense fallback={<div>페이지 로딩 중...</div>}>
        <Routes>
          <Route path="/" element={<LazyHomePage />} />
          <Route path="/about" element={<LazyAboutPage />} />
          <Route path="*" element={<h2>페이지를 찾을 수 없습니다 (404)</h2>} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}
 
// ./pages/HomePage.tsx 예시 (default export 필요)
// import React from 'react';
// function HomePage() { return <h2>홈 페이지</h2>; }
// export default HomePage;

🚀

URL 파라미터 의존 렌더링과 최적화🔗

URL 파라미터 변경 시 컴포넌트가 새 데이터를 가져오는 것은 useEffect 훅의 의존성 배열을 통해 구현됩니다.

동작 원리🔗

URL이 변경되면 React Router는 새로운 파라미터 값으로 컴포넌트를 리렌더링합니다.
이때 useParams 훅은 새로운 파라미터 값을 반환합니다. useEffect는 의존성 배열([userId])에 포함된 값이 변경되었음을 감지하고, effect 함수(데이터 요청 로직)를 다시 실행합니다.
이 과정을 통해 파라미터 변경에 따른 데이터 업데이트가 자동으로 이루어집니다.
// useEffect를 사용한 데이터 페칭 예시 (기존과 동일)
import React, { useState, useEffect } from 'react'; // useState, useEffect 임포트
import { useParams } from 'react-router-dom'; // useParams 임포트
 
// 가짜 API 함수
async function fetchUserData(userId: string): Promise<{ id: string; name: string }> { /* ... */ return { id: userId, name: `사용자 ${userId.toUpperCase()}` }; }
 
function UserProfilePageFetch() { // 컴포넌트 이름 변경
  const { userId } = useParams<{ userId: string }>();
  const [userData, setUserData] = useState</*...*/ | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
 
  useEffect(() => {
    if (userId) {
      // ... 데이터 요청 로직 ...
    }
  }, [userId]); // userId 변경 시 재실행
 
  // ... 렌더링 로직 ...
  if (!userData) return null; // 초기 렌더링 또는 오류 시 null 반환
  return (
    <div>
      <h2>사용자 프로필</h2>
      <p>ID: {userData.id}</p>
      <p>이름: {userData.name}</p>
    </div>
  );
}

🚀

결론🔗

오늘은 React 애플리케이션에서 페이지 이동을 구현하는 핵심 라이브러리인 React Router v6에 대해, 사용법뿐만 아니라 내부 동작 원리까지 함께 알아보았습니다.
React Router의 동작 원리를 이해하면 단순히 기능을 사용하는 것을 넘어, 라우팅 관련 문제를 해결하고 더 효율적인 애플리케이션 구조를 설계하는 데 큰 도움이 됩니다.
다음 시간에는 서버 측 렌더링(SSR), 정적 사이트 생성(SSG), 그리고 Hydration의 원리에 대해 알아보겠습니다. 웹 페이지가 사용자에게 보여지는 다양한 방식과 그 장단점을 비교하며 React 애플리케이션의 성능 및 SEO를 개선하는 방법을 탐구해 보겠습니다.

참고🔗