끄적끄적 코딩
article thumbnail
Published 2023. 5. 2. 02:36
[React] 리액트 라우터 React

웹 애플리케이션에서 라우팅이라는 개념

요청한 URL에 따라 알맞은 페이지를 보여주는 것을 의미


라우팅 시스템

여러 페이지로 구성된 웹 애플리케이션을 만들 때 페이지 별로 컴포넌트들을 분리해서 프로젝트를 관리하기 위해 필요


리액트에서 라우트 시스템 구축

- 리액트 라우트
- Next.js
- react-location
- rakkas

리액트 라우트 (React Router)

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

Next.js

- 리액트 프로젝트의 프레임워크
- 리액트 프로젝트 설정 기능, 라우팅 시스템, 최적화, 다국어 시스템 지원, 서버 사이드 렌더링 등 다양한 기능 제공
- 라우팅 시스템은 파일 경로 기반으로 작동, 리액트 라우터의 대안으로 많이 사용

* 라우팅 관련 기능은 리액트 라이브러리에서 공식적을 지원하는 것이 아닌 서드 파티로 제공


싱글 페이지 애플리케이션

하나의 페이지로 이루어진 애플리케이션이라는 의미
html을 한번만 받아와서 웹 애플리케이션을 실행시킨 후, 이후에는 필요한 데이터만 받아와서 화면에 업데이트 하는 방식
* 기술적으로 한 페이지만 존재하지만, 사용자가 경험하기에는 여러 페이지가 존재하는 것처럼 느낄 수 있음

멀티 페이지 애플리케이션

- 사용자가 다른 페이지로 이동할 때마다 새로운 html을 받아오고, 페이지를 로딩할 대마다 서버에서 CSS, JS, 이미지 파일 등의 리소스를 전달받아 브라우저 화면에 보여 주는 방식
- 페이지마다 다른 html 파일을 만들어서 제공을 하거나, 데이터에 따라 유동적인 html을 생성해 주는 템플릿 엔진을 사용
- 사용자 인터랙션이 별로 없는 정적인 페이지들은 기존의 방식이 적합


리액트 라우터 설치

npm add react-router-dom

 

// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter } from "react-router-dom";

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

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

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

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

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

Route 컴포넌트Routes 컴포넌트 내부에서 사용되어야 합니다.


Link 컴포넌트
다른 페이지로 이동하는 링크 보여주기 위해서 사용합니다.
a태그는  페이지를 새로 불러오기 때문에 사용하지 않습니다.
Link 컴포넌트도 a 태그를 사용하지만, 페이지를 새로 불러오는 것을 막고 History API를 통해 브라우저 주소의 경로만 바꾸는 기능이 내장되어 있습니다.

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

 

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

페이지 주소를 정의할 때 유동적인 값을 사용해야 하는 경우
URL 파라미터 예시 : /profile/velopert
쿼리스트링 예시 : /articles?page=1&keyword=react

URL 파라미터

// src/pages/Profile.js
import { useParams } from "react-router-dom";

const data = {
  jetty: {
    name: "Jetty",
    description: "제티는 간식을 좋아해",
  },
  coke: {
    name: "Coke",
    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를 통해 설정합니다.

// 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 파라미터와 달리 Route 컴포넌트를 사용할 때 별도로 설정해야 하는 것이 없습니다.

// 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을 사용했습니다. 이 훅은 location 객체를 반환합니다. 
이 객체는 현재 사용자가 보고 있는 페이지의 정보를 지니고 있습니다.

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

쿼리스트링 값에서 ?를 지우고 &로 분리하며 key와 value를 파싱하는 작업이 필요합니다.
이 작업은 보통 npm에서 qs 또는 querystring 패키지를 설치해 처리 할 수 있습니다.

리액트 라우터에서는 v6부터 useSearchParams라는 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 => 'true', 숫자를 다룬다면 parseInt로 변환이 필요)


중첩된 라우트

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

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;

Route 안에 중첩(자식) Route를 작성해서 서브 페이지를 만들어 줄 수 있습니다.
서브 페이지의 path는 / 를 생략하고 작성합니다.
마찬가지로 element는 보여줄 컴포넌트 또는 HTML 태그를 작성해줍니다.

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

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


공통 레이아웃 컴포넌트

각 페이지에 공통으로 보여줘야 하는 레이아웃이 있을 때
ex) Home, About, Profile 페이지 상단에 헤더를 보여줘야 하는 경우
=> Header 컴포넌트를 만들어서 재사용하는 방법도 있음
=> 중첩된 라우트와 Outlet을 을 활용하여 구현이 가능 = 컴포넌트를 한번만 사용해도 된다는 장점이 있음

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

각 페이지 컴포넌트가 보여져야 하는 부분에 Outlet 컴포넌트를 사용해주었습니다.


index props

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

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

index props를 사용하면 상위 라우트의 경로와 일치하며 (path="/") 좀 더 명시적으로 표현할 수 있는 방법입니다.


useNavigate

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

// src/Layout.js
import { Outlet, useNavigate } from "react-router-dom";

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

  const goBack = () => {
    navigate(-1);
  };

  const goArticles = () => {
    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 옵션을 사용하면 페이지를 이동할 때 현재 페이지를 페이지 기록에 남기지 않습니다.

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


NavLink

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

// 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)}
      >
        게시글 1
      </NavLink>
    </li>
  );
};

export default Articles;

 

NotFound 페이지 만들기

*NotFound = 사전에 정의되지 않은 경로에 사용자가 진입했을 때 발생하는 에러

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

 path="*"로 매칭해주었습니다. *은 wildcard 문자로 아무 텍스트나 매칭한다는 뜻입니다.
라우트들의 규칙을 모두 확인하고 일치하는 라우트가 없다면 이 라우트를 화면에 나타나게 됩니다.

Navigate 컴포넌트

컴포넌트를 화면에 보여주는 순간 다른 페이지로 이동을 하고 싶을 때 사용하는 컴포넌트입니다. (페이지를 리다이렉트하고 싶은 경우)
ex) 사용자의 로그인이 필요한 페이지인데 로그인을 안 했다면 로그인 페이지로 넘겨줘야하는 경우 사용

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

로그인이 되어 있지 않다면 Navigate 컴포넌트를 통해 /login 경로로 이동합니다. 

'React' 카테고리의 다른 글

[React] 리액트란  (0) 2023.06.20
[React] 리덕스 미들웨어  (0) 2023.05.30
[React] ToDo List 만들기  (0) 2023.04.25
[React] JSX  (0) 2023.04.18
[React] props, defaultProps, propTypes  (0) 2021.03.06

검색 태그