말 그대로 존재하지 않는 URL에 접근했을 때 설정방법에 대해 알아보자. 아직 설정을 하지 않았으므로 경고 메세지만 나올 뿐 애플리케이션이 종료되거나 다른 동작을 하지는 않는다.
/user
라는 없는 URL에 접근했지만 아직 애플리케이션이 정상 동작하는 것을 볼 수 있다. 이러한 사용자 경험은 유저 프렌들리하지 않게 느껴진다.
먼저 /src/components/NoMatch.js
라는 파일을 하나 생성한다.
import React from 'react';
function NoMatch() {
return <div>Page not found</div>;
}
export default NoMatch;
그리고 App.js
에 다음을 추가한다.
function App() {
return (
<>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="order-summary" element={<OrderSummary />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
}
path='*'
는 어떤 매치 결과도 없을 때를 뜻한다.
어떤 매칭결과가 없을 때는 NoMatch
컴포넌트를 렌더링하게 된다.
똑같이 /user
URL로 접근했을 때의 결과이다.
중첩된 라우팅을 어떻게 구현하는지 알아보자.
먼저 /src/components/product.js
파일을 생성해보자.
import React from 'react';
function Product() {
return (
<div>
<input type="search" placeholder="Seach products" />
</div>
);
}
export default Product;
Navbar.js
에도 링크 컴포넌트를 추가하자.
const Navbar = () => {
const navLinkStyles = ({ isActive }) => {
return {
fontWeight: isActive ? 'bold' : 'normal',
textDecoration: isActive ? 'none' : 'underline',
};
};
return (
<nav>
<NavLink style={navLinkStyles} to="/">
Home
</NavLink>
<NavLink style={navLinkStyles} to="/about">
About
</NavLink>
<NavLink style={navLinkStyles} to="/products">
Products
</NavLink>
</nav>
);
};
App 컴포넌트에 라우트 추가하는 것은 계속해왔으니 생략하겠다. 당연히 해야됨!
중첩된 라우팅을 표현하기 위해서 컴포넌트 내에 링크를 더 추가하해보자.
Product.js
function Product() {
return (
<>
<div>
<input type="search" placeholder="Seach products" />
</div>
<nav>
<Link to="featured">Featured</Link>
<Link to="new">new</Link>
</nav>
</>
);
}
주의할점! nested routing을 표현하고 싶을 때 to 프롭스에
/
를 붙이지 않는다
이제 앞에 표현된 featured와 new 컴포넌트를 만들어보자.
NewProducts.js
import React from 'react';
function NewProducts() {
return <div>List of New Products</div>;
}
export default NewProducts;
FeaturedProducts.js
도 동일하게 만들어준다.
App.js에 nested route를 세팅해보자..!
function App() {
return (
<>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="order-summary" element={<OrderSummary />} />
<Route path="products" element={<Product />}>
<Route path="featured" element={<FeaturedProducts />} />
<Route path="new" element={<NewProducts />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
}
이렇게 기존 products
에 children으로 설정해주면 nesting이 완료된다.
이렇게까지 하면 nested routing 설정이 완료되었다. 하지만 아직 해당 컴포넌트를 어디에 렌더링해서 보여줘야하는지 정해주지 않았다.
react-router-dom
에서 제공하는 Outlet
컴포넌트를 사용해보자.
Product.js
import React from 'react';
import { Link, Outlet } from 'react-router-dom';
function Product() {
return (
<>
<div>
<input type="search" placeholder="Seach products" />
</div>
<nav>
<Link to="featured">Featured</Link>
<Link to="new">new</Link>
</nav>
<Outlet />
</>
);
}
export default Product;
완성이다.
위에서 우리는 두 개의 nested route를 설정해보았다. 그런데 어떨 때 우리는 /products
로 접근했을 때 기본적으로 Featured
컴포넌트를 보여주고 싶을 때가 있을 것이다. 이런 것을 Index Routef라고 부르는데 설정해보자.
App.js
function App() {
return (
<>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="order-summary" element={<OrderSummary />} />
<Route path="products" element={<Product />}>
<Route index element={<FeaturedProducts />} />
<Route path="featured" element={<FeaturedProducts />} />
<Route path="new" element={<NewProducts />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
}
path 대신에 index라는 prop으로 어떤 것이 index로 보여질지 설정할 수 있다.
이번엔 동적 라우팅을 구현해보자.
먼저 /src/components/User.js
를 생성한다.
function Users() {
return (
<div>
<h2>User 1</h2>
<h2>User 2</h2>
<h2>User 3</h2>
</div>
);
}
export default Users;
App.js
에 라우팅 추가하는 것을 잊지말자.
function App() {
return (
<>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="order-summary" element={<OrderSummary />} />
<Route path="products" element={<Product />}>
<Route index element={<FeaturedProducts />} />
<Route path="featured" element={<FeaturedProducts />} />
<Route path="new" element={<NewProducts />} />
</Route>
<Route path="users" element={<Users />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
}
이번엔 User의 상세정보를 나타내줄 수 있는 UserDetails.js
를 만들어보자
import React from 'react';
function UserDetails() {
return <div>Details about user</div>;
}
export default UserDetails;
App.js
function App() {
return (
<>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="order-summary" element={<OrderSummary />} />
<Route path="products" element={<Product />}>
<Route index element={<FeaturedProducts />} />
<Route path="featured" element={<FeaturedProducts />} />
<Route path="new" element={<NewProducts />} />
</Route>
<Route path="users" element={<Users />} />
<Route path="users/1" element={<UserDetails />} />
<Route path="users/2" element={<UserDetails />} />
<Route path="users/3" element={<UserDetails />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
}
이렇게 User1,User2,User3에 대해 모두 라우팅을 추가해준다.
잘 동작하는 것처럼 보이지만 당연히 이것이 Dynamic Routing은 아니다. 100명이면 100명 라우팅을 설정하는 것은 사실상 불가능하기 때문이다.
아까 작성한 App.js
를 수정해보자.
<Route path="users/:userId" element={<UserDetails />} />
userId는 Params로 어떤 값이든 올 수가 있다.
Params에 전달된 userId
는 문자열 형태로 어떤 값이든 올 수 있다고 했다.
users/admin
으로 접속한다면 어떻게 될까?
당연히 Details about user라는 텍스트를 보여준다.
그러면 이런 상황을 대비해서는 새로운 컴포넌트를 만들고 라우트를 미리 설정해주어야 한다.
<Route path="users/admin" element={<Admin />} />
이렇게 말이다. react-router-dom
라이브러리는 params보다 먼저 매칭되는 라우팅을 찾는다. 그렇기 때문에 params가 존재하더라도 매칭되는 라우팅이 있다면 그 라우팅이 먼저 렌더링된다.
우리는 앞에서 User1,2,3에 대해서 같은 컴포넌트를 하나의 dynamic route로 처리하는 기능을 사용해봤다.
하지만 우리는 세명이 다른 정보를 가지고 있을 때 어떻게 해야할까?
react-router-dom에서 제공하는 useParams라는 훅을 사용할 수 있다
UserDetails.js
import React from 'react';
import { useParams } from 'react-router-dom';
function UserDetails() {
const params = useParams();
const userId = params.userId;
return <div>Details about user {userId}</div>;
}
export default UserDetails;
우리는 id같은 정보들은 params로 넘겨줄 수 있다는 것을 확인했다. 그런데 params이외에도 정보를 넘겨줄 수 있는 방법이 있는데 그것은 querystring이다.
리액트 라우터에서는 쿼리스트링을 Search Params라고 부른다
예시를 보면서 이해해보자.
Users.js
import React from 'react';
import { Outlet, useSearchParams } from 'react-router-dom';
function Users() {
const [searchParams, setSearchParams] = useSearchParams();
const showActiveUsers = searchParams.get('filter') === 'active';
return (
<>
<div>
<h2>User 1</h2>
<h2>User 2</h2>
<h2>User 3</h2>
</div>
<Outlet />
<div>
<button onClick={() => setSearchParams({ filter: 'active' })}>
Active Users
</button>
<button onClick={() => setSearchParams({})}>Reset Filter</button>
</div>
{showActiveUsers ? (
<h2>Showing active users</h2>
) : (
<h2>showing all users</h2>
)}
</>
);
}
export default Users;
쿼리스트링을 설정하기 위해서 useSearchParams
라는 훅을 제공한다. 이 훅은 마치 useState
와 흡사하다.
active 버튼을 눌렀을 때)
filter 버튼을 눌렀을 때)
About.js
를 lorem ipsum으로 20 문단정도로 채운다. 다음 코드를 추가해보자.
import React from 'react';
import { Routes, Route } from 'react-router-dom';
const LazyAbout = React.lazy(() => import('./components/About'));
function App() {
return (
<>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route
path="about"
element={
<React.Suspense fallback="loading...">
<LazyAbout />
</React.Suspense>
}
/>
</Routes>
</>
);
}
export default App;
몇 몇 컴포넌트들은 인증되지 않은 사용자에게 접근권한을 막기도한다. 이런기능을 리액트라우터와 context-api로 구현해보자.
시나리오
profile 라우팅을 추가한다. 이 라우트는 인증되지않은 사용자들로부터 막혀 있는 라우팅 주소이다.
그렇다면 인증없는 사용자는 로그인페이지로 redirect 시킬 것이다.
username을 입력하고 프로필 페이지로 이동시킬 것이다.
Profile.js
import React from 'react';
function Profile() {
return <div>Profile Page</div>;
}
export default Profile;
Navbar.js
와 App.js
에 라우트를 추가해준다.
이번에는 로그인 스테이트를 저장하고 모든 컴포넌트에서 사용할 수 있도록 src/components/auth.js
를 추가해보자.
import { useState, createContext, useContext } from 'react';
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const login = (user) => {
setUser(user);
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
return useContext(AuthContext);
};
App.js
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Admin from './components/Admin';
import { AuthProvider } from './components/auth';
import FeaturedProducts from './components/FeaturedProducts';
import Home from './components/Home';
import Navbar from './components/Navbar';
import NewProducts from './components/NewProducts';
import NoMatch from './components/NoMatch';
import OrderSummary from './components/OrderSummary';
import Product from './components/Product';
import Profile from './components/Profile';
import UserDetails from './components/UserDetails';
import Users from './components/Users';
const LazyAbout = React.lazy(() => import('./components/About'));
function App() {
return (
<AuthProvider>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route
path="about"
element={
<React.Suspense fallback="loading...">
<LazyAbout />
</React.Suspense>
}
/>
<Route path="order-summary" element={<OrderSummary />} />
<Route path="products" element={<Product />}>
<Route index element={<FeaturedProducts />} />
<Route path="featured" element={<FeaturedProducts />} />
<Route path="new" element={<NewProducts />} />
</Route>
<Route path="users" element={<Users />}>
<Route path=":userId" element={<UserDetails />} />
<Route path="admin" element={<Admin />} />
</Route>
<Route path="profile" element={<Profile />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</AuthProvider>
);
}
export default App;
이제 로그인 컴포넌트를 생성해보고 로그인 로직을 구현해보자.
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from './auth';
function Login() {
const [user, setUser] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const handleLogin = () => {
login(user);
navigate('/');
};
return (
<div>
<label>
username :
<input type="text" onChange={(e) => setUser(e.target.value)} />
</label>
<button onClick={handleLogin}></button>
</div>
);
}
export default Login;
Navbar.js
import React from 'react';
import { Link, NavLink } from 'react-router-dom';
import { useAuth } from './auth';
const Navbar = () => {
const navLinkStyles = ({ isActive }) => {
return {
fontWeight: isActive ? 'bold' : 'normal',
textDecoration: isActive ? 'none' : 'underline',
};
};
const auth = useAuth();
return (
<nav className="primary-nav">
<NavLink style={navLinkStyles} to="/">
Home
</NavLink>
<NavLink style={navLinkStyles} to="/about">
About
</NavLink>
<NavLink style={navLinkStyles} to="/products">
Products
</NavLink>
<NavLink style={navLinkStyles} to="/profile">
Profile
</NavLink>
{!auth.user && (
<NavLink style={navLinkStyles} to="/login">
Login
</NavLink>
)}
</nav>
);
};
export default Navbar;
Profile.js
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from './auth';
function Profile() {
const auth = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
auth.logout();
navigate('/');
};
return (
<>
<div>welcome {auth.user}</div>
<button onClick={handleLogout}>logout</button>
</>
);
}
export default Profile;
여기까지하면 로그인 했을 때 '/' 으로 넘어가고 로그아웃했을 때도 '/'으로 넘어가는 로직이 완성되었다.
하지만 아직 Profile 컴포넌트가 protect 되어지지 않고 있다
RequireAuth
라는 재사용 가능한 컴포넌트를 이용해서 한번씩 래핑해주자.
RequireAuth.js
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from './auth';
function RequireAuth({ children }) {
const auth = useAuth();
if (!auth.user) {
return <Navigate to="/login" />;
}
return children;
}
export default RequireAuth;
App.js
<Route
path="profile"
element={
<RequireAuth>
<Profile />
</RequireAuth>
}
/>
감사합니다!