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