728x90
728x90

리액트 라우터(React Router)

들어가며

  • 리액트(React.js)에서 라우팅을 할 때 많이 사용하는 리액트 라우터(React Router) 라이브러리에 대해 정리해본다.
  • v6을 기준으로 정리하였다.

 

리액트 라우터(React Router)

개념

  • 리액트 애플리케이션에서 클라이언트 사이드 라우팅을 처리하기 위한 라이브러리
  • 리액트 라우터를 사용하면, 사용자가 페이지를 전환할 때 전체 페이지를 새로고침하지 않고도 URL에 따라 서로 다른 컴포넌트를 렌더링할 수 있다.

 

구성 요소

BrowserRouter
  • HTML5의 History API를 사용하여 URL을 관리한다.
  • 주로 애플리케이션의 최상위 컴포넌트로 사용된다.

 

Routes
  • 각 경로에 대한 라우트를 정의하는 컨테이너
  • 하나 이상의 @Route@를 자식으로 가질 수 있다.

 

Route
  • 특정 경로와 컴포넌트를 매핑한다.
  • 사용자가 특정 URL로 접근했을 때 어떤 컴포넌트를 보여줄지를 정의한다.

 

Link
  • 다른 경로로 이동하기 위한 컴포넌트
  • @<a>@ 태그처럼 작동하지만, 페이지를 새로 고침하지 않고 클라이언트 사이드에서 라우팅한다.

 

Navigate
  • 다른 경로로 리다이렉션(Redirection)하는 데 사용된다.

 

 

설치하기

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

 

사용하기

기본 사용법

import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;

const App = () => {
  return (
    <Router>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Router>
  );
};

export default App;

 

라우트 정의

  • @Route@ 컴포넌트는 @element@ prop을 사용하여 렌더링할 컴포넌트를 지정한다.
<Route path="/about" element={<About />} />

 

 

동적 라우팅(Dynamic Routing)

  • 리액트 라우터는 동적 라우팅(Dynamic Routing)을 지원하여 URL의 일부를 매개변수(Parameter)로 사용할 수 있다.
  • 예를 들어, @/users/:id@와 같은 경로를 정의하여 특정 사용자 ID에 따라 다른 내용을 표시할 수 있다.
import { useParams } from 'react-router-dom';

const User = () => {
  const { id } = useParams();
  
  return <h2>User ID: {id}</h2>;
};

// 라우트에서 사용
<Route path="/users/:id" element={<User />} />

 

중첩 라우트

  • 라우트를 계층적으로 구성할 수 있다.
const Dashboard = () => {
  return (
    <div>
      <h2>Dashboard</h2>
      <Routes>
        <Route path="settings" element={<Settings />} />
        <Route path="profile" element={<Profile />} />
      </Routes>
    </div>
  );
};

// 라우트에서 사용
<Route path="/dashboard/*" element={<Dashboard />} />

 

리다이렉션(Redirection)

  • 리다이렉션 기능을 사용하면 특정 경로에서 다른 경로로 자동으로 이동할 수 있다.
  • @Navigate@ 컴포넌트를 사용하여 리다이렉션을 처리할 수 있다.
import { Navigate } from 'react-router-dom';

<Navigate to="/login" />

 

404 에러 페이지 처리

  • 다음과 같이 @Routes@의 마지막에 @Route@를 추가하고, @path@를 @*@로 설정하여 404 에러 페이지를 처리할 수 있다.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="*" element={<NotFound />} />
</Routes>

 

여러가지 훅

  • @useNavigate@ : 프로그래밍적으로 페이지를 이동할 수 있도록 해준다.
  • @useNavigation@ : 경로 변경 상태를 추적할 수 있도록 도와준다. (@idle@, @loading@, @submitting@)
  • @useLocation@ : 현재 위치 정보를 가져올 수 있게 해준다.
  • @useMatch@ : 특정 경로와 매칭되는지 여부를 확인해준다.
  • @useParams@ : 동적 라우트 매개변수에 접근할 수 있게 해준다.
import { useNavigate } from 'react-router-dom';

const MyComponent = () => {
  const navigate = useNavigate();

  const handleClick = () => {
    navigate('/about');
  };

  return <button onClick={handleClick}>Go to About</button>;
};

 

⇒ 자세한 내용은 아래 글을 참고한다.

 

[React.js] 라우팅 관련 기능들 정리 (React Router) : useNavigate, useNavigation, redirect, useLocation, useParams, useH

라우팅 관련 기능들 정리 (React Router) : useNavigate, useNavigation, redirect, useLocation, useParams, useHistory, Navigate들어가며리액트(React.js)에서 라우팅을 위해 사용되는 관련 기능들에 대해 간단하게 정리해

dev-astra.tistory.com

 

데이터 로딩 및 처리

  • 로더(@loader@) 액션(@action@)을 통해 데이터를 로딩하거나 특정 작업을 처리할 수 있는 기능을 제공한다.
  •  이 기능은 @createBrowserRouter@@RouterProvider@와 함께 사용된다.

 

/src/App.jsx
import { createBrowserRouter, RouterProvider, Route } from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Home />,
    loader: () => fetch('/api/home'),
  },
  {
    path: '/about',
    element: <About />,
  },
]);

const App = () => (
  <RouterProvider router={router} />
);

 

createBrowserRouter와 RouterProvider

개념

  • 리액트 라우터 v6에서 사용 가능하며, 애플리케이션의 라우트를 더 효과적으로 관리할 수 있도록 해준다.
  • @createBrowserRouter@를 사용하면 라우트 정의로더, 액션한 곳에서 관리할 수 있다. 
  • 최상위 컴포넌트(@App.jsx@)에서 라우터 생성 및 설정을 해준다.

 

./src/App.jsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';

// 라우터 생성
const router = createBrowserRouter([
  {
    path: '/',
    element: <Home />,
  },
  {
    path: '/about',
    element: <About />,
  },
  {
    path: '*',
    element: <NotFound />,
  },
]);

// 애플리케이션의 최상위 컴포넌트
export const App = () => {
  return (
    <div>
      <RouterProvider router={router} />
    </div>
  );
};

export default App;

 

사용하기 : @children@

  • 중첩 라우트(Nested Route)를 정의하는 데 사용된다.
  • 라우트를 계층적으로 구성하고, 부모 라우트 아래에 여러 자식 라우트를 설정할 수 있다.
  • 중첩 라우트를 사용하면, 특정 경로에 따라 다른 컴포넌트를 렌더링하고, 구조적으로 애플리케이션을 조직할 수 있다.

 

예제 코드
  • 부모 컴포넌트 (@./src/App.jsx@)
import {
  createBrowserRouter,
  RouterProvider,
} from 'react-router-dom';

import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import Team from './pages/Team';

// 라우트 정의
const router = createBrowserRouter([
  {
    path: '/',
    element: <Home />,   // 부모 라우트 정의
    children: [ // 자식 라우트 정의
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'contact',
        element: <Contact />,
      },
      {
        path: 'team',
        element: <Team />,
      },
    ],
  },
]);

const App = () => {
  return (
    <div>
      <RouterProvider router={router} />
    </div>
  );
};

export default App;

 

  • @./src/components/Nav.jsx@
import { Link } from "react-router-dom";

const Nav = () => {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/contact">Contact</Link>
      <Link to="/team">Team</Link>
    </nav>
  );
};

export default Nav;

 

  • @./src/pages/Home.jsx@ (부모 컴포넌트)
    • @<Outlet>@은 중첩 라우트를 사용할 때 자식 라우트를 렌더링하는 데 사용되는 컴포넌트이다.
      • 자식 컴포넌트 : @<About>@, @<Contact>@, @<Team>@
    • 부모 라우트가 렌더링되는 위치에 @Outlet@을 삽입하면, 해당 경로에 맞는 자식 라우트가 그 자리에서 렌더링된다.
    • @<Outlet conext={{ value }} />@와 같이 @context@ prop을 이용하여 자식 컴포넌트에 전역 변수를 넘겨줄 수 있다. 
import { Outlet } from "react-router-dom";

import Nav from '../components/Nav'

const Home = () => (
  const value = "some value";

  <div>
    <Nav />
    <Outlet /> {/* 자식 컴포넌트를 렌더링할 위치 */}
  </div>
);

 

사용하기 : @errorElement@

  • 특정 라우트에서 오류가 발생했을 때 렌더링할 컴포넌트를 지정하기 위해 사용된다.
  • 예를 들어, 데이터 로딩 중 오류가 발생하거나 해당 컴포넌트가 예외를 던질 경우, 사용자에게 표시할 에러 페이지를 제공할 수 있다.

 

예제 코드
  • @./src/App.jsx@
import {
  createBrowserRouter,
  RouterProvider,
} from 'react-router-dom';

import Posts, { loader as postsLoader } from './pages/Posts';
import ErrorPage from './pages/ErrorPage';

// 라우트 정의
const router = createBrowserRouter([
  {
    path: '/posts',
    element: <Posts />,
    loader: loadData,
    errorElement: <ErrorPage />, // 에러 발생 시 표시할 컴포넌트
  },
]);

const App = () => {
  return (
    <div>
      <RouterProvider router={router} />
    </div>
  );
};

export default App;

 

  • @./pages/Posts@
import { useLoaderData } from 'react-router-dom';

// 데이터 로딩 함수
export const loader = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  
  if (!response.ok) {
    throw new Error('Failed to fetch data');
  }
  
  return response.json();
};

const Posts = () => {
  const posts = useLoaderData();

  return (
    <div>
      <h2>Posts</h2>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Posts;

 

  • @./pages/ErrorPage.jsx@
const ErrorPage = () => <h2>Something went wrong!</h2>;

export default ErrorPage;

 

로더(Loader)와 액션(Action)

  • 로더(@loader@)액션(@action@)클라이언트 사이드 라우팅에서 데이터를 로드하거나 작업을 수행하는 데 사용된다.
  • 애플리케이션의 URL에 따라 필요한 데이터를 미리 로드하고, 폼 제출이나 특정 이벤트에 대한 처리를 가능하게 한다.

 

로더(Loader)

  • 특정 경로에 대해 데이터를 비동기적으로 로드하는 데 사용된다.
  • URL이 변경될 때마다 로더가 호출되어 필요한 데이터를 가져올 수 있다.
  • @createBrowserRouter@를 통해 정의된 각 라우트에서 사용할 수 있다.
  • 서버에서 데이터를 비동기적으로 가져오는 작업을 처리한다.
  • URL 경로에 따라 로드할 데이터를 동적으로 결정할 수 있다. 
  • 로드된 데이터는 해당 경로의 컴포넌트에서 @useLoaderData@ 훅을 통해 접근할 수 있다. 

 

@loader@는 반드시 @return@ 값을 가져야 한다.
  • @loader@는 데이터를 미리 로드하고, 그 데이터를 라우트 컴포넌트로 전달하는 역할을 한다.
  • return 값이 없으면 @useLoaderData()@를 사용해서 데이터를 접근할 때 문제가 발생한다.
  • @loader@는 주로 비동기 함수로 정의되며, 보통 서버에서 데이터를 가져온 후 그 데이터를 반환한다.

 

예제 코드
  • @./src/components/UserList.jsx@
import { useLoaderData } from 'react-router-dom';

// 데이터 로딩 함수
export const loader = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  
  if (!response.ok) {
    throw new Error('Failed to fetch data');
  }
  
  return response.json();   // 반드시 반환값이 있어야 한다.
};

export const UserList = () => {
  const users = useLoaderData(); // 로더 함수에서 반환된 데이터를 사용하기

  return (
    <div>
      <h2>User List</h2>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default UserList;

 

  • @./src/App.jsx@
    • @loader@ 속성에 로더 함수를 넣어준다.
import {
  createBrowserRouter,
  RouterProvider,
} from 'react-router-dom';

import UserList, { loader as loadUserData } from './component/UserList';

// 라우터 생성
const router = createBrowserRouter([
  {
    path: '/users',
    element: <UserList />,
    loader: loadUserData, // 데이터 로딩 함수 연결
  },
]);

const App = () => {
  return <RouterProvider router={router} />;
};

export default App;

 

액션(Action)

  • 폼 제출 및 특정 이벤트에 대해 작업을 수행하는 데 사용된다.
  • 예를 들어,  데이터 생성, 업데이트, 삭제하는 등의 작업을 처리할 수 있다.
  • @createBrowserRouter@를 통해 정의된 각 라우트에서 사용할 수 있다.
  • 폼 데이터를 서버에 제출하거나 상태를 변경하는 비동기 작업을 처리한다.
  • URL 경로에 따라 처리할 작업을 동적으로 결정할 수 있다.
  • 작업이 완료된 후 특정 경로로 리다이렉션할 수 있다.
action은 return 값을 반드시 가질 필요는 없다.
예제 코드
  • @./components/Contact.jsx@
import { Form, redirect } from 'react-router-dom';

// 액션 처리 함수
export const action = async ({ request }) => {
  const formData = await request.formData();

  const data = Object.fromEntries(formData); // 키-값 쌍의 배열을 객체로 변환
  // data는 { name: "John", age: "30" } 형태의 객체가 된다.
  // -> 키 값은 input 요소의 name 속성을 통해 가져온다.

  try {
    const response = await axios.post('api.example.com', data);
    
    return redirect('/');    // 메인으로 이동
  } catch (error) {
    console.log(error);
    
    return error;
  }
};

export const Contact = () => {
  return (
    <div>
      <h2>Contact</h2>
      <Form>
        <input type="text" name="name" placeholder="Your Name" required />
        <input type="email" name="email" placeholder="Your Email" required />
        <button type="submit">Submit</button>
      </Form>
    </div>
  );
};

export default Contact;

 

  • @./src/App.jsx@
    • @action@ 속성에 액션 함수를 넣어준다.
import {
  createBrowserRouter,
  RouterProvider,
} from 'react-router-dom';

import Contact, { action as contactAction } from './components/Contact';

// 라우터 생성
const router = createBrowserRouter([
  {
    path: '/contact',
    element: <Contact />,
    action: contactAction, // 액션 연결
  },
]);

const App = () => {
  return <RouterProvider router={router} />;
};

export default App;

 

종합 예제 코드

  • 리액트 라우터의 종합적인 기능들이 포함되어 있는 예제 코드이다.
  • 실제 제작했었던 미니 프로젝트의 코드 중, 뼈대가 되는 일부 코드만 첨부하였다.

 

./src/App.jsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

import {
  About,
  HomeLayout,
  Landing,
  Error,
  Newsletter,
  Cocktail,
  SinglePageError,
} from './pages';

import { loader as landingLoader } from './pages/Landing';
import { loader as singleCocktailLoader } from './pages/Cocktail';
import { action as newsletterAction } from './pages/Newsletter';

const queryClient = new QueryClient({
  defaultOptions: {
    // 쿼리 지속 시간
    queries: {
      staleTime: 1000 * 60 * 5, // 5분
    },
  },
});

const router = createBrowserRouter([
  {
    // Nested Pages
    path: '/',
    element: <HomeLayout />,
    errorElement: <Error />,
    children: [
      {
        index: true,
        element: <Landing />,
        errorElement: <SinglePageError />, // 개별 페이지 에러
        loader: landingLoader(queryClient), // 로더 설정
      },
      {
        path: 'cocktail/:id',
        loader: singleCocktailLoader(queryClient),
        element: <Cocktail />,
        errorElement: <SinglePageError />,
      },
      {
        path: 'newsletter',
        element: <Newsletter />,
        action: newsletterAction,
        errorElement: <SinglePageError />,
      },
      {
        path: 'about',
        element: <About />,
      },
    ],
  },
]);

const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={router} />
      {/* <ReactQueryDevtools initialIsOpen={false} /> */}
    </QueryClientProvider>
  );
};

export default App;

 

./src/pages/index.js
export { default as Landing } from './Landing';
export { default as About } from './About';
export { default as Cocktail } from './Cocktail';
export { default as Newsletter } from './Newsletter';
export { default as HomeLayout } from './HomeLayout';
export { default as Error } from './Error';
export { default as SinglePageError } from './SinglePageError';

 

./src/pages/HomeLayout.jsx
import { useNavigation } from 'react-router-dom';
import { Outlet } from 'react-router-dom';

import Navbar from '../components/Navbar';

const HomeLayout = () => {
  const navigation = useNavigation(); // 'idle', 'loading'
  const isPageLoading = navigation.state === 'loading';
  const value = 'some value';

  return (
    <>
      <Navbar />
      <section className="page">
        {isPageLoading ? (
          <div className="loading"></div>
        ) : (
          <Outlet context={{ value }} /> // 전역 컨텍스트로 전달
        )}
      </section>
    </>
  );
};

export default HomeLayout;

 

참고 사이트

 

Guides | React Router

 

reactrouter.com

 

728x90
728x90