본문 바로가기
FE/React

5. React router로 SPA(Single Page Application) 개발하기

by aeyong-dev 2023. 8. 1.

5. 1 라우팅이란?

먼저 라우팅이란, 사용자가 요청한 url에 따라 다른 페이지를 보여주는 것을 말합니다. 

웹 페이지를 만들때는 단일 페이지로 구성할 수도, 여러 페이지로 구성할 수도 있습니다.

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

라우팅 시스템을 위해 많이 사용하는 두 개의 선택지를 소개할게요.

 

  • React-Router

컴포넌트 기반의 라우팅 시스템을 설정할 수 있습니다. 

가장 오래됐고, 가장 많이 씁니다.

  • Next.js

리액트 프로젝트의 프레임워크입니다. 

파일 경로 기반의 시스템을 설정할 수 있습니다. 

라우팅, 최적화, 다국어지원, SSR(Server-Side-Rendering) 등의 다양한 기능을 지원합니다. 

 

이 외에도 react-location, rakkas 등이 있습니다. 

우리는 react-router를 사용할거에요. 

가장 많이 쓰는, 인기있는 것 부터 배워보자구요.

리액트 라우터를 통해 싱글 페이지 애플리케이션(Single Page Application, SPA)을 만들어 보겠습니다. 


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

SPA, Single Page Application이란 말 그대로 '하나의 페이지로 이루어진 애플리케이션'을 의미합니다. 

그런데 라우팅으로 여러 페이지의 애플리케이션을 만들 수 있다면서요? 

근데 왜 싱글 페이지임?

 

싱글 페이지 애플리케이션 존재 이전에는, 페이지마다 html 파일 전체를 가졌습니다. 

정적인 페이지들은 괜찮겠지만, 현대의 동적인 웹 페이지들은 너무 많은 자원과 트래픽을 소모하게 됩니다. 

그래서 리액트와 같은 라이브러리로 필요한 부분만 JS를 호출해서 업데이트 하게 되었습니다. 

 

결국, 웹 애플리케이션을 실행시킨 후에 필요한 데이터를 받아 업데이트 하는 것싱글 페이지 애플리케이션 입니다. 

사용자는 여러 페이지가 존재하는 것 처럼 느끼지만, 기술적으로는 하나의 페이지만 존재하는 것입니다. 

 

라우팅 시스템은 브라우저가 서버에 html 파일을 요청하는 것이 아니라, 

브라우저 내부의 History API를 이용해

주소창 값을 변경하고,

기존 페이지의 웹 애플리케이션을 유지하면서, 

라우팅 설정에 따라 다른 페이지를 보여줍니다. 

 

History API가 뭘까요?

브라우저의 History API는 웹 브라우저의 세션 기록(히스토리)을 조작하고, 브라우저 세션의 상태를 관리하는 API입니다.
이 API를 사용하여 브라우저의 URL을 동적으로 변경하고, 페이지를 이동하거나 앞으로/뒤로 이동하는 등의 동작을 수행할 수 있습니다. 주로 라우팅 라이브러리나 SPA(Single Page Application)에서 사용되어 브라우저의 주소 표시줄과 웹 페이지의 상태를 조작하는데 활용됩니다.
History API에는 다음과 같은 주요 메서드와 속성이 있습니다:

1. `history.pushState(state, title, url)`: 현재 페이지의 상태를 변경하고 새로운 URL을 추가합니다. 이로 인해 브라우저의 히스토리에 새로운 항목이 추가됩니다. 페이지가 리로드되지 않고 URL만 변경됩니다.
2. `history.replaceState(state, title, url)`: 현재 페이지의 상태를 변경하고 현재 URL을 새로운 URL로 대체합니다. 브라우저의 히스토리에 새로운 항목이 추가되지 않고, 현재 항목이 변경됩니다.
3. `history.popstate`: 사용자가 앞으로/뒤로 버튼을 클릭하거나, `history.pushState()` 또는 `history.replaceState()`를 호출하여 히스토리를 변경하면 `popstate` 이벤트가 발생합니다. 이를 통해 페이지 전환에 대응할 수 있습니다.
4. `history.back()`: 뒤로 가기 기능을 수행합니다. 브라우저의 히스토리를 이용해 이전 페이지로 이동합니다.
5. `history.forward()`: 앞으로 가기 기능을 수행합니다. 브라우저의 히스토리를 이용해 다음 페이지로 이동합니다.
History API를 활용하면 SPA에서 페이지 이동을 더욱 부드럽게 처리할 수 있습니다. 또한, 브라우저 히스토리를 조작함으로써 뒤로 가기와 앞으로 가기 기능을 제어하고, 원하는 페이지를 표시하는 등의 다양한 사용자 경험을 제공할 수 있습니다. 이러한 기능을 이용하여 라우팅 라이브러리(예: React Router)를 사용하여 SPA의 라우팅을 관리하게 됩니다.

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

본격적으로 사용해봅시다. 

 

5. 3. 1 프로젝트 생성과 라이브러리 설치

리액트 프로젝트를 생성하고, 다음 명령어로 리액트 라우터를 설치해주세요.

$ yarn add react-router-dom

 

5. 3. 2 프로젝트에 라우터 적용

index.js를 다음과 같이 수정합니다. 

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

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

reportWebVitals();

BrowserRouter 컴포넌트로 감싸줬어요. 

이 컴포넌트는 History API를 사용하지 않고도 주소를 변경하고 주소의 경로정보를 컴포넌트에서 사용할 수 있게 해줍니다.

 

5. 3. 3 페이지 컴포넌트 만들기

src에 pages 디렉토리 생성 후 그 안에 페이지 컴포넌트 파일을 생성하겠습니다. 

const Home = () => {
  return (
    <div>
      <h1>Home</h1>
      <p>Home page body content</p>
    </div>
  );
};
export default Home;

👆Home.js

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>This is the about page</p>
    </div>
  );
};
export default About;

👆About.js

 

꼭 pages 디렉토리를 추가로 만들 필요는 없습니다. 

하지만 기능별로 컴포넌트를 나누어야만 나중에 복잡해지지 않겠죠?

 

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

브라우저 주소에 따라 원하는 컴포넌트를 보여주기 위해 Route 라는 컴포넌트를 사용합니다. 

사용법은 다음과 같습니다. 

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

 

또한 Route 컴포넌트는 Routes 컴포넌트 내부에서 사용해야 합니다. 

App 컴포넌트를 다음과 같이 수정할게요.

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

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

export default App;

localhost:3000/

서버를 실행시켜보면 처음 화면에 home 컴포넌트가 보입니다. 

 

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

Link 컴포넌트로 다른 페이지 링크 보여주는법을 알아보겠습니다. 

원래 html에서 <a>태그로 링크를 사용했지만, 리액트 라우터를 쓸 때는 사용하면 안됩니다. 

<a>태그로 페이지를 이동하게 되면 브라우저가 페이지를 새로 불러오기 때문입니다. 

Link컴포넌트 또한 <a>태그를 사용하긴 하지만, 페이지를 새로 불러오지 않고 History API를 통해 주소 경로만 바꿉니다.

 

사용법은 다음과 같습니다.

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

이제 Home 컴포넌트에서 About 컴포넌트로 이동할 수 있도록 코드를 수정해보겠습니다. 

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

const Home = () => {
  return (
    <div>
      <h1>Home</h1>
      <p>Home page body content</p>
      <Link to="/about">About</Link>
    </div>
  );
};

export default Home;

Left: localhost:3000/   Right: localhost:3000/about

이렇게 페이지가 수정된 모습을 확인할 수 있습니다. 


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

웹 페이지의 주소는 유동적일 수 있습니다. 

유동적으로 웹 페이지를 표시하는 방법은 다음 두 가지가 있습니다. 

  • URL 파라미터:
    • 주소의 경로에 유동적인 값
    • 특정 데이터를 조회할 때 사용
    • /profile/velopert
  • 쿼리스트링:
    • 주소 뒷부분에 ?문자열 이후에 key=value 쌍으로 값을 정의, &로 구분
    • 데이터 조회에 필요한 옵션을 전달할 때 사용
    • /articles?page=1&keyword=react

5. 4. 1 URL 파라미터

URL 파라미터 방법에 대해 알아보기 위해 다음의 컴포넌트를 추가로 생성하겠습니다. 

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.description}</h2>
        </div>
      ) : (
        <p>존재하지 않는 프로필입니다.</p>
      )}
    </div>
  );
};

export default Profile;
URL 파라미터는 useParams 라는 Hook을 이용하여 객체 형태로 조회할 수 있습니다. 
URL 파라미터의 이름은 Route 컴포넌트의 path props를 통해 설정할 수 있습니다. 
위의 컴포넌트에서는 data 객체에 정보를 key-value의 형태로 저장했습니다. 
그리고 username이 존재한다면 description을 보여주고, 존재하지 않는다면 존재하지 않는다는 문구를 보여줍니다. 

책에는 이렇게 적혀있었습니다. 

그리고 한동안 이해를 하지 못했어요. 

엥? username이 갑자기 어디서 튀어나온거지? 뭐지? 

이를 이해하기 위해 아래의 App 컴포넌트를 보고 오겠습니다.

 

이제 App.js를 다음과 같이 수정합니다. 

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

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

export default App;

Route 컴포넌트의 path에 :username이라고 적어줬죠. 

Profile 컴포넌트에서 username을 가져와 저기에 넣어주겠다는 말입니다. 

그러면 Profile 컴포넌트를 다시 볼게요. 

params에 useParams를 할당해줬구요. 

profile에 data[params.username] 을 할당했습니다. 

 

data는 객체인데 대괄호로 접근한다고?
저는 data[...] 이게 무슨말인지 헷갈렸어요.
이것은 data라는 객체에 접근하는 방법 중 하나입니다.
data 객체는 두 객체로 이루어진 객체이죠?
data[velopert], data[gildong] 이런식으로 객체 내부의 객체에 접근할 수 있는 것입니다.

 

아무튼, url의 /profiles/ 뒤에 오는 문자열이 username이고, 

profile은 data 객체에서 username에 해당하는 객체를 할당받은 것입니다.

/profiles/velopert 라고 작성하게 된다면, 

data[velopert]가 존재하므로 velopert.description이 화면에 나오겠네요. 

한번 확인해볼까요.

Home.js를 조금 수정할게요.

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

const Home = () => {
  return (
    <div>
      <h1>Home</h1>
      <p>Home page body content</p>
      <ul>
        <li>
          <Link to="/about">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;

이런식으로 작동하게 됩니다. 

url 파라미터에 따라 다른 결과물이 잘 보이네요.

 

5. 4. 2 쿼리스트링

URL 파라미터에서는 Route 컴포넌트에서 path 값을 조정해주었습니다.

하지만 쿼리스트링에서는 그럴 필요가 없어요. 따로 설정해야 하는 것이 없습니다. 

일단 About페이지를 수정해볼게요. 

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

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

  return (
    <div>
      <h1>About</h1>
      <p>This is the about page</p>
      <p>qeurystring: {location.search}</p>
    </div>
  );
};
export default About;

useLocation 이라는 훅이 등장했습니다. 

이 훅은 location 객체를 반환하는데, 현재 사용자가 보고있는 페이지의 정보를 가집니다. 

이 객체에는 아래 값들을 지닙니다.

  • pathname: 현재 주소 경로(쿼리스트링 제외)
  • search: ?문자를 포함한 쿼리스트링 값
  • hash: #문자열 뒤의 값. HistoryAPI가 지원되지 않는 브라우저의 해시 라우터에서 사용하는데, 저희는 쓸 일이 없으리라 예상됩니다.
  • state: 페이지 이동 시 임의로 넣을 수 있는 값
  • key: location객체의 고유값. 초기값은 default이고 페이지가 변경될 때 마다 고유값이 생성됨.

location.search를 통해 쿼리스트링의 값을 조회할 수 있네요.

이제 localhost:3000/about?detail=true&mode=1 로 접속해보겠습니다. 

쿼리스트링 값이 ?detail=true&mode=1 이네요.

원래는 ?를 지우고, &로 이어진 문자열들을 key, value쌍으로 파싱해야하지만(npm qs 또는 querystring 패키지로 가능), 리액트 라우터 v6부터는 useSearchParams라는 훅으로 해결할 수 있습니다. 

아래 코드로 수정하면서 사용해볼게요.

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>About</h1>
      <p>This is the about page</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는 배열 타입인 [searchParams, setsearchParams]를 반환합니다.

searchParams는 쿼리파라미터의 조회, 수정이 가능한 메서드들이 있는 객체를 return합니다.

get 메서드는 특정 쿼리파라미터 조회

set 메서드는 특정 쿼리파라미터 업데이트를 할 수 있어요.

setsearchParams는 쿼리파라미터를 객체형태로 업데이트 하는 함수입니다.

그러니까, 구조는 이런거죠. 

음, 그림이 개떡같긴 하지만 대충 알아먹을 수 있다 그쵸

쿼리파라미터를 쓸 때 주의사항이 있습니다. 

쿼리파라미터 조회 시, 무조건 문자열로 값을 사용해야해요. 

숫자를 다룬다면 parseInt로 숫자타입을 써야합니다. 


5. 5 중첩된 라우트

라우팅을 중첩해서 할 수 있대요.

이를 이해하기 위해 컴포넌트부터 수정해보겠습니다.

게시글 목록을 보여주는 페이지, 게시글을 읽는 페이지를 만들거에요.

먼저 pages에 Articles.js를 만들겠습니다.

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

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

export default Articles;

Article.js도 만들게요.

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

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

export default Article;

App 컴포넌트에서 라우트를 설정하겠습니다.

import Home from "./pages/Home";
import About from "./pages/About";
import { Route, Routes } from "react-router-dom";
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="/articles/:id" element={<Article />} />
    </Routes>
  );
};

export default App;

 

Home 컴포넌트에서 게시글 목록 페이지로 가는 링크도 추가할게요.

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

const Home = () => {
  return (
    <div>
      <h1>Home</h1>
      <p>Home page body content</p>
      <ul>
        <li>
          <Link to="/about">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">Article List</Link>
        </li>
      </ul>
    </div>
  );
};

export default Home;

잘 작동하는 모습을 볼 수 있습니다. 

여기서, 게시글을 열었을 때, 게시글의 목록을 하단에 보여주고 싶다면 어떻게 해야할까요?

뭐, 아래의 코드를 사용하면 되겠죠.

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

 

 

하지만 중첩 라우트를 사용한다면 더 좋게 구현할 수 있습니다. 

중첩라우트를 사용해볼게요. App컴포넌트를 수정합시다. 

import Home from "./pages/Home";
import About from "./pages/About";
import { Route, Routes } from "react-router-dom";
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;

:id 라고만 쓰고, /articles 아래에 넣었네요. 

다음으로, Articles 컴포넌트에 Outlet 컴포넌트를 사용할 것입니다. 

이 컴포넌트는 리액트 라우터에서 기본적으로 제공해요. 

Route 컴포넌트의 Children으로 들어가는 JSX 엘리먼트를 보여줍니다. 

대충 Route 아래의 태그들을 보여준다는 뜻이에요. 

그럼 위의 App 컴포넌트에서는 <Route path="/articles/:id " element={<Article/>}/> 이 Outlet으로 보이겠네요. 

Articles 컴포넌트를 수정해봅시다. 

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

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

export default Articles;

Outlet 컴포넌트가 사용된 자리에 , 위에서 언급한 <Route path="/articles/:id" element={<Article/>}/>이 보이겠습니다.

확인해볼게요.

게시글 하단에 목록이 잘 보이네요. 

 

5. 5. 1 공통 레이아웃 컴포넌트

중첩된 라우트와 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;

 

 

각각의 페이지 컴포넌트가 보여져야 하는 곳에 Outlet 컴포넌트를 작성했습니다. 

이제 App.js를 수정할게요.

import Home from "./pages/Home";
import About from "./pages/About";
import { Route, Routes } from "react-router-dom";
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;

Layout을 라우트하는 컴포넌트 아래에 Home, About, Profile 컴포넌트를 넣어줬구요. 

실행시켜봅시다. 

헤더가 고정적으로 잘 보이네요. 멋있다. 

 

5. 5. 2 index props

Route 컴포넌트에는 index라는 props가 있습니다. 

이 props는 path="/" 와 같은 의미에요. 

그러니까, 다음과 같이 App 컴포넌트를 수정할 수 있다는 말이죠. 

...
<Routes>
      <Route element={<Layout />}>
      <Route index element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/profiles/:username" element={<Profile />} />
</Route>
...

 

이렇게 코드를 바꾸어도 여전히 잘 동작합니다. 

상위 라우트 경로와 일치하면서 - 그 이후 경로가 주어지지 않았을 때 보여지는 라우트를 설정할 수 있습니다. 

path="/"와 기능은 같지만 더 직관적이에요.


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

리액트 라우터에서 제공하는 유용한 API들을 알아보겠습니다. 

 

5. 6. 1 useNavigate

useNavigate는 Link 컴포넌트를 사용하지 않고 다른 페이지로 이동해야 할 때 사용합니다. 

Layout 컴포넌트를 수정해볼게요. 

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;

useNavigate 함수에서 파라미터가 숫자라면 앞 또는 뒤로 갑니다. 

navigate(-1)은 뒤로 한번 가기, navigate(-2)는 뒤로 두번 가기. 

반대로 navigate(1)은 앞으로 한번 가기 이런식이에요. 

당연히, 뒤로가기를 한번 이상 한 상태이어야만 사용할 수 있겠죠? 

 

다른 페이지로 갈 때, replace라는 옵션이 있습니다. 

페이지를 이동할 때, 현재 페이지를 기록에 남기지 않는다고 해요. 

무슨말일까요? 일단 만들어보겠습니다. 

goArticles 함수를 수정해보세요. 

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

home 페이지에서 about을 갔다가, 게시글 목록 페이지를 눌렀어요. 

그리고 뒤로가기 버튼을 눌렀습니다. 

정상적인 상황이라면 about으로 갈거겠죠? 하지만 home으로 이동하네요.

replace: true 옵션이 활성화돼있기 때문입니다. 

기록에 남지 않는다는 말은 이런 뜻이에요. 

뒤로가기 또는 앞으로가기 기록에 남지 않는다는 이런뜻이에요.

 

5. 6. 2 NavLink

NavLink 컴포넌트는 스타일과 관련이 있습니다. 

링크의 경로가 현재 라우트 경로와 일치한다면 특정 스타일 또는 클래스를 적용합니다. 

예시로 확인해볼게요. 

<NavLink
	style={{isActive} => isActive ? activeStyle : undefined}
/>

<NavLink
	class={{isActive} => isActive ? 'active' : undefined}
/>

 

 

이런식으로 사용합니다. 

isActive의 값에 따라 activeStyle 이라는 스타일을 주거나, active라는 클래스를 적용할 수도 있어요. 

그럼 이것을 Articles 컴포넌트에 적용시켜 보겠습니다. 

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)}
          >
            Article 1
          </NavLink>
        </li>
        <li>
          <NavLink
            to="/articles/2"
            style={({ isActive }) => (isActive ? activeStyle : undefined)}
          >
            Article 2
          </NavLink>
        </li>
        <li>
          <NavLink
            to="/articles/3"
            style={({ isActive }) => (isActive ? activeStyle : undefined)}
          >
            Article 3
          </NavLink>
        </li>
      </ul>
    </div>
  );
};

export default Articles;

활성화된 항목들은 크기가 커지고 초록색이 됩니다. 

 

그런데 저 코드에서, 같은 부분이 반복되죠? 

저는 이게 마음에 들지 않아요. 

아래와 같이 수정해서 더 깔끔하게 만들어주겠습니다. 

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

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

  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)}
      >
        Article {id}
      </NavLink>
    </li>
  );
};

export default Articles;

 

5. 6. 3 404 NotFound 페이지 만들기

잘못된 경로로 진입했을 때 보여주는 페이지를 만들겠습니다. 

pages 디렉터리에 NotFound.js를 만들게요. 

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

그리고 App 컴포넌트를 수정하겠습니다. 

import Home from "./pages/Home";
import About from "./pages/About";
import { Route, Routes } from "react-router-dom";
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;

*는 wildcard입니다. 모두 다 선택, 뭐 그런뜻으로 통용됩니다. 

그래서 저기서는, 위의 규칙들을 제외한 나머지 경로들은 NotFound 페이지를 보여준다는 뜻입니다. 

존재하지 않는 경로를 만들었더니 잘 뜨는군요. 

 

5. 6. 4 Navigate 컴포넌트

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

리다이렉트 할 때 사용한다, 라고 보면 됩니다. 

예를 들어서, 마이페이지로 이동하는데 만약에 로그인을 안했다면? 로그인페이지로 넘어가야겠죠?

이럴때 사용합니다. 

마이페이지를 띄우는 대신 로그인 화면으로 보내버리는거죠. 

pages/Login.js를 만들겠습니다. 

const Login = () => {
  return <div>Login page</div>;
};
export default Login;

간단하게 로그인 페이지다, 하는 정도만 만들었습니다. 

마이페이지도 만들어볼게요. 

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

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

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

  return <div>Mypage</div>;
};

export default MyPage;

여기서 isLoggedIn은 기본값으로 false이지만, 로그인을 한다면 true가 될 것입니다. 

로그인되지 않은 상태라면 Navigate 컴포넌트로 인해 /login으로 이동하겠죠. 

replace는 기록을 남기지 않는다고 했고요. 

이제 App 컴포넌트를 수정하겠습니다. 

import Home from "./pages/Home";
import About from "./pages/About";
import { Route, Routes } from "react-router-dom";
import Profile from "./pages/Profile";
import Articles from "./pages/Articles";
import Article from "./pages/Article";
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;

url에 /mypage를 입력했지만 /login으로 이동되는 모습을 확인할 수 있습니다.