Ayden's journal

React Router

Routes로 페이지 나누기

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import App from './components/App';
import HomePage from './pages/HomePage';
import CoursePage from './pages/CoursePage';
import CourseListPage from './pages/CourseListPage';
import WishlistPage from './pages/WishlistPage';

function Main() {
  return (
    <BrowserRouter>
      <App>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="courses" element={<CourseListPage />} />
          <Route path="wishlist" element={<WishlistPage />} />
        </Routes>
      </App>
    </BrowserRouter>
  );
}

export default Main;
import Nav from '../components/Nav';
import Footer from '../components/Footer';
import styles from './App.module.css';
import './App.font.css';

function App({ children }) {
  return (
    <>
      <Nav className={styles.nav} />
      <div className={styles.body}>{children}</div>
      <Footer className={styles.footer} />
    </>
  );
}

export default App;

Routes 컴포넌트가 골라주는 path에 해당하는 각 컴포넌트가 App 컴포넌트의 Children prop으로 내려가서 동작하는 방식인 것 같다.

 

Router 내 이동

Link

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

<li>
  <Link to="/courses">카탈로그</Link>
  <Link to={`/questions/${question.id}`}></Link>
</li>

리액트 라우터 내에서의 이동은 a 태그 대신 Link라고 하는 컴포넌트를 사용해 구현한다. 이 컴포넌트에는 to prop을 부여해 이동하고자 하는 주소를 지정할 수 있다.

중괄호와 백틱을 이용하여 prop으로 내려받은 객체의 값 일부를 Link의 to prop에 넣어줄 수도 있다.

 

NavLink

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

function getLinkStyle({ isActive }) {
  return {
    textDecoration: isActive ? 'underline' : '',
  };
}

<NavLink to="/courses" style={getLinkStyle}>카탈로그</NavLink>

Link 컴포넌트와 같이 리액트 라우터 내에서의 이동을 지원하지만, 추가적으로 style prop을 통해 함수를 지정해줄 수 있다는 점이 다르다. 이 함수에는 프로퍼티로 isActive 라는 Boolean 값을 받는다. 현재 페이지의 경로와 NavLink의 to prop 값이 같다면 isActive가 참이 된다.

 

Route 중첩

<Route path="courses">
  <Route index element={<CourseListPage />} />
  <Route path="react-frontend-development" element={<CoursePage />} />
</Route>

페이지간의 트리 구조를 Route 중첩을 통해 구현할 수 있다. Route index는 상위 path의 값을 그대로 물려받는다. 이렇게 하면 최상단에서 각 페이지들의 관계를 조금 더 쉽게 파악할 수 있다.

 

outlet

function Main() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />}>
          <Route index element={<HomePage />} />
          <Route path="questions" element={<QuestionListPage />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}
function App() {
  return (
    <>
      <Nav className={styles.nav} />
      <div className={styles.body}><Outlet /></div>
      <Footer className={styles.footer} />
    </>
  );
}

Routes 안에는 반드시 Route나 Fragment만 배치할 수 있다. App 컴포넌트를 Routes 안에서 사용하려면 element prop을 사용해야 한다. 이렇게 하면 route가 선택한 컴포넌트는 <Outlet />이 있는 부분에서 랜더링 된다.

 

useParams

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

<Route path="courses">
  <Route index element={<CourseListPage />} />
  <Route path=":courseSlug" element={<CoursePage />} />
</Route>

function getCourseBySlug(courseSlug) {
  return courses.find((course) => course.slug === courseSlug);
}

function CoursePage() {
  const { courseSlug } = useParams();
  const course = getCourseBySlug(courseSlug);

useParams이란 리액트 라우터에서 제공하는 Hooks 중 하나로 파라미터 값을 URL을 통해서 넘겨받은 페이지에서 사용할 수 있도록 해준다.

내가 이해한 것이 맞다면, 페이지가 /courses/???? 로 접근할 때, 이 ???? 부분을 리액트 라우터가 콜론으로 선언한 변수 courseSlug에 넣어주는 것이다. 이렇게 선언된 변수들은 useParams 라는 훅에 저장되어있어서, 이 문자열을 사용해 배열에서 (find 메소드를 사용해) 특정한 인덱스 하나만을 집어오는 것이다.

 

존재하지 않는 페이지 처리

<Route path="*" element={<NotFoundPage />} />

자주 일어나는 일은 아니겠지만, 이따금씩 사용자가 멋대로 주소를 입력하여 Routes 안에 존재하지 않는 path로 이동하려 할 수도 있다. 이럴 경우에 대비해 예외처리를 해두지 않는다면 사용자는 괴상한 에러 페이지를 만나게 될 것이다. Route의 path를 "*"로 처리하면 Routes 내부를 돌다가 path를 못 찾으면 정해둔 <NotFoundPage /> 컴포넌트를 랜더링하게 된다.

 

앞선 단락에서처럼 useParams을 사용해 path를 변수화해버렸다면, 존재하지 않는 /courses/saldfjioqjfasdjf 로 접근해도 예외 처리에 걸리지 않는다. 리액트 라우터는 'saldfjioqjfasdjf' 부분을 변수에 넣고, CoursePage 컴포넌트는 getCourseBySlug(courseSlug)를 돌려서 존재하지 않는 course 객체를 줏어오려 할 것이다(당연히 없으니까 undef가 튀어나온다).

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

function CoursePage() {
  const { courseSlug } = useParams();
  const course = getCourseBySlug(courseSlug);
  
  if (!course) {
  	return <Navigate to="/courses" />
  }

이럴 때는 if문과 Navigate 컴포넌트를 이용해 해결해줄 수 있다. Navigate는 Link나 NavLink와 비슷하지만, 특정 조건이 만족되면 정해둔 페이지로 리다이렉트 시켜주는 기능이 있다.

 

useSearchParams

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

const [searchParam, setSearchParam] = useSearchParams();
const initKeyword = searchParam.get('keyword');
const [keyword, setKeyword] = useState(initKeyword || '');
const courses = getCourses(initKeyword);

const handleKeywordChange = (e) => setKeyword(e.target.value);

const handleSubmit = (e) => {
  e.preventDefault();
  setSearchParam(keyword ? { keyword } : {});
};
  
<form className={searchBarStyles.form} onSubmit={handleSubmit}>
  <input
    name="keyword"
    value={keyword}
    onChange={handleKeywordChange}
    placeholder="검색으로 코스 찾기"
  ></input>

form 태그의 input이 모종의 이유로 submit 되면 이 값이 쿼리로 넘어가는데, useSearchParams는 그 쿼리 값을 쌔벼온다. 쌔벼온 쿼리 값은 객체의 형태로 searchParam state에 저장된다. 객체이기 때문에 지원되는 메소드가 이것저것 있는데, get() 메소드를 사용해서 특정한 쿼리의 값을 받아볼 수 있다. 위의 예시에서는 name="keyword"에서 submit된 input value를 useSearchParams가 쌔벼와서, 그걸 initKeyword라는 변수에 넣어주고 있다.

<CourseItem /> 컴포넌트는 courses 배열을 map으로 돌려서 만드는데, getCourses() 함수는 filter 메소드로 전체 코스에서 특정 키워드가 붙어있는 친구만 받아서 courses 배열에 넣어준다. 이 getCourses() 함수의 파라미터에 initKeyword를 넣어주면 그 키워드에 맞는 course만 찾아오는 것이다. 따라서 map이 랜더링할 때 검색한 결과만 보이게 되는 것이이다.

 

왜 keyword state를 바로 이용하지 않고 useSearchParams를 이용하는가. 보니까 useSearchParams만 url에 쿼리가 들어가네.

 

useNavigate

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

const navigate = useNavigate();

const handleClick = () => {
  // ... 어떤 작업을 한 다음에 페이지를 이동
  navigate('/wishlist');
}

앞서 살펴본 Link와 Navigate는 모두 컴포넌트이지만, useNavigate 훅으로 생성한 navigate는 함수이다. 때문에 특정한 함수 안에서 작업이 완료되면 이동할 페이지를 지정하는 방식으로 사용한다.

 

react-helmet

 

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기