React Router v6 tutorial 2탄

오호·2022년 2월 14일
4
post-custom-banner

1. No match route

말 그대로 존재하지 않는 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로 접근했을 때의 결과이다.

2. Nested Routes

중첩된 라우팅을 어떻게 구현하는지 알아보자.
먼저 /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;

완성이다.

3. Index Route

위에서 우리는 두 개의 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로 보여질지 설정할 수 있다.

4. Dynamic Routes

이번엔 동적 라우팅을 구현해보자.
먼저 /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가 존재하더라도 매칭되는 라우팅이 있다면 그 라우팅이 먼저 렌더링된다.

5. URL 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;

6. Search Params

우리는 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 버튼을 눌렀을 때)

7. Lazy Loading

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;

8. Authentication

몇 몇 컴포넌트들은 인증되지 않은 사용자에게 접근권한을 막기도한다. 이런기능을 리액트라우터와 context-api로 구현해보자.

시나리오
profile 라우팅을 추가한다. 이 라우트는 인증되지않은 사용자들로부터 막혀 있는 라우팅 주소이다.
그렇다면 인증없는 사용자는 로그인페이지로 redirect 시킬 것이다.
username을 입력하고 프로필 페이지로 이동시킬 것이다.

Profile.js

import React from 'react';

function Profile() {
  return <div>Profile Page</div>;
}

export default Profile;

Navbar.jsApp.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>
  }
/>
profile
오호
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 8월 12일

감사합니다!

답글 달기