본문 바로가기
나도 공부한다/React

리액트 라우터를 알아보자 (리액트 인터뷰 가이드 4장 보충)

by 꾸빵이 2024. 10. 29.

서론

 

리액트 인터뷰 가이드라는 책으로 스터디를 시작했는데 아무래도 책이 얇다보니 보충 공부를 많이 해야한다. 사실 남들은 다 아는건데 나만 몰라서 이렇게 느끼는 걸지도? 아무튼 앞으로도 보충공부한 내용을 기록해보려고 한다. 틀린 부분이 있으면 언제든 댓글 달아주세요!

 

이번 장은 라우터 이론이었는데 인강으로 리액트 공부할 때 사용 방법만 배워서 책 내용이 조금 어렵게 느껴졌다.

 

1. 리액트에서 라우터, 라우팅, 라우트란?

Routing

url과 매칭되는 특정 페이지를 보여주는 기능

모든 자식 라우트를 검색해 가장 적합한 항목을 찾고 해당 UI 분기를 렌더링함

 

Router

정의 페이지나 컴포넌트 간에 이동할 수 있는 라이브러리. 리액트 자체에서는 라우팅 기능을 제공하지 않기 때문에 라이브러리를 사용해야한다.

라우터를 사용하면 클라이언트 사이드 라우팅이 가능하다. 클라이언트 사이드 라우팅은 애플리케이션의 소스를 미리 받아두고 url에 맞는 컴포넌트를 렌더링할 뿐이다. 즉, 페이지를 이동한다고 해서 서버에게 다시 페이지를 요청하는 게 아니다. 따라서 동적이고 로딩이 더 빠른 사용자 경험을 제공할 수 있다.

 

Route

url과 컴포넌트를 연결하는 컴포넌트. 라우트 중첩을 통해 레이아웃을 일관되게 유지할 수 있고 정보 의존성이 간단해진다.는 장점이 있다.

 

Routes

라우트들을 객체로 묶은 모음. 파일 내의 다른 자식 라우트와 매칭시키는 역할.

 

레이아웃을 어떻게 일관되게 유지하는가?

예를 들어 애플리케이션 구조가 다음과 같다고 가정해보자

/about (소개 페이지)

/about/company (회사 소개 페이지)

/about/team (팀 소개 페이지)

 

이 때 중첩된 자식 라우트를 이용해서 company 페이지와 team 페이지가 about의 레이아웃을 재사용하게 만들 수 있다.

about/team에 접속하면 Layout이 항상 렌더링되고 자식 요소로 team이 렌더링 된다.

import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/about",
    element: <Layout />,  // 공통 레이아웃
    children: [
      { path: "team", element: <Team /> },     // /about/team
      { path: "company", element: <Company /> } // /about/company
    ],
  },
]);

<RouterProvider router={router} />;

 

<BrowserRouter>와 <Route> 컴포넌트로만 경로를 설정해봤던 사람은 위 코드가 낯설 수 있는데 5번에서 다시 언급할거다.

 

정보 의존성이 간단해진다는 게 무슨 뜻일까?

BrowserRouter과 Route를 이용하는 기존 방식에서는 데이터를 전달하려면 props를 사용해야했고 (prop drilling 발생) 라우터에서 데이터를 로드하는 기능이 없었기 때문에 바깥에서 로드한 데이터를 직접 넘겨줘야하는 불편함이 있었다.

하지만 라우터 객체 설정 방식은 부모 라우트의 데이터를 Outlet, useLoaderData로 쉽게 가져올 수 있다. 따라서 복잡한 데이터 의존성을 가지는 경우에 유리하다.

 

Outlet, useLoaderData는 중첩 라우팅이 아니어도 사용할 수 있다. 하지만 자식 라우트가 부모 라우트의 데이터에 쉽게 접근할 수 있다는 점에서 중첩 라우팅인 경우에 더 효과적이다.

import { createBrowserRouter, RouterProvider, Outlet, useLoaderData } from "react-router-dom";

// 부모 컴포넌트 (공통 레이아웃 컴포넌트)
function Layout() {
  // API 호출 결과로 받은 데이터를 useLoaderData로 가져옴
  const data = useLoaderData();  // 팀 정보와 회사 정보를 포함
  
  return (
    <div>
      <h1>About Us</h1>
      <Outlet context={data} />  {/* 자식 컴포넌트에 데이터 전달 */}
    </div>
  );
}

// 자식 컴포넌트들
function Team() {
  const { team } = useOutletContext();  // 팀 정보를 받아서 사용
  return <div>Team Members: {team}</div>;
}

function Company() {
  const { company } = useOutletContext();  // 회사 정보를 받아서 사용
  return <div>Company Info: {company}</div>;
}

// 라우터 설정
const router = createBrowserRouter([
  {
    path: "/about",
    element: <Layout />,
    loader: async () => {
      // 팀 정보와 회사 정보를 가져오는 API 호출
      const team = await fetchTeamInfo();
      const company = await fetchCompanyInfo();
      return { team, company };
    },
    children: [
      { path: "team", element: <Team /> },
      { path: "company", element: <Company /> },
    ],
  },
]);

<RouterProvider router={router} />;

 

 

2. BrowserRouter와 createBrowserRouter

BrowserRouter
HTML5의 히스토리 API를 활용해 경로 변경을 관리. Routes, Route를 사용함. 간단한 경로 기반 라우팅을 구현할 때 좋음

 

createBrowserRouter
라우트 별로 데이터 로딩, 에러 핸들링할 수 있음. RouterProvider를 사용함. createBrowserRouter에서 생성한 라우터 객체를 RouterProvider에 전달하는 방식.

 

 

3. 라우터 객체 설정 방법

아마 예전 강의를 듣고 이후에 학습을 하지 않은 사람들은 위의 코드들이 익숙하지 않을 것이다.
위의 방식은 React Router 6.4버전 이상부터 도입되었으며, 구성 요소들을 객체로 한 번에 정의한다는 특징이 있다.

 

우선 기존 방식과 생김새를 비교해보자

 

[기존]

import { BrowserRouter, Routes, Route } from "react-router-dom";

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="about" element={<About />} />
  </Routes>
</BrowserRouter>

 

[라우터 객체 방식]

const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "about", element: <About /> },
]);

<RouterProvider router={router} />

 

라우터 객체 방식은 createBrowserRouter 등 라우터 생성 함수로 경로를 정의하고 객체를 RouterProvider에 전달하는 구조다. 경로와 렌더링 요소들을 배열형식으로 함수에 넘겨준다.

 

3. 라우트 타입

라우터 생성 함수 내부에 들어가는 path, children, loader 같은 것들을 라우트 타입이라고 한다.

  • path : url 경로를 정의
  • element : 특정 경로에 접근했을 때 렌더링할 컴포넌트 정의
  • loader : 라우트가 렌더링 되기 전에 호출되어 데이터를 비동기로 가져옴. 해당 라우트 컴포넌트에서 useLoaderData로 이 데이터를 가져올 수 있음.
loader: async () => {
      // 팀 정보와 회사 정보를 가져오는 API 호출
      const team = await fetchTeamInfo();
      const company = await fetchCompanyInfo();
      return { team, company };
    },
  • action : 폼 제출과 같은 사용자의 상호작용을 처리. 데이터를 서버로 보내야할 때 주로 사용됨. 폼 제출 후 실행될 로직을 미리 정의하고 그 결과를 useActionData로 가져온다. 이건 조금 복잡해서 예시가 필요하다.

    LoginPage 컴포넌트 - 라우터 - loginAction 함수 순서로 보면 된다.
    LoginPage에서 폼을 submit하면 action에 정의된 loginAction함수가 실행되고 매개변수로 request 객체를 받아 formData.get()으로 제출 정보를 가져온다. 이 정보를 서버에 보내고 요청 실패시 에러 메세지를 띄우고 성공시 성공 메세지를 띄우는 코드이다.
import { createBrowserRouter, RouterProvider, useActionData, Form } from "react-router-dom";

// 로그인 처리 액션 함수
const loginAction = async ({ request }) => {
  const formData = await request.formData();  // 제출된 폼 데이터 가져오기
  const username = formData.get("username");
  const password = formData.get("password");

  // 서버에 로그인 요청 보내기
  const response = await fetch("/api/login", {
    method: "POST",
    body: JSON.stringify({ username, password }),
    headers: {
      "Content-Type": "application/json",
    },
  });

  if (!response.ok) {
    return { error: "로그인에 실패했습니다." };
  }

  return { success: "로그인에 성공했습니다!" };  // 성공 메시지 반환
};

// 로그인 페이지 컴포넌트
function LoginPage() {
  const actionData = useActionData();  // action 함수의 반환 데이터를 가져옴

  return (
    <div>
      <h1>로그인</h1>
      <Form method="post">
        <label>
          사용자명: <input type="text" name="username" />
        </label>
        <label>
          비밀번호: <input type="password" name="password" />
        </label>
        <button type="submit">로그인</button>
      </Form>
     
      {actionData?.error && <p style={{ color: "red" }}>{actionData.error}</p>}
      {actionData?.success && <p style={{ color: "green" }}>{actionData.success}</p>}
    </div>
  );
}

// 라우터 설정
const router = createBrowserRouter([
  {
    path: "/login",
    element: <LoginPage />,
    action: loginAction,  // 폼 제출 시 loginAction 함수 실행
  },
]);

<RouterProvider router={router} />;
  • children : 중첩 라우트에서 자식 라우트를 정의한다.
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/about",
    element: <Layout />,  // 공통 레이아웃
    children: [
      { path: "team", element: <Team /> },     // /about/team
      { path: "company", element: <Company /> } // /about/company
    ],
  },
]);

<RouterProvider router={router} />;
  • index : 중첩 라우트 구조에서 부모 경로에 접근했을 때 기본으로 렌더링될 자식 라우트에 부여하는 속성.
    예시에서 /about에 접근했을 때 AboutHome 컴포넌트가 기본으로 렌더링된다.
const router = createBrowserRouter([
  {
    path: "/about",
    element: <Layout />,
    children: [
      { index: true, element: <AboutHome /> },  // 기본 경로
      { path: "team", element: <Team /> },
      { path: "company", element: <Company /> },
    ],
  },
]);

 

4. element 타입 VS Component 타입

리액트 라우터 v6에서는 다음과 같은 장점 때문에 component 대신 element를 사용한다.

  • prop 전달 방식의 단순화
    component를 사용했을 때는 render prop, 고차 컴포넌트와 같이 복잡한 방식으로 데이터를 전달해야했다.
    element는 JSX 요소 자체를 전달하므로 <Route path=":userId" element={<Profile animate={true} />} /> 처럼 간단히 prop을 추가할 수 있다.
<Route
  path=":userId"
  render={routeProps => (
    <Profile routeProps={routeProps} animate={true} />
  )}
/>

 

왜 component 대신 element를 사용하나요? (공식문서)

 

FAQs | React Router

 

reactrouter.com

 

리액트 v6 시작하기 (공식문서)

 

Feature Overview | React Router

 

reactrouter.com

 

 

5. Outlet의 역할

중첩 라우트에서 부모 라우트는 <Outlet>을 렌더링함으로써 자식 라우트를 렌더링 한다.

부모 컴포넌트에 <Outlet>을 넣어두면 자식 라우트의 컴포넌트가 부모 컴포넌트의 <Outlet> 위치에 렌더링된다.
자식 라우트가 활성화될 때마다 <Outlet>에 해당 자식 라우트 컴포넌트가 표시되므로, 중첩된 경로를 쉽게 관리할 수 있다.

즉, 자식 라우트를 렌더링할 위치를 지정하는 것이다.

import { createBrowserRouter, RouterProvider, Outlet, Link } from "react-router-dom";

function Layout() {
  return (
    <div>
      <h1>About Page</h1>
      {/* 자식 라우트가 이 위치에 렌더링 */}
      <Outlet />
    </div>
  );
}

function Team() {
  return <div>Team Page</div>;
}

function Company() {
  return <div>Company Page</div>;
}

const router = createBrowserRouter([
  {
    path: "/about",
    element: <Layout />,  // 부모 컴포넌트
    children: [
      { path: "team", element: <Team /> },
      { path: "company", element: <Company /> },
    ],
  },
]);

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

export default App;

 

 

8. Link와 a태그의 차이

페이지 전체를 다시 로드하지 않고 클라이언트 사이드에서 경로변경을 처리하여 더 빠르고 부드럽게 동작한다. Link는 내부 경로로 이동할 때, a 태그는 외부 사이트로 이동할 때 사용하는 게 좋다.

 


출처

리액트 라우터 공식문서

 

Feature Overview | React Router

 

reactrouter.com