
프로젝트에 react-router-dom 추가
npm install react-router-dom
src/index.js에서 BrowserRouter콤포넌트로 App을 감싸준다.
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')
);
각 화면에 해당하는 페이지 콤포넌트들을 만들어준다.
const Home = () => {
return (
<div>
<h1><Home/h1>
<p>First page</p>
</div>
);
}
const About = () => {
return (
<div>
<h1><About/h1>
<p>about page</p>
</div>
);
}
Home과 About화면이 준비 되었다.
이제 특정경로에 위 화면을 연결해 주기 위해 Route 콤포넌트를 사용한다.
<Route path="주소규칙" element={연결할 페이지 콤포넌트} />
App콤포넌트에 Routes로 감싸서 페이지들을 초기화 합니다.
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을 수정한다.
const Home = () => {
return (
<div>
<h1><Home/h1>
<p>First page</p>
<Link to="/about">About</Link>
</div>
);
}
페이지 이동간에 데이터를 전달하는 방법은 URL parameter와 Query string이 있다.
Routes에 Route를 배치하는 방법 말고 createBrowserRouter를 사용하여 구성할 수 있다.
createBrowserRouter함수를 사용하여 router객체를 만들어 낸 후 RouterProvider 콤포넌트로 router 객체를 지정하게 함으로서 구성이 가능하다.
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
path: "Home",
element: <Coins />
},
{
path: "Coin",
element: <Coin />
}
]
}
]);
export default router;
루트에 해당하는 App 콤포넌트의 Outlet에 Home과 Coin path에 해당하는 Coins와 Coin 페이지가 렌더링될것이다.
index.tsx에 다음과 같이 RouterProvider에 작성된 router를 넘겨준다.
// index.tsx
<React.StrictMode>
<ThemeProvider theme={darkTheme}>
<RouterProvider router={router} />
</ThemeProvider>
</React.StrictMode>
루트에 해당하는 App component는 다음과 같다.
// App.tsx
function App() {
return <div><Outlet /></div>;
}
요약 하면
URL parameter를 전달하여 사용하는 방법은 useParams hook을 사용하는 방법이다.
Profile이라는 화면을 추가하고 해당 화면으로 진입시 url에 profile id를 넘겨주어 프로필화면을 해당 id에 해당하는 사용자의 정보를 표시하는 예를 들겠다.
<Route path="/profiles/:profile_id" element={<Profile/>} />
위와 같이 route를 구성하면 Profile 콤포넌트에서는 다음과 같이 profile_id에 접근할 수 있다.
const Profile = () => {
const { profile_id } = useParams();
return {
<div>
<p>{profile_id}의 프로필 정보</p>
</div>
};
}
Link 를 통해 해당 화면에 접근하려면 다음과 같이 하면 된다.
<Link to="/profiles/1>1번째 프로필</Link>
query string으로 페이지 이동간에 데이터를 전달할때 useLocation hook을 사용할 수 있다.
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;
location객체는 다음 값들을 가지고 있다.
주소창에서 http://localhost:3000/about?detail=true&mode=1 이라고 치면 location.search에 ?detail=true&mode=1 이란 값이 들어오게 된다.
useSearchParams hook을 사용하면 query string 처리를 더 쉽게 할 수 있다.
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>
);
}
useSearchParams는 useParams처럼 배열을 리턴하며 첫번째 객체는 get/set으로 값에 접근하거나 변경할수 있다. 두번째는 함수이며 객체형태로 값을 변경할 수 있고 변경되면 화면이 렌더링 된다.
게시글목록(/articles), 게시글상세(/articles/article_id)와 같은 주소체계를 사용하고자 한다면 라우트는 다음과 같은 형태를 갖게 될것이다.
<Route path="/articles" element={<Articles />} />
<Route path="/articles/:id" element={<Article />} />
그런데 게시글상세에 게시글 목록도 표시하고 싶다면 게시글목록 화면에 게시글 상세화면을 삽입하는 방식을 생각해 볼수 있다. 이는 Outlet 콤포넌트를 이용해서 구현이 가능하다.
라우트는 Route안에 Route를 배치한다.
<Route path="/articles" element={<Articles />}>
<Route path=":id" element={<Article />} />
</Route>
그리고 내부 라우트의 콤포넌트가 배치될 위치를 외부라우트 콤포넌트에 Outlet 콤포넌트로 정의해준다.
Articles 콤포넌트에 Outlet을 추가해준다.
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/1 과 같은 경로에 들어가면 Articles와 Article 콤포넌트가 순서대로 렌더링된다.
Link콤포넌트가 아니라 코드로 화면을 이동하고 싶을 때 사용할 수 있는 hook으로 useNavigate가 있다.
const navigate = useNavigate();
const goBack = () => {
// 이전 페이지로 이동
navigate(-1); // -2하면 두번뒤로 간다. 1 하면 앞으로 한번 간다.
};
const goArticles = () => {
// articles 경로로 이동
navigate('/articles');
// articles 경로로 이동하며 히스토리에 남기지 않음.
navigate('/articles', { replace: true });
};
to props의 주소와 동일한 경우 특정 스타일을 주입하고 싶을때 사용한다.
다음은 ArticleItem 콤포넌트가 렌더링된 라우트 경로와 일치 하는경우 activeStyle을 li 태그에 적용하는 예제이다.
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;
라우트에 등록되지 않은 경로로의 접근이 시도될때 표시할 페이지를 정의할 수 있다.
const App = () => {
return (
<Routes>
<Route path="/" 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>
);
};
여기서 * 는 wildcard 문자이며 아무 텍스트나 매칭되는 경우를 커버하며 위 예에서 상단의 라우트들을 모두 통과하는 경로의 경우 이 케이스에 걸리면서 NotFound 콤포넌트를 표시하게 된다.
createBrowserRouter를 사용한다면 다음과 같은 형식이 될것이다.
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{
path: "Home",
element: <Coins />,
errorElement: <ErrorComponent />
},
{
path: "Coin",
element: <Coin />
}
],
errorElement: <NotFound />
}
]);
export default router;
화면에 진입하는 순간 로그인여부를 확인하여 로그인이 되어 있지 않다면 로그인 페이지로 이동시키고 싶다면 Navigate콤포넌트를 리턴해주면 된다.
다음 MyPage 콤포넌트는 isLoggoedIn이 false라면 Navigate콤포넌트를 리턴하고 있다. 이 때 /login 경로를 지정하고 replace props를 지정하여 히스토리를 삭제하면서 이동하고 있다.
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;
Outlet은 router에서 자식뷰를 렌더링하는 위치를 지정한다. Root에는 그래서 반드시 하나의 Outlet이 필요하다.
root처럼 children을 갖는 자식이 있을 수 있으며 이런 경우 당연히 Outlet이 필요하다.
/coin/1/code
/coin/2/code
와 같은 url표현이 가능하다.
위 샘플에서 Coin 페이지에서 coin의 code를 표시해주는 child를 갖는 라우터는 아래와 같이 정의한다.
{
path: "coin/:coinId",
element: <Coin />,
children: [
{
path: "code",
element: <CoinCode />
}
]
},
children이 추가되었고 추가되는 path는 code이며 해당 페이지는 CoinCode이다. 이는 순서대로 Root가 렌더링되고 Root의 Outlet에 Coin이 렌더링되고 인자는 coinId로 전달된다. 그리고 Coin의 Outlet에 CoinCode가 렌더링된다.
function Coin() {
const [isShowCode, setShowCode] = useState(true);
const { coinId } = useParams();
function onToggle() {
setShowCode(!isShowCode);
}
return <div><h1>Coin name is {coinsDB[Number(coinId) - 1].name}</h1>
<Link to="./code" onClick={onToggle}>See code</Link>
{isShowCode ? <Outlet context={
{
coinId: coinId
}
}/> : null}
</div>
}
Coin page가 위와 같이 수정되었는데 Link가 하나 추가되었고 버튼 이벤트로 onToggle이 호출되면서 Outlet을 보였다 숨겼다 하고 있다.
Link가 상대 경로로 "./code"로 보내고 있기 때문에 바로 아래의 Outlet에 CoinCode가 표시된다.
useOutletContext
Outlet에 context 속성으로 값을 넘기고 있는데 이렇게 하면 Outlet에 표시되는 페이지들에 정보를 넘길 수 있다.
위와 같이 coinId를 넘기면 CoinCode페이지에서는 useOutletContext로 접근이 가능하다.
interface CoinCodeProps {
coinId: string
}
const CoinCode = () => {
const {coinId} = useOutletContext<CoinCodeProps>();
return <h1>{coinsDB[Number(coinId)-1].name} code is {coinsDB[Number(coinId)-1].code}</h1>
}
export default CoinCode;
위 샘플에서는 Props와 함께 사용된 예제이다.
useOutletContext는 Root에도 적용이 가능하기 때문에 darkMode여부나 서비스전체적으로 공유되어도 되는 정보를 저장하는 용도로 사용이 가능하다.
이 때 Root에서 전달한 context는 Coin에서 전달한 context와 별개로 동작한다. 즉 context끼리는 공유가 안된다. 다시말해 Coin과 CoinCode에서 전달받은 context는 서로 다른 값이된다.
만일 CoinCode도 Coin에서 받은 context정보를 필요로 한다면 CoinCode를 위한 Outlet의 context에 추가해주어야 한다.
let context: any = useOutletContext(); // Root에서 받은 context
context["coinId"] = coinId; // coinId추가
return <div><h1>Coin name is {coinsDB[Number(coinId) - 1].name}</h1>
<Link to="./code" onClick={onToggle}>See code</Link>
{isShowCode ? <Outlet context={
context // 추가된 context를 인자로 넘김
}/> : null}
</div>
콤포넌트 로딩 시 Navigate를 통해서 페이지 접근제거가 가능하지만 접근제어를 제공하는 별도의 콤포넌트를 만들어서 Route 구성시 접근제어를 정의할 수 있다.
interface IProtectedRoute {
children: any,
isAuthenticated: boolean
}
const ProtectedRoute = ({ children, isAuthenticated }: IProtectedRoute) => {
if (!isAuthenticated) {
// 로그인이 되어 있지 않다면 리다이렉트
return <Navigate to="/" />;
}
return children;
};
또는
const ProtectedRoute: React.FC<IProtectedRoute> = ({ children, isAuthenticated }) => {
if (!isAuthenticated) {
// 로그인이 되어 있지 않다면 리다이렉트
return <Navigate to="/" />;
}
return children;
}
이 콤포넌트를 routes에서 다음과 같이 구성하면 된다.
{
path: "search/:keyword",
element: <ProtectedRoute isAuthenticated={isLoggedIn}>
<Search />
</ProtectedRoute>
},
isLoggedIn 변수값이 true일때만 Search 콤포넌트로 이동하게 된다.