우리는 지금까지 컴포넌트를 만들고, 상태를 관리하고, 화면을 업데이트하는 방법에 대해 배웠습니다.
하지만 대부분의 웹 애플리케이션은 여러 개의 '페이지'로 구성되어 있고, 사용자는 이 페이지들 사이를 자유롭게 이동할 수 있어야 합니다.
전통적인 웹사이트는 페이지를 이동할 때마다 서버에서 새로운 HTML 문서를 받아왔지만, React와 같은 프레임워크로 만드는
싱글 페이지 애플리케이션(Single Page Application, SPA)
에서는 조금 다른 방식으로 페이지 전환을 처리합니다.
SPA는 처음에 하나의 HTML 페이지만 로드하고, 이후 페이지 이동 요청이 있을 때는 서버에 전체 페이지를 다시 요청하는 대신, JavaScript를 사용하여 동적으로 화면의 내용만 바꿔줍니다.
이러한 SPA의 페이지 이동, 즉
라우팅(Routing)
을 구현하는 데 가장 널리 사용되는 라이브러리가 바로 React Router입니다.
오늘은 React Router의 최신 버전인 v6를 중심으로, 단순히 사용하는 방법을 넘어
HTML5 History API를 사용하여 URL과 UI를 동기화하는 라우터입니다. 애플리케이션의 최상위 컴포넌트 중 하나로 감싸줍니다. 브라우저의 주소창을 사용하여 경로를 관리합니다. (잠시 후 더 자세히 알아볼게요!)
Routes
여러 Route 컴포넌트를 포함하는 컨테이너입니다. 현재 URL 경로와 가장 일치하는 첫 번째 Route를 찾아서 해당 Route의 element를 렌더링합니다.
Route
특정 경로(path)와 해당 경로에 보여줄 컴포넌트(element)를 연결하는 역할을 합니다.
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 페이지를 보여주는 데 사용됩니다.
를 사용하여 동작합니다.
History API는 개발자가 JavaScript를 통해 브라우저의 세션 기록(방문 기록 스택)을 조작할 수 있게 해주는 기능입니다.
주요 기능은 다음과 같습니다.
pushState(state, title, url)
브라우저의 세션 기록 스택에 새로운 상태를 추가합니다.
URL 주소창의 주소를 변경하지만, 페이지를 실제로 새로고침하지는 않습니다. 이것이 SPA에서 페이지 이동처럼 보이게 하는 핵심입니다.
Link 컴포넌트 클릭 시 내부적으로 이 기능이 사용됩니다.
replaceState(state, title, url)
현재 세션 기록을 새로운 상태로 교체합니다. 뒤로 가기 시 이전 기록이 남지 않아야 할 때 사용됩니다.
popstate 이벤트
사용자가 브라우저의 뒤로 가기/앞으로 가기 버튼을 사용하거나, JavaScript로 history.back(), history.forward() 등을 호출하여 URL이 변경될 때 발생하는 이벤트입니다.
BrowserRouter는 이 이벤트를 감지하여 현재 URL에 맞는 컴포넌트를 다시 렌더링합니다.
URL 매칭을 통해 선택된 Route의 element가 화면에 렌더링됩니다.
만약 중첩 라우트 구조라면, 부모 Route의 element가 먼저 렌더링되고, 그 안의 Outlet 컴포넌트 위치에 매칭된 자식 Route의 element가 렌더링됩니다.
Outlet은 마치 '이 자리에 자식 라우트의 내용을 넣어주세요'라고 React Router에게 알려주는 표시와 같습니다.
이 과정을 통해 복잡한 레이아웃 구조도 깔끔하게 관리하고 렌더링할 수 있습니다.
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의 상세 정보(pathname, search, hash, state)를 담고 있는 location 객체를 반환합니다. React Router는 URL이 변경될 때마다 이 location 객체를 업데이트하고, 이 훅을 사용하는 컴포넌트는 최신 location 정보를 받을 수 있습니다.
useSearchParams
: URL 쿼리 스트링을 파싱하고 업데이트하는 편리한 방법을 제공합니다. 내부적으로 location.search 값을 읽고, URLSearchParams API를 사용하여 쿼리 파라미터를 조작하며, 필요시 navigate 함수를 호출하여 URL을 업데이트합니다.
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로 감싸진 컴포넌트는 해당 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이 변경되면 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에 대해, 사용법뿐만 아니라 내부 동작 원리까지 함께 알아보았습니다.
SPA 라우팅은 브라우저의
History API
를 사용하여 페이지 새로고침 없이 URL을 변경하고 UI를 업데이트합니다.
React Router는
URL 매칭
과정을 통해 현재 경로에 맞는 Route를 찾아 해당 컴포넌트를 렌더링합니다.
Link
컴포넌트는 pushState를 호출하여 URL을 변경하고,
useNavigate
훅은 프로그래밍 방식으로 URL 변경을 가능하게 합니다.
useParams
,
useLocation
등 훅들은 URL 정보를 컴포넌트에서 쉽게 사용할 수 있도록 돕습니다.
중첩 라우트
와
Outlet
은 효율적인 레이아웃 관리와 코드 구성을 가능하게 하며, Outlet은 자식 라우트 컴포넌트의 렌더링 위치를 지정합니다.
코드 스플리팅
(React.lazy, Suspense)은 동적 import를 활용하여 초기 로딩 성능을 개선합니다.
React Router의 동작 원리를 이해하면 단순히 기능을 사용하는 것을 넘어, 라우팅 관련 문제를 해결하고 더 효율적인 애플리케이션 구조를 설계하는 데 큰 도움이 됩니다.
다음 시간에는 서버 측 렌더링(SSR), 정적 사이트 생성(SSG), 그리고 Hydration의 원리에 대해 알아보겠습니다. 웹 페이지가 사용자에게 보여지는 다양한 방식과 그 장단점을 비교하며 React 애플리케이션의 성능 및 SEO를 개선하는 방법을 탐구해 보겠습니다.