티스토리 뷰

WEB/React

[React] Routing: React Router

춘햄 2023. 3. 30. 12:46

 웹 App에서 Routing이라는 개념은 사용자가 요청한 URL에 따라 알맞는 페이지를 보여주는 것을 의미한다. Todo List의 경우에는 하나의 페이지만 사용해도 충분히 구현이 가능했지만, 블로그와 같은 경우에는 여러 페이지로 구성해야 제 기능을 모두 구현할 수 있을 것이다.

 

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

 

일반적으로 리액트에서 라우트 시스템을 구축하기 위해 사용할 수 있는 선택지는 두 가지이다. 

 

 1. 리액트 라우터(React Router): 이 라이브러리는 리액트의 라우팅 관련 라이브러리들 중에서 가장 오래됐고, 가장 많이 사용되고 있는 라이브러리이다. 컴포넌트 기반으로 라우팅 시스템을 설정할 수 있다.

 

 2. Next.js: 리액트 프로젝트의 프레임워크이다. 앞에서 사용한 Create React App 처럼 리액트 프로젝트를 설정하는 기능, 라우팅 시스템, 최적화, 다국어 시스템 지원, 서버 사이드 렌더링 등 다양한 기능을 제공한다. 리액트 라우터의 대안으로 많이 사용된다.

 

라우팅 관련 기능은 리액트 라이브러리에서 공식적으로 지원하는 것이 아니라 서드 파티로 제공되기 때문에, 이 외에도 다양한 라이브러리들이 존재한다.

 

이 포스팅에서는 라우팅 시스템의 개념을 익히는 것이 주된 목적이기 때문에 라우팅 기능에만 집중하고 있는 라이브러리인 리액트 라우터를 사용하여 예제를 들어가 보려고 한다.

 

리액트 라우터를 사용하면 손쉽게 싱글 페이지 어플리케이션을 제작할 수 있다.

 

바로 한번 들어가보자.


우선, npm에서 패키지를 설치해야 한다.

npm install react-router-dom

 

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

 

◎index.js

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

페이지 컴포넌트 만들기

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

 

◎Home.js

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

export default Home;

 

◎About.js

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

export default About;

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

사용자의 브라우저 주소 경로에 따라 원하는 컴포넌트를 보여주려면 Route라는 컴포넌트를 통해 라우트 설정을 해줘야 한다.

 

Route 컴포넌트는 아래와 같이 사용한다.

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

Route 컴포넌트는 반드시 Routes 컴포넌트 내부에서 사용되어야 한다.

 

◎App.js

import logo from './logo.svg';
import './App.css';
import { Route, Routes } from 'react-router-dom';
import About from "./pages/About";
import Home from "./pages/Home";

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

export default App;

App.js 에서 Routes, Route 컴포넌트를 사용하여 각 URL 경로를 잡아주고, 보여줄 컴포넌트를 넣어주면 아래와 같이 기본 페이지로 Home 컴포넌트를 보여준다.


Link 컴포넌트를 사용하여 다른 페이지로 이동하는 링크 보여주기

웹 페이지에서 원래 링크를 보여줄 때 a 태그를 사용하는데, 리액트 라우터를 사용하는 프로젝트에서는 a 태그를 사용해서는 안된다. 왜냐하면 a 태그를 클릭하여 페이지를 이동할 때 브라우저에서는 페이지를 새로 불러오기 때문이다.

 

아래와 같이 Link 컴포넌트를 사용하면, 페이지를 새로 불러오는 것을 막고 브라우저의 경로만 바꾸는 기능을 구현할 수 있다.

 

◎Home.js

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

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

export default Home;

URL 파라미터와 쿼리스트링

페이지 주소를 정의할 때 가끔은 유동적인 파라미터를 사용해야할 때가 있다. 혹은 쿼리스트링으로 어떤 값을 전달해줘야 하는 상황도 굉장히 자주 생긴다.

 

1) URL 파라미터

우선 URL 파리미터를 사용하는 방법을 알아보자. 이를 사용하기 위해 새로운 페이지 컴포넌트를 만든다. 

 

◎Profile.js

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

const data = {
    velopert: {
        name: '춘햄',
        description: '그냥 사람'
    },
    coffee: {
        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을 사용하여 객체 형태로 조회할 수 있다. 

 

◎App.js

import logo from './logo.svg';
import './App.css';
import { Route, Routes } from 'react-router-dom';
import About from "./pages/About";
import Home from "./pages/Home";
import Profile from  "./pages/Profile";

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

export default App;

 

◎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="/profile/choonham">choonham의 프로필</Link>
                </li>
                <li>
                    <Link to="/profile/coffee">coffee 프로필</Link>
                </li>
                <li>
                    <Link to="/profile/void">존재하지 않는 프로필</Link>
                </li>
            </ul>
        </div>
    );
};

export default Home;

 2) 쿼리스트링 

이번에는 라우트에서 쿼리스트링을 다뤄보자.

 

쿼리스트링을 사용할 때는 URL 파라미터와 달리 Route 컴포넌트를 사용할 때 별도로 설정해야 하는 것이 없다.

 

◎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이며 페이지가 변경될 때마다 고유의 값이 생성됨

또한 아래와 같이 useSearchParams라는 Hook을 통해 쿼리스트링을 좀 더 쉽게 파싱할 수 있다.

 

◎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;


중첩된 라우트

중첩된 라우트는 아래와 같은 상황에서 사용할 수 있다. 

 

◎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;

 

◎Article.js

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

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

export default Article;

 

◎App.js

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

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

export default App;

 

◎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="/profile/choonham">choonham의 프로필</Link>
                </li>
                <li>
                    <Link to="/profile/coffee">coffee 프로필</Link>
                </li>
                <li>
                    <Link to="/profile/void">존재하지 않는 프로필</Link>
                </li>
                <li>
                    <Link to ="/articles">게시글 목록</Link>
                </li>
            </ul>
        </div>
    );
};

export default Home;

 

여기에서 게시글을 열었을 때, 게시글 하단에 목록을 보여줘야 한다면? 아마도 ArticleList와 같은 컴포넌트를 따로 만들어서 각 페이지 컴포넌트에 넣고 사용했을 것이다. 

 

그러나 중첩된 라우트를 사용한다면, 좀 더 나은 방식으로 구현할 수 있다. 

 

우선, App.js 에서 articles 라우트가 article 라우트를 자식으로 두게 설정해야 한다.

 

◎App.js

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

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

export default App;

그러고 나서 Articles 컴포넌트에 리액트 라우터에서 제공하는 Outlet이라는 컴포넌트를 사용해줘야 한다. 이 컴포넌트를 사용하면 Route의 children으로 들어가는 JSX 엘리먼트를 보여주는 역할을 한다. 

 

즉, 아래의 내용이 Outlet 컴포넌트로 보여지는 것이다.

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

 

Articles를 아래와 같이 수정하자.

 

◎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 컴포넌트가 사용된 자리에 중첩된 라우트가 보여지게 된다.


공통 레이아웃 컴포넌트

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

 

즉, 모든 페이지에 공통으로 들어갈 헤더를 놓아야 할 때 주로 사용한다.

 

◎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;

 

◎App.js

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

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

    </Routes>
  );
}

export default App;


index props

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

 

즉, 

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

    </Routes>
  );
}

위 코드와 index를 사용한 아래 코드는 

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

    </Routes>
  );
}

구조적으로 같다는 이야기이다.


리액트 라우터 부가 기능

리액트 라우터는 웹 App에서 라우팅과 관련된 작업을 할 때 사용할 수 있는 유용한 API들을 제공한다. 자주 사용되는 것을 알아보자.

 

1. useNavigate

useNavigate는 Link 컴포넌트를 사용하지 않고 다른 페이지로 이동해야 하는 상황에서 자주 사용한다.

이는 아래와 같이 사용한다.

 

◎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;

 

여기서 replace 옵션을 사용하면 페이지를 이동할 때 현재 페이지를 페이지 기록에 남기지 않는다.

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

 

2. NavLink

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

 

이 컴포넌트의 style과 className은 {isActive: boolean} 을 파라미터로 전달받는 함수 타입의 값을 전달한다.

 

아래와 같이 사용한다.

 

◎Articles.js

import { Outlet, NavLink } 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;

결과를 보면, 현재 URL 과 동일한 Link의 컴포넌트 style만 변경되었다.

 

3. NotFount 페이지 만들기

사전에 정의되지 않는 경로로 사용자가 진입했을 때 보여줄 NotFound 페이지 또한 리액트 라우터로 구현이 가능하다.

 

◎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;

위와 같이 NotFound 컴포넌트를 만들고

 

◎App.js

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

export default App;

와일드 카드인 *로 path를 지정해주기만 하면 된다.

 

4. Navigate 컴포넌트

Navigate 컴포넌트는 컴포넌트를 화면에 보여주는 순간 다른 페이지로 리다이렉트하고 싶을 때 사용한다.

예를 들어 사용자의 로그인이 필요한 페이지인데, 로그인을 안했다면 로그인 페이지를 보여주는 식이다.

 

로그인 페이지를 하나 만들어서 확인해보자.

 

◎Login.js

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

export default Login;

 

◎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;

 

◎App.js

function App() {
  return (
    <Routes>
        <Route  element={<Layout/>}>
          <Route index element={<Home />}></Route>
          <Route path="/about" element={<About />}></Route>
          <Route path="/profile/:username" element={<Profile />}></Route>
        </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>
  );
}

 

이렇게 설정해주면, mypage로 이동했을 때 자동으로 로그인 페이지로 리다이렉트 시킨다.


리액트 라우터를 사용하면 주소 경로에 따라 다양한 페이지를 보여줄 수 있지만, 프로젝트의 규모가 커지게 될 경우 자바스크립트 파일의 크기가 매우 커진다는 단점이 있다. 

 

즉, About 페이지에 사용자가 들어왔을 때 지금 당장 필요하지 않는 Profile, Articles 등의 컴포넌트 코드까지 함께 불러온다. 라우트에 따라 필요한 컴포넌트만 불러오고, 다른 컴포넌트는 이동하는 시점에 불러오면 좀 더 효율적으로 리소스를 관리할 수 있다. 

 

이 문제는 코드 스플리팅이라는 기술로 해결할 수 있는데, 이는 나중에 다뤄본다.

 

 

끝!

'WEB > React' 카테고리의 다른 글

[React] Context API  (0) 2023.03.30
[React] 외부 API 활용: News viewer  (0) 2023.03.30
[React] 불변성 유지: immer  (0) 2023.03.29
[React] Todo List: 컴포넌트 성능 최적화  (0) 2023.03.29
[React] First Project: Todo List  (0) 2023.03.28
Comments