React Router v6 튜토리얼

velopert·2021년 12월 27일
97
post-thumbnail

지난 11월에 리액트 라우터 v5 → v6 에서 어떤 업데이트가 있었는지 소개하는 영상은 찍었었으나, 리액트 라우터 v6를 새로 접하시는 입문자들을 위하여 리액트 라우터 v6 튜토리얼을 작성하게 됐습니다. 이 내용은 리액트를 다루는 기술 개정판 8쇄에도 실리게 됩니다.

이 튜토리얼에서 사용된 코드는 GitHub Repo에서 열람 가능합니다.

1. 라우팅이란?

웹 애플리케이션에서 라우팅이라는 개념은 사용자가 요청한 URL에 따라 알맞는 페이지를 보여주는 것을 의미합니다. 웹 애플리케이션을 만들때 프로젝트를 하나의 페이지로 구성할 수도 있고, 여러 페이지를 구성할 수도 있겠지요.

예를 들어, 우리가 이전에 만들었던 일정 관리 애플리케이션에서는 하나의 페이지로 충분할 수 있겠지만, 우리가 블로그를 만든다고 가정해봅시다. 블로그 애플리케이션은 주로 여러 페이지로 구성이 되어있습니다. 어떤 페이지가 필요한지, 한번 생각을 해볼까요?

  • 글쓰기 페이지: 새로운 포스트를 작성하는 페이지 입니다.
  • 포스트 목록 페이지: 블로그에 작성된 여러 포스트들의 목록을 보여주는 페이지입니다.
  • 포스트 읽기 페이지: 하나의 포스트를 보여주는 페이지입니다.

이렇게 여러 페이지로 구성된 웹 애플리케이션을 만들 때 페이지별로 컴포넌트들을 분리해가면서 프로젝트를 관리하기 위해 필요한 것이 바로 라우팅 시스템입니다.

리액트에서 라우트 시스템을 구축하기위해 사용할 수 있는 선택지는 크게 두가지가 있습니다. 어떤 선택지들이 있는지 한번 알아보겠습니다.

  • 리액트 라우터(React Router): 이 라이브러리는 리액트의 라우팅 관련 라이브러리들 중에서 가장 오래됐고, 가장 많이 사용되고 있습니다. 이 라이브러리는 컴포넌트 기반으로 라우팅 시스템을 설정할 수 있습니다.
  • Next.js: Next.js 는 리액트 프로젝트의 프레임워크입니다. 이 프레임워크는 우리가 사용했던 Create React App처럼 리액트 프로젝트 설정을 하는 기능, 라우팅 시스템, 최적화, 다국어 시스템 지원, 서버 사이드 렌더링 등 다양한 기능들을 제공합니다. 이 프레임워크의 라우팅 시스템은 파일 경로 기반으로 작동합니다. 이 프레임워크는 리액트 라우터의 대안으로 많이 사용되고 있습니다.

라우팅 관련 기능을 리액트 라이브러리에서 공식적으로 지원하는 것이 아니라 써드 파티로서 제공되기 때문에, 이 외에도 react-location, rakkas 등의 프로젝트들이 존재합니다.

이 책에서 우리는 리액트 라우터를 사용하여 라우팅 시스템을 구축하겠습니다. 이 라이브러리를 선택한 이유는, 가장 인기있고, 라우팅 기능만을 집중한 라이브러리이기 때문에 리액트 프로젝트에서의 라우팅 시스템 개념을 익히기에 적합하기 때문입니다. 오랫동안 개발된 프로젝트인만큼 편리하고 다양한 라우팅 기능을 제공하며 프로덕션에서 사용하기에 안정적입니다.

리액트 라우터를 사용하면 손쉽게 리액트 라우터로 싱글 페이지 애플리케이션 (Single Page Application)을 만들 수 있습니다.

2. 싱글 페이지 애플리케이션이란?

싱글 페이지 애플리케이션이란, 한 개의 페이지로 이루어진 애플리케이션이라는 의미입니다. 리액트 라우터를 사용하여 여러 페이지로 구성된 프로젝트를 만들 수 있다고 했었는데 왜 싱글 페이지 애플리케이션이라고 불리는지 의문이 들 수 있습니다.

이를 이해하기 위해서는, 싱글 페이지 애플리케이션이란 개념이 생기기 전에 사용되던 멀티 페이지 애플리케이션은 어떻게 작동하는지 살펴볼 필요가 있습니다.

멀티 페이지 애플리케이션에서는 사용자가 다른 페이지로 이동할 때마다 새로운 html을 받아오고, 페이지를 로딩할 때마다 서버에서 CSS, JS, 이미지 파일 등의 리소스를 전달받아 브라우저 화면에 보여 주었습니다. 각 페이지마다 다른 html 파일을 만들어서 제공을 하거나, 데이터에 따라 유동적인 html을 생성해 주는 템플릿 엔진을 사용하기도 했죠.

사용자 인터랙션이 별로 없는 정적인 페이지들은 기존의 방식이 적합하지만, 사용자 인터랙션이 많고 다양한 정보를 제공하는 모던 웹 애플리케이션은 이 방식이 적합하지 않았습니다. 새로운 페이지를 보여주어야 할 때마다 서버 측에서 모든 준비를 한다면 그만큼 서버의 자원을 사용하는 것이고, 트래픽도 더 많이 나올 수 있기 때문이죠.

그래서, 리액트 같은 라이브러리를 사용해서 뷰 렌더링을 사용자의 브라우저가 담당하도록 하고, 우선 웹 애플리케이션을 브라우저에 불러와서 실행시킨 후에 사용자와의 인터랙션이 발생하면 필요한 부분만 자바스크립트를 사용하여 업데이트하는 방식을 사용하게 됐습니다. 만약 새로운 데이터가 필요하다면 서버 API를 호출하여 필요한 데이터만 새로 불러와 애플리케이션에서 사용할 수 있게 됐죠.

이렇게 html은 한번만 받아와서 웹 애플리케이션을 실행시킨 후에 그 이후에는 필요한 데이터만 받아와서 화면에 업데이트 해주는 것이 싱글 페이지 애플리케이션입니다.

싱글 페이지 애플리케이션은 기술적으로는 한 페이지만 존재하는 것이지만, 사용자가 경험하기에는 여러 페이지가 존재하는 것 처럼 느낄 수 있습니다. 리액트 라우터와 같은 라우팅 시스템은 사용자의 브라우저 주소창의 경로에 따라 알맞는 페이지를 보여주는데요, 이후 링크를 눌러서 다른 페이지로 이동하게 될 때 서버에 다른 페이지의 html을 새로 요청하는 것이 아니라, 브라우저의 History API를 사용하여 브라우저의 주소창의 값만 변경하고 기존에 페이지에 띄웠던 웹 애플리케이션을 그대로 유지하면서 라우팅 설정에 따라 또 다른 페이지를 보여주게 됩니다.

3. 리액트 라우터 적용 및 기본 사용법

이제 라우팅과 싱글 페이지 애플리케이션이 무엇인지 배웠으니, 본격적으로 리액트 라우터를 사용해 봅시다.

이번 실습은 다음 흐름대로 진행됩니다.

3.1. 프로젝트 생성 및 라이브러리 설치

우선 리액트 라우터를 적용해 볼 리액트 프로젝트를 새로 생성해 주세요.

$ yarn create react-app router-tutorial

그리고 해당 프로젝트 디렉터리로 이동하여 리액트 라우터 라이브러리를 설치하세요. 리액트 라우터를 설치할 때는 yarn 을 사용하여 react-router-dom이라는 라이브러리를 설치하면 됩니다.

$ cd router-tutorial
$ yarn add react-router-dom

3.2. 프로젝트에 라우터 적용

프로젝트에 리액트 라우터를 적용할 때는 src/index.js 파일에서 react-router-dom에 내장되어 있는 BrowserRouter라는 컴포넌트를 사용하여 감싸면 됩니다. 이 컴포넌트는 웹 애플리케이션에 HTML5의 History API를 사용하여 페이지를 새로 불러오지 않고도 주소를 변경하고 현재 주소의 경로에 관련된 정보를 리액트 컴포넌트에서 사용할 수 있도록 해 줍니다.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

3.3. 페이지 컴포넌트 만들기

이제 리액트 라우터를 통해 여러 페이지로 구성된 웹 애플리케이션을 만들기 위하여 각 페이지에서 사용할 컴포넌트를 만들 차례입니다. 사용자가 웹 사이트에 들어오게 됐을 때 가장 먼저 보여지게 될 Home 페이지 컴포넌트와 웹 사이트를 소개하는 About 페이지 컴포넌트를 만들어봅시다.

src 디렉터리에 pages 경로를 만들고, 그 안에 다음 파일들을 생성하세요.

src/pages/Home.js

const Home = () => {
  return (
    <div>
      <h1></h1>
      <p>가장 먼저 보여지는 페이지입니다.</p>
    </div>
  );
};

export default Home;

src/pages/About.js

const About = () => {
  return (
    <div>
      <h1>소개</h1>
      <p>리액트 라우터를 사용해 보는 프로젝트입니다.</p>
    </div>
  );
};

export default About;

이제 페이지로 사용할 컴포넌트들이 준비되었습니다. 이 컴포넌트들을 꼭 pages 경로에 넣을 필요는 없습니다. 이는 단순히 페이지를 위한 컴포넌트들을 다른 파일들과 구분하기 위함이며, routes 라는 이름을 써도 되고 그냥 src 경로에 바로 생성해도 문제가 되지 않습니다.

3.4. Route 컴포넌트로 특정 경로에 원하는 컴포넌트 보여주기

사용자의 브라우저 주소 경로에 따라 우리가 원하는 컴포넌트를 보여주기 위해서 Route 라는 컴포넌트를 통해 라우트 설정을 해주어야 합니다.

Route  컴포넌트는 다음과 같이 사용합니다.

<Route path="주소규칙" element={보여 줄 컴포넌트 JSX} />

그리고, Route 컴포넌트는 Routes 컴포넌트 내부에서 사용되어야 합니다.

App 컴포넌트를 다음과 같이 Route 컴포넌트를 사용하여 라우트 설정을 해보세요.

src/App.js

import { Route, Routes } from 'react-router-dom';
import About from './pages/About';
import Home from './pages/Home';

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

export default App;

이제 yarn start 를 입력하여 개발 서버를 시작해 보세요. 첫 화면에 다음과 같이 Home 컴포넌트가 나타났나요?

이번에는 Link 컴포넌트를 사용하여 다른 페이지로 이동하는 링크를 보여주는 방법을 알아보겠습니다. 웹 페이지에서는 원래 링크를 보여줄 때 a 태그를 사용하는데요, 리액트 라우터를 사용하는 프로젝트에서 a 태그를 바로 사용하면 안됩니다. 왜냐하면, a 태그를 클릭하여 페이지를 이동할 때 브라우저에서는 페이지를 새로 불러오게 되기 때문입니다.

Link 컴포넌트 역시 a 태그를 사용하긴 하지만, 페이지를 새로 불러오는 것을 막고 History API를 통해 브라우저 주소의 경로만 바꾸는 기능이 내장되어 있습니다.

Link  컴포넌트는 다음과 같이 사용합니다.

<Link to="경로">링크 이름</Link>

Home 페이지에서 About 페이지로 이동할 수 있도록 Link 컴포넌트를 Home 페이지 컴포넌트에서 사용해봅시다.

src/pages/Home.js

import { Link } from 'react-router-dom';

const Home = () => {
  return (
    <div>
      <h1></h1>
      <p>가장 먼저 보여지는 페이지입니다.</p>
      <Link to="/about">소개</Link>
    </div>
  );
};

export default Home;

이제, 브라우저에서 우리가 방금 만든 소개 링크를 눌러보세요. About 페이지가 보여졌나요?

4. URL 파라미터와 쿼리스트링

페이지 주소를 정의할 때 가끔은 유동적인 값을 사용해야 할 때도 있습니다. 다음과 같이 말이죠.

  • URL 파라미터 예시: /profile/velopert
  • 쿼리스트링 예시: /articles?**page=1&keyword=react

URL 파라미터는 주소의 경로에 유동적인 값을 넣는 형태고, 쿼리 스트링은 주소의 뒷부분에 ? 문자열 이후에 key=value 로 값을 정의하며 & 로 구분을 하는 형태입니다.

주로 URL 파라미터는 ID 또는 이름을 사용하여 특정 데이터를 조회할 때 사용하고, 쿼리스트링(Querystring)은 키워드 검색, 페이지네이션, 정렬 방식 등 데이터 조회에 필요한 옵션을 전달할 때 사용합니다.

4.1. URL 파라미터

우선 URL 파라미터를 사용하는 방법을 알아봅시다. 이를 사용하기 위해 새로운 페이지 컴포넌트를 만들겠습니다. Profile 컴포넌트를 pages 경로에 다음과 같이 작성하세요.

src/pages/Profile.js

import { useParams } from 'react-router-dom';

const data = {
  velopert: {
    name: '김민준',
    description: '리액트를 좋아하는 개발자',
  },
  gildong: {
    name: '홍길동',
    description: '고전 소설 홍길동전의 주인공',
  },
};

const Profile = () => {
  const params = useParams();
  const profile = data[params.username];

  return (
    <div>
      <h1>사용자 프로필</h1>
      {profile ? (
        <div>
          <h2>{profile.name}</h2>
          <p>{profile.description}</p>
        </div>
      ) : (
        <p>존재하지 않는 프로필입니다.</p>
      )}
    </div>
  );
};

export default Profile;

URL 파라미터는 useParams 라는 Hook을 사용하여 객체 형태로 조회할 수 있습니다. URL 파라미터의 이름은 라우트 설정을 할 때 Route 컴포넌트의 path  props를 통하여 설정합니다.

위 코드에서는 data 객체에 예시 프로필 정보들을 key-value 형태로 담아두었습니다. 그리고, Profile  컴포넌트에서는 username URL 파라미터를 통하여 프로필을 조회한 뒤에 프로필이 존재하지 않으면 ‘존재하지 않는 프로필입니다.’ 라는 문구를 보여주고 존재한다면 프로필 정보를 보여주도록 로직을 작성했습니다.

컴포넌트를 다 작성하셨으면 App 컴포넌트 파일을 열어서 새로운 라우트를 다음과 같이 설정해주세요.

src/App.js

import { Route, Routes } from 'react-router-dom';
import About from './pages/About';
import Home from './pages/Home';
import Profile from './pages/Profile';

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/profiles/:username" element={<Profile />} />
    </Routes>
  );
};

export default App;

URL 파라미터는 /profiles/:username 과 같이 경로에 : 를 사용하여 설정합니다. 만약 URL 파라미터가 여러개인 경우엔 /profiles/:username/:field 와 같은 형태로 설정할 수 있습니다.

이제, Profile 페이지로 이동을 할 수 있도록 Home 페이지에 Link 를 더 만들어보세요. 링크가 여러 개 이기 때문에, ul 태그를 사용하여 리스트 형태로 보여주겠습니다.

src/pages/Home.js

import { Link } from 'react-router-dom';

const Home = () => {
  return (
    <div>
      <h1></h1>
      <p>가장 먼저 보여지는 페이지입니다.</p>
      <ul>
        <li>
          <Link to="/about">소개</Link>
        </li>
        <li>
          <Link to="/profiles/velopert">velopert의 프로필</Link>
        </li>
        <li>
          <Link to="/profiles/gildong">gildong의 프로필</Link>
        </li>
        <li>
          <Link to="/profiles/void">존재하지 않는 프로필</Link>
        </li>
      </ul>
    </div>
  );
};

export default Home;

링크가 리스트 형태로 잘 보여졌나요? 이제 새로 만든 링크를 눌러서 Profile 페이지로 이동해보세요.

URL 파라미터에 따라 다른 결과물이 잘 보여지고 있나요?

4.2. 쿼리스트링

이번에는 라우트에서 쿼리스트링을 사용하는 방법을 알아봅시다. 쿼리스트링을 사용할 때는 URL 파라미터와 달리 Route 컴포넌트를 사용할 때 별도로 설정해야되는 것은 없습니다.

우선 쿼리스트링을 화면에 띄워보는 작업부터 해볼까요? About 페이지 컴포넌트를 다음과 같이 수정해보세요.

src/pages/About.js

import { useLocation } from 'react-router-dom';

const About = () => {
  const location = useLocation();

  return (
    <div>
      <h1>소개</h1>
      <p>리액트 라우터를 사용해 보는 프로젝트입니다.</p>
      <p>쿼리스트링: {location.search}</p>
    </div>
  );
};

export default About;

위 컴포넌트에서 우리는 useLocation 이라는 Hook을 사용했는데요, 이 Hook은 location 객체를 반환하며 이 객체는 현재 사용자가 보고있는 페이지의 정보를 지니고 있습니다. 이 객체에는 다음과 같은 값들이 있습니다.

  • pathname: 현재 주소의 경로 (쿼리스트링 제외)
  • search: 맨 앞의 ? 문자 포함한 쿼리스트링 값
  • hash: 주소의 # 문자열 뒤의 값 (주로 History API 가 지원되지 않는 구형 브라우저에서 클라이언트 라우팅을 사용할 때 쓰는 해시 라우터에서 사용합니다.)
  • state: 페이지로 이동할때 임의로 넣을 수 있는 상태 값
  • key: location 객체의 고유 값, 초기에는 default 이며 페이지가 변경될때마다 고유의 값이 생성됨

쿼리스트링은 location.search 값을 통해 조회를 할 수 있습니다. 주소창에 http://localhost:3000/about?detail=true&mode=1 라고 직접 입력해서 어떤 값이 나타나는지 확인해보세요.

쿼리스트링 값이 현재 ?detail=true&mode=1 으로 표시가 되고 있습니다. 이 문자열에서 앞에 있는 ? 로 지우고, & 문자열로 분리한뒤 key 와 value 를 파싱하는 작업을 해야 하는데요, 이 작업은 보통 npm 에서 qs 또는 querystring 패키지를 설치해서 처리할 수 있습니다.

쿼리스트링을 따로 파싱까지 해야된다면 번거로울수도 있는데, 다행히도 리액트 라우터에서는 v6부터 useSearchParams 라는 Hook을 통해서 쿼리스트링을 더욱 쉽게 다룰 수 있게 됐습니다.

다음은 이 Hook을 사용하여 쿼리스트링을 쉽게 파싱하여 사용하는 예시입니다.

src/pages/About.js

import { useSearchParams } from 'react-router-dom';

const About = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  const detail = searchParams.get('detail');
  const mode = searchParams.get('mode');

  const onToggleDetail = () => {
    setSearchParams({ mode, detail: detail === 'true' ? false : true });
  };

  const onIncreaseMode = () => {
    const nextMode = mode === null ? 1 : parseInt(mode) + 1;
    setSearchParams({ mode: nextMode, detail });
  };

  return (
    <div>
      <h1>소개</h1>
      <p>리액트 라우터를 사용해 보는 프로젝트입니다.</p>
      <p>detail: {detail}</p>
      <p>mode: {mode}</p>
      <button onClick={onToggleDetail}>Toggle detail</button>
      <button onClick={onIncreaseMode}>mode + 1</button>
    </div>
  );
};

export default About;

useSearchParams 는 배열 타입의 값을 반환하며, 첫번째 원소는 쿼리파라미터를 조회하거나 수정하는 메서드들이 담긴 객체를 반환합니다. get 메서드를 통해 특정 쿼리파라미터를 조회할 수 있고, set 메서드를 통해 특정 쿼리파라미터를 업데이트 할 수 있습니다. 만약 조회시에 쿼리파라미터가 존재하지 않는다면 null 로 조회됩니다. 두번째 원소는 쿼리파라미터를 객체형태로 업데이트할 수 있는 함수를 반환합니다.

쿼리파라미터를 사용하실 때 주의하실점은 쿼리파라미터를 조회할 때 값은 무조건 문자열 타입이라는 것 입니다. 즉, true 또는 false 값을 넣게 된다면 값을 비교할 때 꼭 'true' 와 같이 따옴표로 감싸서 비교를 하셔야 하고, 숫자를 다루게 된다면 parseInt 를 사용하여 숫자 타입으로 변환을 해야 합니다.

5. 중첩된 라우트

이번에는 리액트 라우터에서 중첩된 라우트를 다룰 때 어떻게 해야 하는지에 대해서 배워보겠습니다. 우선, 중첩된 라우트를 이해해보기 위하여, 게시글 목록을 보여주는 페이지와 게시글을 읽는 페이지를 만들어보겠습니다.

pages 디렉터리에 다음 페이지 컴포넌트를 만들어보세요.

src/pages/Articles.js

import { Link } from 'react-router-dom';

const Articles = () => {
  return (
    <ul>
      <li>
        <Link to="/articles/1">게시글 1</Link>
      </li>
      <li>
        <Link to="/articles/2">게시글 2</Link>
      </li>
      <li>
        <Link to="/articles/3">게시글 3</Link>
      </li>
    </ul>
  );
};

export default Articles;

src/pages/Article.js

import { useParams } from 'react-router-dom';

const Article = () => {
  const { id } = useParams();
  return (
    <div>
      <h2>게시글 {id}</h2>
    </div>
  );
};

export default Article;

이렇게 두 컴포넌트를 다 만드셨다면, 해당 페이지들의 라우트를 App 컴포넌트에서 설정해보세요.

src/App.js

import { Route, Routes } from 'react-router-dom';
import About from './pages/About';
import Article from './pages/Article';
import Articles from './pages/Articles';
import Home from './pages/Home';
import Profile from './pages/Profile';

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/profiles/:username" element={<Profile />} />
      <Route path="/articles" element={<Articles />} />
      <Route path="/articles/:id" element={<Article />} />
    </Routes>
  );
};

export default App;

그 다음에는 Home 컴포넌트에서 게시글 목록 페이지로 가는 링크를 추가하세요.

src/pages/Home.js

import { Link } from 'react-router-dom';

const Home = () => {
  return (
    <div>
      <h1></h1>
      <p>가장 먼저 보여지는 페이지입니다.</p>
      <ul>
        <li>
          <Link to="/about">소개</Link>
        </li>
        <li>
          <Link to="/profiles/velopert">velopert의 프로필</Link>
        </li>
        <li>
          <Link to="/profiles/gildong">gildong의 프로필</Link>
        </li>
        <li>
          <Link to="/profiles/void">존재하지 않는 프로필</Link>
        </li>
        <li>
          <Link to="/articles">게시글 목록</Link>
        </li>
      </ul>
    </div>
  );
};

export default Home;

이제 게시글 목록 페이지를 열어서 목록이 잘 나타나는지 확인해보고 게시글의 링크를 눌러 게시글 읽기 페이지도 잘 나타나는지 확인을 해보세요.

게시글 목록 페이지에서 게시글을 열었을 때, 게시글의 하단에 목록을 보여줘야한다면 어떨까요?

만약 기존 방식으로 구현을 한다면 아마 다음과 같이 ArticleList 컴포넌트를 따로 만들어서 각 페이지 컴포넌트에서 사용을 해야됐을 것입니다.

<div>
  <h2>게시글 {id}</h2>
  <ArticleList />
</div>

만약 중첩된 라우트를 사용한다면 좀 더 나은 방식으로 구현을 할 수 있답니다. 이번에는 중첩된 라우트 형태로 라우트를 설정해보겠습니다.

App 컴포넌트를 다음과 같이 수정해보세요.

src/App.js

import { Route, Routes } from 'react-router-dom';
import About from './pages/About';
import Article from './pages/Article';
import Articles from './pages/Articles';
import Home from './pages/Home';
import Profile from './pages/Profile';

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/profiles/:username" element={<Profile />} />
      <Route path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />} />
      </Route>
    </Routes>
  );
};

export default App;

그 다음에는 Articles 컴포넌트에서 리액트 라우터에서 제공하는 Outlet 이라는 컴포넌트를 사용해주어야 합니다. 이 컴포넌트는 Routechildren 으로 들어가는 JSX 엘리먼트를 보여주는 역할을 합니다. 지금의 경우엔 다음 내용이 Outlet 컴포넌트를 통해서 보여지겠지요.

<Route path=":id" element={<Article />} />

Articles 컴포넌트를 다음과 같이 수정해주세요.

src/pages/Articles.js

import { Link, Outlet } from 'react-router-dom';

const Articles = () => {
  return (
    <div>
      <Outlet />
      <ul>
        <li>
          <Link to="/articles/1">게시글 1</Link>
        </li>
        <li>
          <Link to="/articles/2">게시글 2</Link>
        </li>
        <li>
          <Link to="/articles/3">게시글 3</Link>
        </li>
      </ul>
    </div>
  );
};

export default Articles;

위 코드에서 Outlet 컴포넌트가 사용된 자리에 중첩된 라우트가 보여지게됩니다.

이제 /articles/1 경로에 들어가볼까요?

게시글 하단에 게시글 목록이 잘 나타나는지 확인하세요.

5.1. 공통 레이아웃 컴포넌트

중첩된 라우트와 Outlet 은 페이지끼리 공통적으로 보여줘야 하는 레이아웃이 있을때도 유용하게 사용할 수 있습니다.

예를 들어서, Home, About, Profile 페이지에서 상단에 헤더를 보여줘야 하는 상황을 가정해봅시다. 첫 번째로 드는 생각은 아마 Header 컴포넌트를 따로 만들어두고 각 페이지 컴포넌트에서 재사용을 하는 방법일 것입니다. 물론 이 방법이 틀린것은 아니지만, 방금 배운 중첩된 라우트와 Outlet을 활용하여 구현을 할 수도 있습니다. 중첩된 라우트를 사용하는 방식을 사용하면 컴포넌트를 한번만 사용해도 된다는 장점이 있죠. 상황에 따라 그리고 여러분의 취향에 따라 구현을 하시면 됩니다.

이번에는 중첩된 라우트를 통해 공통 레이아웃 컴포넌트를 사용해봅시다.

우선, 공통 레이아웃을 위한 Layout 컴포넌트를 src 디렉터리에 만드세요.

src/Layout.js

import { Outlet } from 'react-router-dom';

const Layout = () => {
  return (
    <div>
      <header style={{ background: 'lightgray', padding: 16, fontSize: 24 }}>
        Header
      </header>
      <main>
        <Outlet />
      </main>
    </div>
  );
};

export default Layout;

각 페이지 컴포넌트가 보여져야 하는 부분에 Outlet 컴포넌트를 사용해주었습니다. 컴포넌트를 다 작성하셨으면 App 컴포넌트를 다음과 같이 수정하세요.

src/App.js

import { Route, Routes } from 'react-router-dom';
import Layout from './Layout';
import About from './pages/About';
import Article from './pages/Article';
import Articles from './pages/Articles';
import Home from './pages/Home';
import Profile from './pages/Profile';

const App = () => {
  return (
    <Routes>
      <Route element={<Layout />}>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/profiles/:username" element={<Profile />} />
      </Route>
      <Route path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />} />
      </Route>
    </Routes>
  );
};

export default App;

이제 Home 페이지에 들어가세요.

상단에 위 스크린샷과 같이 헤더가 잘 나타났나요?

5.2. index props

Route 컴포넌트에는 index 라는 props가 있습니다. 이 props 는 path="/"와 동일한 의미를 가집니다.

Home 컴포넌트가 사용된 Route 컴포넌트를 다음과 같이 변경해보세요.

src/App.js

import { Route, Routes } from 'react-router-dom';
import Layout from './Layout';
import About from './pages/About';
import Article from './pages/Article';
import Articles from './pages/Articles';
import Home from './pages/Home';
import Profile from './pages/Profile';

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/profiles/:username" element={<Profile />} />
      </Route>
      <Route path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />} />
      </Route>
    </Routes>
  );
};

export default App;

그리고, / 경로로 들어갔을 때 여전히 Home 페이지가 여전히 잘 나오고 있나요? index prop은 상위 라우트의 경로와 일치하지만, 그 이후에 경로가 주어지지 않았을때 보여지는 라우트를 설정할때 사용합니다. path="/"와 동일한 역할을 하며 이를 좀 더 명시적으로 표현하는 방법입니다.

6. 리액트 라우터 부가기능

리액트 라우터에는 웹 애플리케이션에서 라우팅에 관련된 작업을 할 때 사용할 수 있는 유용한 API들을 제공합니다. 자주 사용되는 것들을 알아봅시다.

6.1. useNavigate

useNavigateLink 컴포넌트를 사용하지 않고 다른 페이지로 이동을 해야 하는 상황에 사용하는 Hook 입니다.

Layout 컴포넌트를 다음과 같이 수정해보세요.

src/Layout.js

import { Outlet, useNavigate } from 'react-router-dom';

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

  const goBack = () => {
    // 이전 페이지로 이동
    navigate(-1);
  };

  const goArticles = () => {
    // articles 경로로 이동
    navigate('/articles');
  };

  return (
    <div>
      <header style={{ background: 'lightgray', padding: 16, fontSize: 24 }}>
        <button onClick={goBack}>뒤로가기</button>
        <button onClick={goArticles}>게시글 목록</button>
      </header>
      <main>
        <Outlet />
      </main>
    </div>
  );
};

export default Layout;

다음 스크린샷처럼 헤더에 버튼 두개가 나타났나요?

버튼을 눌렀을때 정상적으로 이전 페이지로 이동하거나, 게시글 목록 페이지로 이동이 되는지 확인하세요.

navigate 함수를 사용할 때 파라미터가 숫자 타입이라면 앞으로 가거나, 뒤로 갑니다. 예를 들어서 navigate(-1) 을 하면 한 번 뒤로가고 navigate(-2) 를 하면 두 번 뒤로갑니다. 반대로 navigate(1) 을 하면 앞으로 한 번 갑니다. 물론, 뒤로가기를 한번 한 상태여야 합니다.

다른 페이지로 이동을 할 때 replace 라는 옵션이 있는데요, 이 옵션을 사용하면 페이지를 이동할 때 현재 페이지를 페이지 기록에 남기지 않습니다.

방금 작성했던 goArticles 함수를 다음과 같이 수정해보세요.

src/Layout.js - goArticles

const goArticles = () => {
  navigate('/articles', { replace: true });
}

그 다음에 / 경로로 들어가서 Home 페이지를 띄운 뒤에, 소개 링크를 눌러 About 페이지로 이동하세요. 그리고, 상단의 게시글 목록 페이지를 눌러보세요.

그 상태에서 브라우저의 뒤로가기 버튼을 눌러 이전 페이지로 이동을 해보세요. 만약 { replace: true } 설정이 없었더라면 직전에 봤던 페이지인 About 페이지가 나타나야 하지만, 이 옵션이 활성화되어있기 때문에, 그 전의 페이지인 Home 페이지가 나타나게 됩니다.

NavLink 컴포넌트는 링크에서 사용하는 경로가 현재 라우트의 경로와 일치하는 경우 특정 스타일 또는 CSS 클래스를 적용하는 컴포넌트입니다.

이 컴포넌트를 사용할 때 style 또는 className을 설정할 때 { isActive: boolean } 을 파라미터로 전달받는 함수 타입의 값을 전달합니다. 예시를 확인해볼까요?

<NavLink 
  style={({isActive}) => isActive ? activeStyle : undefined} 
/>
<NavLink 
  className={({isActive}) => isActive ? 'active' : undefined} 
/>

Articles 페이지 컴포넌트에서 이 컴포넌트를 사용해봅시다.

src/pages/Articles.js

import { NavLink, Outlet } from 'react-router-dom';

const Articles = () => {
  const activeStyle = {
    color: 'green',
    fontSize: 21,
  };

  return (
    <div>
      <Outlet />
      <ul>
        <li>
          <NavLink
            to="/articles/1"
            style={({ isActive }) => (isActive ? activeStyle : undefined)}
          >
            게시글 1
          </NavLink>
        </li>
        <li>
          <NavLink
            to="/articles/2"
            style={({ isActive }) => (isActive ? activeStyle : undefined)}
          >
            게시글 2
          </NavLink>
        </li>
        <li>
          <NavLink
            to="/articles/3"
            style={({ isActive }) => (isActive ? activeStyle : undefined)}
          >
            게시글 3
          </NavLink>
        </li>
      </ul>
    </div>
  );
};

export default Articles;

위 스크린샷과 같이 현재 보고 있는 게시글의 링크의 텍스트가 더 커지고 초록색으로 잘 변했나요?

현재 반복되는 코드가 여러번 사용되고 있지요? 여러분이 나중에 실제로 비슷한 작업을 하게 된다면 현재 NavLink 를 감싼 또 다른 컴포넌트를 만들어서 다음과 같이 리팩토링하여 사용하시는 것을 권장드립니다.

src/pages/Articles.js

import { NavLink, Outlet } from 'react-router-dom';

const Articles = () => {
  return (
    <div>
      <Outlet />
      <ul>
        <ArticleItem id={1} />
        <ArticleItem id={2} />
        <ArticleItem id={3} />
      </ul>
    </div>
  );
};

const ArticleItem = ({ id }) => {
  const activeStyle = {
    color: 'green',
    fontSize: 21,
  };
  return (
    <li>
      <NavLink
        to={`/articles/${id}`}
        style={({ isActive }) => (isActive ? activeStyle : undefined)}
      >
        게시글 {id}
      </NavLink>
    </li>
  );
};

export default Articles;

6.3. NotFound 페이지 만들기

이번에는 NotFound 페이지를 만드는 방법을 배워봅시다. 이 페이지는 사전에 정의되지 않는 경로에 사용자가 진입했을 때 보여주는 페이지입니다. 즉, 페이지를 찾을 수 없을 때 나타나는 페이지입니다.

우선, 이 컴포넌트를 pages 디렉터리에 만들어주세요.

src/pages/NotFound.js

const NotFound = () => {
  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        fontSize: 64,
        position: 'absolute',
        width: '100%',
        height: '100%',
      }}
    >
      404
    </div>
  );
};

export default NotFound;

그 다음에 App 컴포넌트를 다음과 같이 수정해보세요.

src/App.js

import { Route, Routes } from 'react-router-dom';
import Layout from './Layout';
import About from './pages/About';
import Article from './pages/Article';
import Articles from './pages/Articles';
import Home from './pages/Home';
import NotFound from './pages/NotFound';
import Profile from './pages/Profile';

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/profiles/:username" element={<Profile />} />
      </Route>
      <Route path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />} />
      </Route>
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
};

export default App;

여기서 * 는 wildcard 문자인데요, 이는 아무 텍스트나 매칭한다는 뜻 입니다. 이 라우트 엘리먼트의 상단에 위치하는 라우트들의 규칙을 모두 확인하고, 일치하는 라우트가 없다면 이 라우트가 화면에 나타나게 됩니다.

6.4. Navigate 컴포넌트

Navigate 컴포넌트는 컴포넌트를 화면에 보여주는 순간 다른 페이지로 이동을 하고 싶을 때 사용하는 컴포넌트입니다. 즉, 페이지를 리다이렉트 하고 싶을 때 사용합니다. 예를 들어서, 사용자의 로그인이 필요한 페이지인데 로그인을 안했다면 로그인 페이지를 보여주어야겠죠? 그러한 상황에 사용 할 수 있습니다.

먼저, 두 페이지 컴포넌트를 pages 디렉터리에 만드세요.

src/pages/Login.js

const Login = () => {
  return <div>로그인 페이지</div>;
};

export default Login;

src/pages/MyPage.js

import { Navigate } from 'react-router-dom';

const MyPage = () => {
  const isLoggedIn = false;

  if (!isLoggedIn) {
    return <Navigate to="/login" replace={true} />;
  }

  return <div>마이 페이지</div>;
};

export default MyPage;

여기서 isLoggedIn은 현재 false라는 고정값을 가지고 있지만, 이 값이 로그인 상태에 따라 true 또는 false를 가르킨다고 가정을 해봅시다.

위 컴포넌트에서는 만약 이 값이 false 라면 Navigate 컴포넌트를 통해 /login 경로로 이동합니다. 여기서 replace props는 useNavigate 에서 설명한 것과 동일합니다. 페이지를 이동할 때 현재 페이지를 기록에 남기지 않기 때문에 이동 후 뒤로가기를 눌렀을 때 2 페이지 전의 페이지로 이동합니다.

컴포넌트를 다 작성하셨으면 App 컴포넌트를 다음과 같이 수정해주세요.

src/App.js

import { Route, Routes } from 'react-router-dom';
import Layout from './Layout';
import About from './pages/About';
import Article from './pages/Article';
import Articles from './pages/Articles';
import Home from './pages/Home';
import Login from './pages/Login';
import MyPage from './pages/MyPage';
import NotFound from './pages/NotFound';
import Profile from './pages/Profile';

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/profiles/:username" element={<Profile />} />
      </Route>
      <Route path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />} />
      </Route>
      <Route path="/login" element={<Login />} />
      <Route path="/mypage" element={<MyPage />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
};

export default App;

그 다음에는 브라우저에서 /mypage 경로로 이동해보세요. 페이지가 로딩되는 순간 바로 Login 페이지로 이동이 되나요?

7. 정리

이 튜토리얼에서는 리액트 라우터를 사용하여 주소 경로에 따라 다양한 페이지를 보여 주는 방법을 알아보았습니다. 큰 규모의 프로젝트를 진행하다 보면 한 가지 문제가 발생합니다. 바로 웹 브라우저에서 사용할 컴포넌트, 상태 관리를 하는 로직, 그 외 여러 기능을 구현하는 함수들이 점점 쌓이면서 최종 결과물인 자바스크립트 파일의 크기가 매우 커진다는 점 입니다.

예를 들어 방금 만든 프로젝트는 About 페이지에 사용자가 들어왔을 때 지금 당장 필요하지 않은 Profile, Articles 등의 컴포넌트의 코드까지 함께 불러옵니다. 라우트에 따라 필요한 컴포넌트만 불러오고, 다른 컴포넌트는 다른 페이지로 이동하게되어 필요한 시점에 불러오면 더 효율적이지 않을까요? 이 문제는 코드 스플리팅이라는 기술로 해결을 할 수 있습니다. 코드 스플리팅에 대해서는 여기에서 확인할 수 있습니다.

이 포스트에서 다루지 못한 내용들이 존재합니다. 리액트 라우터에 대해서 더 알고 싶으시다면 꼭 공식문서를 참조하세요.

profile
Frontend Engineer@Laftel Inc. 개발을 재미있게 이것 저것 하는 개발자입니다.

7개의 댓글

comment-user-thumbnail
2021년 12월 28일

감사합니당~~

답글 달기
comment-user-thumbnail
2021년 12월 29일

👍👍

답글 달기
comment-user-thumbnail
2022년 1월 17일

감사합니다!!

답글 달기
comment-user-thumbnail
2022년 2월 21일

책으로 공부하고있었는데 버전호환이 안되는것 같아 v5로 다운그레이드 해서 학습중 이였는데

라우터에 대해 더 찾아보다가 개발자님 블로그를 발견했는데 v6 튜토리얼까지 있네요

책 잘 보고있습니다. 친절한 설명 감사합니다

답글 달기
comment-user-thumbnail
2022년 2월 28일

7쇄 읽으면서 라우터에서 막혀가지구 막막했는데.. 8쇄부터 변경된 거군요ㅠㅠ 다행히 초입에 알아서 블로그 보면서 학습해야겠습니당 감사합니다! 서점에 항의할뻔..ㅎㅎ

답글 달기
comment-user-thumbnail
2022년 3월 14일

좋은글 감사합니다.
덕분에 리액트 라우터에 대해 깊이 배웟습니다.

답글 달기
comment-user-thumbnail
2022년 3월 14일

아직은 쓰지 않는게 좋은 시기일듯합니다,,
많은 리액트 라이브러리들이 router v6 버전 문법을 인식 못하는 이슈가 많더라구요

답글 달기