이번 포스팅에서는 라우터와 성능 개선을 위한 코드 스플리팅에 대해 간단히 정리해보겠다.
라우터는 실제 페이지 이동 없이 화면 전환을 도와주고, 코드 스플리팅은 사용자가 필요로 하는 시점에 해당 코드만 불러와 초기 로딩 속도를 개선하는 최적화 기법으로 자주 쓰이는 개념같다.
SPA(Single Page Application)의 필수 요소: 단일 HTML 페이지 위에서 여러 '페이지'를 보여주는 것처럼 만드는 기술임.
URL과 UI 동기화: 브라우저 주소창의 URL 변경 시 해당 URL에 맞는 컴포넌트를 화면에 렌더링하여, 사용자가 다른 페이지로 이동한 것처럼 느끼게 함.
클라이언트 사이드 라우팅: 서버에 페이지를 요청하는 대신, 브라우저(클라이언트) 자체에서 URL 변화를 감지하고 미리 로드된 자바스크립트를 이용해 페이지 전환을 처리함. 서버 부하 감소 및 빠른 페이지 전환 가능.
라이브러리: react-router-dom
라이브러리 사용이 일반적임.
createBrowserRouter()
: 브라우저 환경에서 사용되는 라우터 인스턴스 생성 함수. 경로 설정 객체 배열을 인자로 받음.
어떤 경로에 어떤 컴포넌트를 연결할지 직접 설정함.
const router = createBrowserRouter([
{
path: '/',
element: <Default />,
errorElement: <NotFound />,
children: [
{ path: '', element: <MainPage /> },
{ path: '/shop', element: <ShopPage /> },
{ path: '/about', element: <AboutPage /> },
{ path: '/blog', element: <BlogPage /> },
{ path: '/cart', element: <CartPage /> },
],
},
{
path: '*',
element: (
<Suspense fallback={<Loading />}>
<NotFound />
</Suspense>
),
},
])
RouterProvider
: createBrowserRouter
로 생성된 라우터 설정을 앱 전체에 적용하고 라우팅 기능을 활성화하는 컴포넌트. 앱 최상위 또는 라우팅이 필요한 부분의 상위에 위치 (main.jsx 같은 파일)
// main.jsx
import { RouterProvider } from 'react-router-dom';
import router from './router';
root.render(<RouterProvider router={router} />);
라우트 객체: 라우터 설정 배열의 각 요소. 경로와 컴포넌트를 매핑.
{ path: '주소', element: <컴포넌트 />, ... }
형식 path
: URL 경로 문자열 (예: '/', '/users', '/users/:id').element
: 해당 경로에 렌더링될 React 엘리먼트 (컴포넌트).children
: 중첩된 하위 라우트 객체 배열. 부모 라우트의 element
안에 렌더링됨.errorElement
: 라우트 로딩 또는 렌더링 중 에러 발생 시 보여줄 에러 컴포넌트.<Outlet />
: 부모 라우트의 element
내에서 중첩된 자식 라우트의 element
가 렌더링될 위치를 지정하는 컴포넌트. 레이아웃 컴포넌트 등에서 사용.
// layout/Default.jsx
import { Outlet } from 'react-router-dom';
function DefaultLayout() {
return (
<div>
<header>헤더</header>
<main>
<Outlet /> /* 자식 페이지 컴포넌트가 여기에 렌더링됨 */
</main>
<footer>푸터</footer>
</div>
);
}
useNavigate()
: 페이지 이동 함수(Maps
)를 반환하는 훅 (로그인안되면 바로 로그인 창 띄우는 거)
import { useNavigate } from 'react-router-dom';
function LoginPage() {
const navigate = useNavigate();
const handleLogin = () => {
// 로그인 성공 후 메인 페이지로 이동
navigate('/');
};
return <button onClick={handleLogin}>로그인</button>;
}
useParams()
: 동적 라우트의 URL 파라미터 값을 객체 형태로 반환하는 훅. (예: path: '/users/:userId'
에서 userId
값 추출)
import { useParams } from 'react-router-dom';
function UserProfilePage() {
const { userId } = useParams(); // { userId: '값' } 형태로 반환
return <div>사용자 ID: {userId}</div>;
}
useLocation()
: 현재 페이지의 위치 정보(location
객체)를 반환하는 훅. pathname
(경로), search
(쿼리스트링), hash
, state
(네비게이션 시 전달된 상태) 등의 정보 포함.
useSearchParams()
: URL 쿼리 스트링(?name=react&version=18
)을 읽고 업데이트하는 기능을 제공하는 훅.
중첩 라우트는 부모 라우트가 헤더나 사이드바 같은 공통 틀(레이아웃)을 딱 잡아주고, 그 안의 특정 자리에 자식 라우트들의 내용만 바꿔 끼우는 방식이다.
동적 라우트는 URL 주소 일부를 '변수'처럼 쓰는 기술이다. 예를 들어 /product/:productId라고 경로를 정하면, :productId 자리에는 '123'이든 'abc'든 뭐든 들어올 수 있다. 이렇게 URL에 담긴 변수 값은 useParams라는 훅으로 컴포넌트 안에서 꺼내 쓸 수 있다.
프로그래밍 방식 네비게이션은 사용자가 눌러서 이동하는 거 말고 (useNavigate, Link, NavLink), 코드 내에서 특정 조건(로그인 성공, 폼 제출 등)이 만족됐을 때 페이지를 강제로 이동시키는 방법(Navigate)이다.
상태 전달은 페이지 이동할 때 URL에는 안 보이는 임시 데이터를 몰래 같이 보내는 기능이다. 이동된 페이지에서는 useLocation 훅으로 location.state.userName 처럼 그 값을 받아서 쓸 수 있다. 근데 이건 진짜 임시라서, 페이지 새로고침하면 데이터가 싹 날아가니까 중요한 데이터는 이걸로 보내면 안 된다.
목적: 자바스크립트 번들 크기 감소 및 웹 애플리케이션 초기 로딩 성능 개선. 불필요한 코드 로딩 방지로 사용자 경험 향상.
정의: 애플리케이션 코드를 여러 개의 작은 조각(Chunk)으로 분할하고, 현재 필요한 코드만 동적으로 로드하는 기술.
기반 기술: 모듈 번들러(Vite, Webpack 등)가 지원하는 동적 import()
구문 활용.
대시보드 애플리케이션
대규모 SPA(Single Page Application)
이미지 갤러리 또는 미디어 중심 애플리케이션
데이터 집약적 애플리케이션
React.lazy()
역할: 컴포넌트 코드를 동적으로 불러오기 위한 React 내장 함수. 컴포넌트 렌더링 시점까지 로딩을 지연시킴.
사용법: 동적 import()
구문을 실행하는 함수를 인자로 전달. 해당 함수는 Promise를 반환해야 하며, 이 Promise는 default export
된 React 컴포넌트를 resolve해야 함.
import React, { lazy } from 'react';
// 컴포넌트 로딩을 지연시킴
const MyLazyComponent = lazy(() => import('./components/MyLazyComponent'));
반환값: 일반 React 컴포넌트처럼 JSX 내에서 사용할 수 있는 특수한 형태의 컴포넌트 반환.
React.Suspense
역할: React.lazy
로 로드되는 컴포넌트가 준비될 때까지(로딩 중) 사용자에게 보여줄 대체 UI(로딩 화면 등)를 제공하는 React 내장 컴포넌트.
필수 Prop: fallback
: 컴포넌트 로딩 대기 중에 렌더링할 React 엘리먼트(예: <p>로딩 중...</p>
, 스피너 컴포넌트 등)를 값으로 지정.
import React, { lazy, Suspense } from 'react';
import LoadingIndicator from './components/LoadingIndicator';
const MyLazyComponent = lazy(() => import('./components/MyLazyComponent'));
function App() {
return (
<div>
{/* MyLazyComponent가 로딩되는 동안 LoadingIndicator를 보여줌 */}
<Suspense fallback={<LoadingIndicator />}>
<MyLazyComponent />
</Suspense>
</div>
);
}
동작 방식: React가 lazy
컴포넌트 렌더링 시도 시, 해당 컴포넌트 코드가 아직 로드되지 않았으면 가장 가까운 상위의 Suspense
컴포넌트가 fallback
을 대신 렌더링함. 로딩 완료 후 실제 lazy
컴포넌트로 교체.
배치: lazy
컴포넌트는 반드시 Suspense
컴포넌트의 하위 트리 어딘가에 위치해야 정상 작동함.
라우트 기반 코드 스플리팅은 페이지(라우트)별로 코드를 나누는 가장 대표적인 방법이다. React.lazy로 페이지 컴포넌트를 감싸두면, 딱 그 페이지에 들어갈 때만 해당 코드를 불러온다.
조건부 렌더링 스플리팅은 당장 필요 없는 무거운 컴포넌트(예: 클릭해야 열리는 모달창) 코드를 React.lazy로 분리해두는 거다. 그래서 필요할 때만 불러오면 앱 초기 로딩이 한결 가벼워진다.
로딩 상태 UI/UX는 Suspense의 fallback으로 "지금 로딩 중" 인 걸 사용자에게 명확히 보여주는 게 중요하다는 것이다.
에러 처리는 코드를 불러오다 네트워크 문제 등으로 실패할 수 있으니, 앱이 멈추지 않도록 에러 경계(Error Boundary) 컴포넌트로 대비하는 것이 좋다.