[React] React Router

문지은·2023년 7월 29일
0

React

목록 보기
15/24
post-thumbnail

react-router-dom v6을 기준으로 작성되었습니다.

Routing

  • 사용자가 요청한 URL에 따라 알맞는 페이지를 보여주는 것
  • 여러 페이지로 구성된 웹 애플리케이션을 만들 때 페이지별로 컴포넌트들을 분리해가며 프로젝트를 관리하기 위해 필요

React Router

  • 리액트의 라우팅 관련 라이브러리들 중에서 가장 많이 사용되고 있는 라이브러리
  • 리액트 라우터를 사용하면 손쉽게 싱글 페이지 애플리메티션 (Single Page Application)을 만들 수 있음
  • 공식문서

react-router-dom 라이브러리 설치

  • 아래와 같은 명령어를 사용하면 최신 버전(현재 v6)의 리액트 라우터를 설치할 수 있음
# npm 사용
npm install react-router-dom

# yarn 사용
yarn add react-router-dom

프로젝트에 라우터 적용하기

  • 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')
);

페이지 컴포넌트 만들기

  • 이제 리액트 라우터를 통해 여러 페이지로 구성된 웹 애플리케이션을 만들어보자.
  • 사용자가 웹 사이트에 들어오게 됐을 때 가장 먼저 보여지게 될 Home 페이지 컴포넌트와 웹 사이트를 소개하는 About 페이지 컴포넌트를 만들어보자.
  • src 디렉터리에 pages 경로를 만들고 그 안에 파일 생성하기

src/pages/Home.jsx

import React from 'react'

function Home() {
  return (
    <div>
        <h1>Home</h1>
        <p>가장 먼저 보여지는 페이지</p>
    </div>
  )
}

export default Home

src/pages/About.jsx

import React from 'react'

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

export default About

Route 컴포넌트 사용

  • 사용자의 브라우저 주소 경로에 따라 원하는 컴포넌트를 보여주기 위해 Route 컴포넌트를 통해 라우트 설정
<Route path="주소규칙" element={보여줄 컴포넌트 JSX} />
  • Route 컴포넌트는 Routes 컴포넌트 내부에서 사용되어야 함
  • App 컴포넌트를 다음과 같이 Route 컴포넌트를 사용하여 라우트 설정해보자.
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;
  • 실행 결과
  • 다른 페이지로 이동하는 링크를 보여주기 위해 Link 컴포넌트 사용
<Link to="경로">링크 이름</Link>
  • Home 페이지에서 About 페이지로 이동할 수 있도록 Link 컴포넌트를 Home 페이지 컴포넌트에서 사용해보자.

src/pages/Home.js

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

function Home() {
  return (
    <div>
        <h1>Home</h1>
        <p>가장 먼저 보여지는 페이지</p>
        <Link to="/about">About</Link>
    </div>
  )
}

export default Home
  • 실행 결과
  • 링크를 누르면 About 페이지로 이동함

URL 파라미터와 쿼리스트링

  • 페이지 주소를 정의할 때 유동적인 값을 사용해야할 때도 있음
  • URL 파라미터
    • 주소의 경로에 유동적인 값을 넣는 형태
    • 예시: /profile/mjieun
  • 쿼리 스트링
    • 주소의 뒷부분에 ? 문자열 이후에 key=value 로 값을 정의하며 & 로 구분을 하는 형태
    • 예시: /articles?page=1&keyword=react

URL 파라미터

  • URL 파라미터는 useParams 라는 Hook을 사용하여 객체 형태로 조회할 수 있음
  • URL 파라미터의 이름은 라우트 설정을 할 때 Route 컴포넌트의 path props를 통하여 설정
  • URL 파라미터를 사용하기 위해 새로운 페이지 컴포넌트를 만들어보자.
    • data 객체에 예시 프로필 정보들을 key-value 형태로 담기
    • Profile 컴포넌트에서는 username URL 파라미터를 통하여 프로필을 조회한 뒤에 프로필이 존재하지 않으면 ‘존재하지 않는 프로필입니다.’ 라는 문구를 보여주고 존재한다면 프로필 정보를 보여주도록 로직을 작성

src/pages/Profile.js

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

const data = {
  mjieun: {
    name: '문지은',
    description: '리액트를 공부하고 있어요.',
  },
  gildong: {
    name: '홍길동',
    description: '고전 소설 홍길동전의 주인공',
  },
};

function 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
  • App 컴포넌트 파일에 다음과 같이 새로운 라우트를 설정
    • /profiles/:username 과 같이 경로에 : 를 사용하여 설정
    • 만약 URL 파라미터가 여러개인 경우엔 /profiles/:username/:field 와 같은 형태로 설정할 수 있음
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;
  • Profile 페이지로 이동할 수 있도록 Home 페이지에 Link 추가하기
    • 링크가 여러 개 이기 때문에 ul 태그를 사용하여 리스트 형태로 작성
import { Link } from 'react-router-dom';
import React from 'react'

function Home() {
  return (
    <div>
        <h1>Home</h1>
        <p>가장 먼저 보여지는 페이지</p>
        <ul>
            <li>
            	<Link to="/about">About</Link>
            </li>
            <li>
            	<Link to="/profiles/mjieun">mjieun의 프로필</Link>
            </li>
            <li>
            	<Link to="/profiles/gildong">gildong의 프로필</Link>
            </li>
            <li>
            	<Link to="/profiles/void">존재하지 않는 프로필</Link>
            </li>
      </ul>
    </div>
  )
}

export default Home
  • 실행 결과
  • 새로 만든 링크를 눌러 Profile 페이지로 이동할 수 있음
    • URL 파라미터에 따라 다른 페이지가 출력됨

쿼리 스트링

  • useLocation이라는 Hook을 사용하여 쿼리스트링 사용
  • useLocationlocation 객체를 반환하며 이 객체는 현재 사용자가 보고 있는 페이지의 정보를 지니고 있음
    • pathname: 현재 주소의 경로 (쿼리스트링 제외)
    • search: 맨 앞의 ? 문자 포함한 쿼리스트링 값
    • hash: 주소의 # 문자열 뒤의 값 (주로 History API 가 지원되지 않는 구형 브라우저에서 클라이언트 라우팅을 사용할 때 쓰는 해시 라우터에서 사용)
    • state: 페이지로 이동할때 임의로 넣을 수 있는 상태 값
    • key: location 객체의 고유 값, 초기에는 default 이며 페이지가 변경될때마다 고유의 값이 생성됨
  • 쿼리 스트링은 location.search 값을 통해 조회할 수 있다.
  • 다음과 같이 About 페이지 컴포넌트를 수정해보고 주소 창에 http://localhost:3000/about?detail=true&mode=1 라고 직접 입력해서 어떤 값이 나타나는지 확인해보자.
import React from 'react'
import { useLocation } from 'react-router-dom';

function About() {
  const location = useLocation();
  return (
    <div>
        <h1>About</h1>
        <p>리액트를 사용해보는 프로젝트 입니다.</p>
        <p>쿼리스트링: {location.search}</p>
    </div>
  )
}

export default About
  • 실행 결과
  • 쿼리스트링 값이 현재 ?detail=true&mode=1 으로 표시가 되고 있음
    • 이 문자열에서 앞에 있는 ? 로 지우고, & 문자열로 분리한뒤 key 와 value 를 파싱하는 작업을 해야 함
    • 리액트 라우터에서는 v6부터 useSearchParams 라는 Hook을 통해서 쿼리스트링을 더욱 쉽게 다룰 수 있음
import React from 'react'
import { useLocation } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';

function 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 를 사용하여 숫자 타입으로 변환을 해야 함
  • 실행 결과

중첩된 라우트

  • 게시글 목록을 보여주는 페이지와 게시글을 읽는 페이지를 만들어 보면서 중첩된 라우트를 이해해보자.

src/pages/Articles.jsx

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

function 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.jsx

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

function 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 Home from './pages/Home';
import Profile from './pages/Profile';
import Article from './pages/Article';
import Articles from './pages/Articles';

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.jsx

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

function Home() {
  return (
    <div>
        <h1>Home</h1>
        <p>가장 먼저 보여지는 페이지</p>
        <ul>
            <li>
            	<Link to="/about">About</Link>
            </li>
            <li>
            	<Link to="/profiles/mjieun">mjieun의 프로필</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
  • 게시글 목록 페이지를 열어 목록이 잘 나타나는지 확인하기

중첩된 라우트 사용

  • 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';
import Article from './pages/Article';
import Articles from './pages/Articles';

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" element={<Articles />}>
        <Route path=":id" element={<Article />} />
      </Route>
    </Routes>
  );
};

export default App;
  • 리액트 라우터에서 제공하는 Outlet 이라는 컴포넌트를 사용하여 Articles 컴포넌트 수정
    • Routechildren 으로 들어가는 JSX 엘리먼트를 보여주는 역할을 함
    • 지금의 경우엔 다음 내용이 Outlet 컴포넌트를 통해 보여짐
<Route path=":id" element={<Article />} />
  • Aritlces 컴포넌트를 다음과 같이 수정

src/pages/Articles.jsx

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

function 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
  • /articles/1 경로로 들어가보면 게시글 하단에 게시글 목록이 나타나는 것을 볼 수 있음

공통 레이아웃 컴포넌트

  • 중첩된 라우트와 Outlet 은 페이지끼리 공통적으로 보여줘야 하는 레이아웃이 있을때도 유용하게 사용할 수 있음
  • 예를 들어서, Home, About, Profile 페이지에서 상단에 헤더를 보여줘야 하는 상황을 가정해보고 중첩된 라우트를 통해 공통 레이아웃 컴포넌트를 사용해보자.
  • 공통 레이아웃을 위한 Layout 컴포넌트 만들기

src/Layout.jsx

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

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

export default Layout
  • 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';
import Article from './pages/Article';
import Articles from './pages/Articles';
import Layout from './Layout';

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 페이지에 이동하면 헤더가 잘 나타나는 것을 볼 수 있음

index props

  • path="/"와 동일한 의미를 가지는 props
  • 상위 라우트의 경로와 일치하지만, 그 이후에 경로가 주어지지 않았을 때 보여지는 라우트를 설정할 때 사용
  • Home 컴포넌트가 사용된 Route 컴포넌트를 다음과 같이 변경

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';
import Article from './pages/Article';
import Articles from './pages/Articles';
import Layout from './Layout';

const App = () => {
  return (
    <Routes>
      <Route 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 페이지가 잘 나오는 것을 볼 수 있음

리액트 라우터 부가 기능

useNavigate

  • Link 컴포넌트를 사용하지 않고 다른 페이지로 이동을 해야 하는 상황에 사용하는 Hook
  • Layout 컴포넌트를 다음과 같이 수정해보자.

src/Layout.js

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

function 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 옵션
      • 페이지를 이동할 때 현재 페이지를 페이지 기록에 남기지 않음
      • navigate('/articles', { replace: true });와 같이 사용
  • 실행 결과
  • 링크에서 사용하는 경로가 현재 라우트의 경로와 일치하는 경우 특정 스타일 또는 CSS 클래스를 적용하는 컴포넌트
  • 이 컴포넌트를 사용할 때 style 또는 className을 설정할 때 { isActive: boolean } 을 파라미터로 전달받는 함수 타입의 값을 전달
<NavLink 
  style={({isActive}) => isActive ? activeStyle : undefined} 
/>
<NavLink 
  className={({isActive}) => isActive ? 'active' : undefined} 
/>
  • Articles 페이지 컴포넌트에서 이 컴포넌트를 사용해보자.

src/pages/Articles.jsx

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

const activeStyle = {
    color: 'green',
    fontSize: 21,
  };

function Articles() {
  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
  • 현재 보고 있는 게시글의 링크의 텍스트의 스타일이 변한 것을 볼수 있음
  • 작성한 코드를 다음과 같이 리팩토링 할 수 있음
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;

NotFound 페이지 만들기

  • 전에 정의되지 않는 경로에 사용자가 진입했을 때 보여주는 페이지
  • NotFound 페이지를 pages 디렉터리에 만들어보자.

src/pages/NotFound.js

import React from 'react'

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

export default NotFound
  • App 컴포넌트 수정
    • wildcard 문자 *를 사용하여 상단에 위치하는 라우트들의 규칙을 모두 확인하고, 일치하는 라우트가 없다면 이 라우트가 화면에 나타나게 함

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';
import Article from './pages/Article';
import Articles from './pages/Articles';
import Layout from './Layout';
import NotFound from './pages/NotFound';

const App = () => {
  return (
    <Routes>
      <Route 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;
  • 일치하는 라우트가 없는 경로로 접속하면 NotFound 페이지를 보여줌
  • 컴포넌트를 화면에 보여주는 순간 다른 페이지로 이동을 하고 싶을 때 사용하는 컴포넌트
  • 즉, 페이지를 리다이렉트 하고 싶을 때 사용
  • 로그인을 안했다면 로그인 페이지를 보여주어야 하는 MyPage 컴포넌트를 만들어보자.

src/pages/Login.jsx

import React from 'react'

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

export default Login

src/pages/MyPage.jsx

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

function MyPage() {
    const isLoggedIn = false;

    if (!isLoggedIn) {
      return <Navigate to="/login" replace={true} />;
    }
  
    return <div>마이 페이지</div>;
}

export default MyPage
  • 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';
import Article from './pages/Article';
import Articles from './pages/Articles';
import Layout from './Layout';
import NotFound from './pages/NotFound';
import Login from './pages/Login';
import MyPage from './pages/MyPage';

const App = () => {
  return (
    <Routes>
      <Route 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 페이지로 이동되는 것을 볼수 있음

실습 전체 코드

References

profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈

0개의 댓글