react-router

react-router는 특정한 주소와 컴포넌트를 연결시켜주고 주소 이동을 통해서 다른 컴포넌트 화면을 보여줄 수 있도록 하는 기능을 제공하는 라이브러리이다.
지금까지는 localhost라는 단일한 페이지에서만 작업을 진행했으나 이제부터는 localhost/posts등 다양한 주소에서 작업을 진행하게 된다.

제일 먼저 해야할 일은 index.js 에서 app을 BrouwerRouter로 감싸주는 것이다. 이렇게 해야 해당 app 에서 브라우저라우팅 기능을 사용할수 있게 된다.

import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <BrowserRouter>
            <App />
        </BrowserRouter>
    </React.StrictMode>
)

그리고 component폴더 안에 PostsUsers 폴더를 각각 만들어서 index.jsx파일을 만들어 준다.

컴포넌트와 주소를 연결시켜놓기위한 태그는 Route이다.

    <Route path="posts" element={<Posts/>}/>

이후에 해당 페이지로 접근할 때는 a 태그가 아니라

<Link to="posts">Posts</Link>

이를 바탕으로 app.js를 작성한다.

import { Link, Route, Routes } from 'react-router-dom'
import Posts from './components/Posts'
import Users from './components/Users'

function App() {
    return (
        <div>
            <nav>
                <Link to="/posts">Posts</Link> | <Link to="/users">Users</Link>
            </nav>
            <Routes>
                <Route path="posts" element={<Posts />} />
                <Route path="users" element={<Users />} />
            </Routes>
        </div>
    )
}

export default App

이렇게 상단에 Posts | Users 항목이 나타나며 클릭하면 Posts Users 컴포넌트에 적힌 내용이 나타난다.

path 관련

버전 5 에서는 '/'라는 route랑 '/posts' 라는 route가 각각 존재할 경우, /post 에서 그냥 /에 지정된 것까지 같이 보이도록 만들어져 있었다. 그래서 exact라는 props를 통해서, 정확히 일치하는 주소일 때만 표시하도록 하는 기능이 있었다.

버전 6에서는 기본적으로 정확히 일치하는 주소일 때만 Route에 연결된 element를 표시하도록 하고 있다.

예외적으로 path에 *문자를 쓰면, 위와 일치하는 것이 없는 모든 경우에 해당 Route에 연결된 element를 표시하게 된다.

import { Link, Route, Routes } from 'react-router-dom'
import Posts from './components/Posts'
import Users from './components/Users'

function App() {
    return (
        <div>
            <nav>
                <Link to="/posts">Posts</Link> | <Link to="/users">Users</Link>
            </nav>
            <Routes>
                <Route path="posts" element={<Posts />} />
                <Route path="users" element={<Users />} />
                <Route path="*" element={<p>Not Found</p>} />
            </Routes>
        </div>
    )
}

export default App

만약 사용자가 이상한 경로를 입력하여 들어가려고 하면

이렇게 not found가 나온다.

Nested Route/ Outlet (useOutletContext)

Nested RouteOutlet에 대해서 설명하기 위해
Posts페이지에서는 글 제목만 보여주고 거기서 제목을 누르면 그제서야 PostDetail페이지로 넘어가면서 내용을 보여주도록 구성한다.

Data세팅

src/constants 폴더 생성후 postData, userData.js 파일을 생성하고 데이터를 작성한다.
postData.js

export const postData = [
    {
        userId: 1,
        id: 1,
        title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
        body: 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto',
    },
    {
        userId: 1,
        id: 2,
        title: 'qui est esse',
        body: 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla',
    },
    {
        userId: 1,
        id: 3,
        title: 'ea molestias quasi exercitationem repellat qui ipsa sit aut',
        body: 'et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut',
    },
    {
        userId: 1,
        id: 4,
        title: 'eum et est occaecati',
        body: 'ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit',
    },
    {
        userId: 1,
        id: 5,
        title: 'nesciunt quas odio',
        body: 'repudiandae veniam quaerat sunt sed\nalias aut fugiat sit autem sed est\nvoluptatem omnis possimus esse voluptatibus quis\nest aut tenetur dolor neque',
    },
    {
        userId: 1,
        id: 6,
        title: 'dolorem eum magni eos aperiam quia',
        body: 'ut aspernatur corporis harum nihil quis provident sequi\nmollitia nobis aliquid molestiae\nperspiciatis et ea nemo ab reprehenderit accusantium quas\nvoluptate dolores velit et doloremque molestiae',
    },
    {
        userId: 1,
        id: 7,
        title: 'magnam facilis autem',
        body: 'dolore placeat quibusdam ea quo vitae\nmagni quis enim qui quis quo nemo aut saepe\nquidem repellat excepturi ut quia\nsunt ut sequi eos ea sed quas',
    },
    {
        userId: 1,
        id: 8,
        title: 'dolorem dolore est ipsam',
        body: 'dignissimos aperiam dolorem qui eum\nfacilis quibusdam animi sint suscipit qui sint possimus cum\nquaerat magni maiores excepturi\nipsam ut commodi dolor voluptatum modi aut vitae',
    },
    {
        userId: 1,
        id: 9,
        title: 'nesciunt iure omnis dolorem tempora et accusantium',
        body: 'consectetur animi nesciunt iure dolore\nenim quia ad\nveniam autem ut quam aut nobis\net est aut quod aut provident voluptas autem voluptas',
    },
    {
        userId: 1,
        id: 10,
        title: 'optio molestias id quia eum',
        body: 'quo et expedita modi cum officia vel magni\ndoloribus qui repudiandae\nvero nisi sit\nquos veniam quod sed accusamus veritatis error',
    },
]

userData.js

export const userData = [
    {
        id: 1,
        name: 'Leanne Graham',
        email: 'Sincere@april.biz',
    },
    {
        id: 2,
        name: 'Ervin Howell',
        email: 'Shanna@melissa.tv',
    },
    {
        id: 3,
        name: 'Clementine Bauch',
        email: 'Nathan@yesenia.net',
    },
    {
        id: 4,
        name: 'Patricia Lebsack',
        email: 'Julianne.OConner@kory.org',
    },
    {
        id: 5,
        name: 'Chelsey Dietrich',
        email: 'Lucio_Hettinger@annie.ca',
    },
    {
        id: 6,
        name: 'Mrs. Dennis Schulist',
        email: 'Karley_Dach@jasper.info',
    },
    {
        id: 7,
        name: 'Kurtis Weissnat',
        email: 'Telly.Hoeger@billy.biz',
    },
    {
        id: 8,
        name: 'Nicholas Runolfsdottir V',
        email: 'Sherwood@rosamond.me',
    },
    {
        id: 9,
        name: 'Glenna Reichert',
        email: 'Chaim_McDermott@dana.io',
    },
    {
        id: 10,
        name: 'Clementina DuBuque',
        email: 'Rey.Padberg@karina.biz',
    },
]

PostDetail컴포넌트는 아까와 마찬가지로 그냥 PostDetail이라는 글자만 표시되도록 만들어 놓는다

posts/1 → 1번 글 세부 내용 표시
posts/2 → 2번 글 세부 내용 표시

<Route path="posts" element={<Posts/>}>
  <Route path=":postId" element={<PostDetail/>}/>
</Route>

이러한 형태를 Nested Route 라고 한다. 서브 라우트라고도 표현하는데, 이렇게 작성하게 되면URL 구조가 주소/posts/:postId 와 같이 계층적으로 이어지면서 설정될 뿐만 아니라 Nested Route 를 쓰면 Outlet 이라는 것을 통해서, 부모 Route 의 요소를 표시하면서 동시에 하위 Route 요소를 표시한다 (하위 Route 요소만 표시되는 것이 아니다! 따라서 해당 Route 에 대해 공통적인 부모 컴포넌트를 표시하고자 할 때만 사용한다.)

→ 여기서 :postId 처럼 문자 앞에 : 를 써주게 되면, 그때부터는 단어 그 자체가 아니라 파라미터로서 인식하겠다는 뜻이 된다. 그래서 이렇게 작성하면 posts/1 이라는 주소로 사용자가 접근할 경우, :postId 에 연결된 컴포넌트가 보여지게 되고, 해당 컴포넌트에서는 주소에 명시된 1이라는 숫자를 postId 라는 이름의 변수로 사용할 수 있게 되는 것이다. 변수를 가져오는 것은 useParams 라는 Hook 이 담당한다.

그렇다면 이제 posts/글번호로 넘어가면 PostDetail페이지에서 해당 글을 볼 수 있도록 한다.

import React from 'react'
import { Link } from 'react-router-dom'
import { postData } from '../../constants/postData'

function Posts() {
    return (
        <div>
            {postData.map((post) => {
                return (
                    <p key={post.id}>
                        <Link to={`/posts/${post.id}`}>{post.title}</Link>
                    </p>
                )
            })}
        </div>
    )
}

export default Posts

이렇게 해주면 postData의 title만 나열되며 해당 항목을 클릭하면 post/id 경로로 이동하게 된다.

여기서 해당 경로로 갈때 그 id 값에 해당하는 글의 내용을 보여줄수 있도록 해보자.

import React from 'react'
import { useParams } from 'react-router-dom'
import { postData } from './../../../constants/postData'

function PostDetail() {
    const { postId } = useParams()
    const post = postData.find((post) => post.id === Number(postId))

    return (
        <div>
            <h3>{post.title}</h3>
            <p>{post.body}</p>
        </div>
    )
}

export default PostDetail

postId는 useParams를 호출하여 가져온 파라미터이고 post값을 postData에서 id가 postId와 일치한 데이터로 선언하고 렌더링을 진행하면

이런 식으로 id 값에 따라서 해당 글의 내용이 나타나게 된다. Outlet을 사용했기 때문에 부모요소의 타이틀 내용은 유지된 상태로 렌더링 되는데, 만약 부모요소를 공통적으로 사용할 필요가 없으면 app.jsx에서 작성한 posts 링크를 분리하면 된다. 분리할때 :postsId 왼쪽에 posts링크 경로를 작성해줘야 한다.

 <Route path="posts" element={<Posts />} />
<Route path="posts/:postId" element={<PostDetail />} />

이렇게 작성하면

이런 식으로 타이틀 목록이 사라지고 글만 나타나게 된다.

만약 Outlet 을 사용하는 경우, useOutletContext 라는 특별한 Hook 을 사용할 수 있는데

아래처럼 context 를 명시하면,

import React from 'react'
import { Link, Outlet } from 'react-router-dom'
import { postData } from '../../constants/postData'

function Posts() {
    return (
        <div>
            {postData.map((post) => {
                return (
                    <p key={post.id}>
                        <Link to={`/posts/${post.id}`}>{post.title}</Link>
                    </p>
                )
            })}
            <Outlet context={{ data: 1 }} />
        </div>
    )
}

export default Posts

하위 컴포넌트에서 useOutletContext를 통해서 해당 context값을 가져와서 사용할수 있다.

const context = useOutletContext()

function PostDetail() {
    const { postId } = useParams()
    const post = postData.find((post) => post.id === Number(postId))
    const context = useOutletContext()

    return (
        <div>
            <h3>{post.title}</h3>
            <p>{post.body}</p>
            <h1>{context.data}</h1>
        </div>
    )
}

이렇게 해주면 어떤 게시글을 가도 하단에 1이라는 숫자가 보이게 된다.

Index Route

Nested Route 구조에서 /뒤에 아무도 없는 주소상태일때 보여줄 컴포넌트를 정하는 Route를 말한다.

posts Nested Route 를 아래처럼 작성한다.

<Route path="posts" element={<PostLayout/>}>
  <Route index element={<Posts/>}/>
  <Route path=":postId" element={<PostDetail/>}/>
</Route>

이렇게 하면 /posts 로 접근했을 때만 Posts 컴포넌트 내용이 보이게 되고 /posts 이하의 어떤 주소로 접근하든 PostLayout에 Outlet이 들어가고 , Posts 에서는 Outlet을 빼야한다.

import React from 'react'
import { Outlet } from 'react-router-dom'

function PostLayout() {
    return (
        <div>
            <h1>PostLayout</h1>
            <Outlet />
        </div>
    )
}

export default PostLayout
import React from 'react'
import { Link } from 'react-router-dom'
import { postData } from '../../constants/postData'

function Posts() {
    return (
        <div>
            {postData.map((post) => {
                return (
                    <p key={post.id}>
                        <Link to={`/posts/${post.id}`}>{post.title}</Link>
                    </p>
                )
            })}
        </div>
    )
}

export default Posts

Router Hooks 활용하기

useSearchParams

useSearchParams 는 쿼리 파라미터를 사용하기 더 편하게 해주는 함수이다.

const [searchParams, setSearchParams] = useSearchParams()

setSearchParams를 통해서 객체를 할당해주면, 주소에 자동으로 해당 객체가 파싱되어서 쿼리 파라미터가 설정된다.

{name:'jyc} 으로 설정한다면 주소?name=jyc형태로 지정

posts/index.jsx에다가 filter:1이라는 쿼리 파라미터를 설정한다.

function Posts() {
    const [searchParams, setSearchParams] = useSearchParams()
    useEffect(() => {
        setSearchParams({ filter: 1 })
    }, [searchParams])

    return (
        <div>
            {postData.map((post) => {
                return (
                    <p key={post.id}>
                        <Link to={`/posts/${post.id}`}>{post.title}</Link>
                    </p>
                )
            })}
        </div>
    )
}

일단 input 없이, 주소에 filter를 명시해서 해당 문자열대로 글이 필터링 되도록 구현해보자.

    const [searchParams, setSearchParams] = useSearchParams()
    const [posts, setPosts] = useState(postData)
    useEffect(() => {
        setPosts(
            postData.filter((post) => {
                const filter = searchParams.get('filter')
                const title = post.title.toLowerCase()
                return filter ? title.includes(filter) : true
            }),
        )
    })

이런식으로 posts 페이지에 들어오자 마자 filter 파라미터 값이 존재한다면 해당 존재하는 filter의 값에 해당하는 title만 불러온다. 만약 존재하지 않으면 전체 데이터를 불러오는 코드이다.

이제 input을 받아서 검생르 할수 있도록 해보자(searchInputHandler를 만들고, searchParams 가 변할 때마다 setPostst 가 실행되도록 해보자.)

import React, { useEffect, useState } from 'react'
import { Link, Outlet, useSearchParams } from 'react-router-dom'
import {postData} from '../../constants/postData'

function Posts() {
  const [searchParams, setSearchParams] = useSearchParams()
  const [posts, setPosts] = useState(postData)

  const searchInputHandler = (e) => {
    const filter = e.target.value;
    filter ? setSearchParams({filter}) : setSearchParams({})
  }

  useEffect(() => {
    setPosts(postData.filter((post) => {
      const filter = searchParams.get("filter")
      const title = post.title.toLowerCase()
      return filter ? title.includes(filter) : true
    }))
  }, [searchParams])

  return (
    <div>
      <input onChange={searchInputHandler}></input>
      {posts.map((post) => {
        return (
          <p key={post.id}>
            <Link to={`/posts/${post.id}`}>{post.title}</Link>
          </p>
        )
      })}
      <Outlet/>
    </div>
  )
}

export default Posts

useEffect에서 작성된 내용이 먼저 호출되는데, filter 값이 존재하면 해당 값의 데이터값이 title에 존재하는 데이터만 posts값으로 변경하고 posts.map을 통해 해당 데이터만 렌더링하게 해주면?

Input 창에 입력한 text값을 filter 쿼리 파라미터로 지정하여 title 에 filter 값을 가지고 있는 게시글 이름만 나타나게 된다.

useLocation

현재 유저가 위치한 url값을 보여주고 싶으면 useLocation을 사용하면된다.

만약 타이틀을 클릭해서 해당 타이틀 내부에 존재하는 게시글을 볼때 전체 게시글에서 find를 진행하는데 그렇게 하지 않고 클릭하여 링크이동을 진행하면서 post 데이터를 전달할수 있다.
Posts

  {posts.map((post) => {
                return (
                    <p key={post.id}>
                        <Link to={`/posts/${post.id}`} state={{ post: posts.find((data) => data.id === post.id) }}>
                            {post.title}
                        </Link>
                    </p>
                )
            })}

이렇게 Link 태그 안에 state를 추가해서 posts에서 post.id 와 일치한 data값만 보내서 PostDetail에 post로 보내줄수 있다.

import React from 'react'
import { useLocation, useOutletContext, useParams } from 'react-router-dom'
import { postData } from './../../../constants/postData'

function PostDetail() {
    const location = useLocation()
    const { post } = location.state
    return (
        <div>
            <h3>{post.title}</h3>
            <p>{post.body}</p>
        </div>
    )
}

export default PostDetail

useLocation을 사용해서 현재 url 값에 내장된 state 값을 post로 지정해서 데이터를 렌더링 할수 있게된다.

useNavigate

주소 이동을 위한 Hook

Link 와는 다르게 함수 안에서 주소 이동 동작을 실행할 때 사용한다.

{ replace: true }
-> 이동하면서 접근 이력을 유지할지, 버릴지 선택

{ state: 넘어가면서 전달하고자 하는 데이터 }

-> 주소 이동하면서 전달하고자 하는 값을 명시

import React from 'react'
import { useLocation, useNavigate, useOutletContext, useParams } from 'react-router-dom'

function PostDetail() {
    const location = useLocation()
    console.log(location)
    const { post } = location.state
    const navigate = useNavigate()
    return (
        <div>
            <h3>{post.title}</h3>
            <p>{post.body}</p>
            <button onClick={() => navigate('/users')}>유저로 가기</button>
            <button onClick={() => navigate('/users', { replace: true })}>뒤로가기</button>
        </div>
    )
}

export default PostDetail

아래와 같은 형태도 가능하다

navigate(숫자)

-(숫자) : 뒤로 숫자만큼 가기
(숫자) : 앞으로 숫자만큼 가기

Router 부가기능

isActive라는 props를 기본적으로 가지고 있어서,
이를 토대로 지금 사용자가 접속한 Nav가 어디인지를 표시하는 과정을 도와준다.

<NavLink style={({ isActive }) => ({color: isActive ? "red" : "black"})} to="주소">
  텍스트
</NavLink>

app.jsx

<nav>
    <NavLink style={({ isActive }) => ({ color: isActive ? 'red' : 'black' })} to="/posts">
        Posts
    </NavLink>{' '}
    |{' '}
    <NavLink style={({ isActive }) => ({ color: isActive ? 'red' : 'black' })} to="/users">
        Users
    </NavLink>
</nav>

이렇게 클릭항 링크만 빨간색으로 색이 변경되는 모습을 볼수 있다.

검색한 후에 링크를 클릭했더니, 검색 파라미터가 다 사라져버린다. 만약 이것을 유지하면서 링크로 넘어가고싶으면?

아래와 같은 파일을 생성한다.
Common/QueryNavLink.jsx

import { useLocation, NavLink } from "react-router-dom";

export default function QueryNavLink({ to, ...props }) {
  let location = useLocation();
  return <NavLink to={to + location.search} {...props} />;
}

Posts/index.jsx

{posts.map((post) => {
  return (
    <p key={post.id}>
      <QueryNavLink style={({ isActive }) => ({color: isActive ? "red" : "black"})} to={`/posts/${post.id}`} state={{ post:posts.find((data) => data.id === post.id) }}>{post.title}</QueryNavLink>
    </p>
  )
})}

원래는 2만 보였었는데 filter 값도 보여주게된다.

routes 하나 더 활용해서 Modal 만들기

modal을 만드는 방법은 다양하지만
react-router는 Routes가 여러개 있을 경우 ,해당되는 element를 모두 보여주는 특징을 가진다.

아래와 같은 코드를 app.jsx에 Routes와 동등한 위치로 추가한다.

<Routes>
  <Route path="posts" element={<p style={{ position: 'absolute', border: '1px solid black', width: '10rem', height: '10rem', top: '0', backgroundColor: 'white' }}>asd</p>}/>
</Routes>

/posts로 접근하면 post 컴포넌트 위로 검은색 네모가 표시된다.

Axios와 함께 써보기

구성

  • apis → axios.js 는 기본 설정이 들어있는 axios 객체를 만들기 위한 파일이며, 나머지 파일은 각 api route 로 요청을 보내기 위한 함수가 들어있는 파일입니다.
  • components → common 폴더는 여러 페이지에서 공통적으로 사용하기 위한 컴포넌트가 들어있으며, posts 는 말그대로 posts 관련 페이지에서 공통적으로 사용하기 위한 컴포넌트가 들어있습니다.
  • pages → jsx 파일 하나가 하나의 페이지에 대응되며, posts 폴더 내에는 posts route 페이지들이 들어있습니다.
  • routes → 라우터 관련 jsx 가 들어있습니다.

Posts 페이지 수정하기

하나의 게시글을 가져오기 위한 요청함수 생성

api/post.js

export const getPost = async (id) => {
  try {
    const response = await instance.get(`/posts/${id}`)
    return response.data;
  } catch (error) {
    console.log(error);
  }
};

axios 함수를 사용하는 부분은 전부 page 컴포넌트에 작성을 실시

import React, { useEffect, useState } from 'react'
import { PostList } from '../../components/Posts'
import { getPosts } from '../../apis/posts'

function Posts() {
    const [posts, setPosts] = useState([])
    useEffect(() => {
        const fetchData = async () => {
            const data = await getPosts()
            setPosts(data)
        }
        fetchData()
    }, [])

    return (
        <div>
            <h3>Posts</h3>
            {posts && <PostList posts={posts} />}
        </div>
    )
}

export default Posts

detail 페이지에서는 useParmas로 아이디를 가져와서 요청을 보낸다.

PostDetail/index.jsx

공통 Layout 컴포넌트 만들기

보통은 header,nav,footer 등 페이지 별로 공통적으로 보여주어야 하는 요소들은 Layout 이라는 이름으로 컴포넌트를 만들어놓고 사용하는데, 이를 구현한다.

components/Common/Layout/index.jsx(Nav도 폴더/index 형태로 바꿔준다)

import React from 'react'
import { Outlet } from 'react-router-dom'
import Nav from '../Nav'

function Layout() {
    return (
        <>
            <Nav />
            <Outlet />
            <footer>푸터입니다</footer>
        </>
    )
}

export default Layout

router에서 Layout 이 항상 표시되도록 설정한다. 방금 전에 만든 Layout 컴포넌트를 Routes 태그로 생성해서 내부에 Route 내용을 전부 추가한다.

routes/Router.jsx

import React from 'react'
import { Route, Routes } from 'react-router-dom'
import Layout from '../components/Common/Layout'
import Posts from '../pages/Posts'
import PostDetail from '../pages/Posts/PostDetail'
import Users from '../pages/Users'
import UserDetail from '../pages/Users/UserDetail'

function Router() {
    return (
        <Routes>
            <Route path="/" element={<Layout />}>
                <Route path="posts" element={<Posts />} />
                <Route path="posts/:postId" element={<PostDetail />} />
                <Route path="users" element={<Users />} />
                <Route path="users/:userId" element={<UserDetail />} />
                <Route path="*" element={<p>Not Found</p>} />
            </Route>
        </Routes>
    )
}

export default Router

이렇게 해주면 app.jsx에는 nav가 필요없어지므로 제거한다.
app.jsx

import Nav from './components/Common/Nav'
import Router from './routes/Router'

function App() {
    return <Router />
}

export default App

이제 어느 페이지를 들어가조 footer와 nav를 볼수 있게 되었다.

특정 route 보호하기 (중요)

웹사이트를 이용하다보면, 로그인을 하지 않고는 들어갈수 없는 영역이 있다.

이를 구현하려면 각 페이지 컴포넌트 마다 로그인했는지 여부를 검증하는 롲기을 추가할수 있지만, 페이지가 많아진다면 너무 비효율적이다.

가장 효과적인 방법은 로그인 여부를 체크하는 Router 혹은 컴포넌트 하나를 만들어놓는것이 좋다.
routes/ProtectedReouter.jsx

import React, { useEffect } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'

function ProtectedRouter() {
    const navigate = useNavigate()
    useEffect(() => {
        if (true) { // 쿠키에 인증 정보가 있는지 확인하는 로직이 추가됩니다.
            alert('보호된 영역입니다.')
            navigate('/')
        }
    }, [])

    return (
        <>
            <Outlet />
        </>
    )
}

export default ProtectedRouter

인증 정보를 확인하는 로직만 추가하면 된다.

이제 Router 쪽에서 해당 컴포넌트를 사용한다.

import React from 'react'
import { Route, Routes } from 'react-router-dom'
import Layout from '../components/Common/Layout'
import Posts from '../pages/Posts'
import PostDetail from '../pages/Posts/PostDetail'
import Users from '../pages/Users'
import UserDetail from '../pages/Users/UserDetail'
import ProtectedRouter from './ProtectedRouter'

function Router() {
    return (
        <Routes>
            <Route path="/" element={<Layout />}>
                <Route path="posts" element={<Posts />} />
                <Route path="posts/:postId" element={<PostDetail />} />
                <Route element={<ProtectedRouter />}>
                    <Route path="users" element={<Users />} />
                    <Route path="users/:userId" element={<UserDetail />} />
                </Route>
                <Route path="*" element={<p>Not Found</p>} />
            </Route>
        </Routes>
    )
}

export default Router

이렇게하면 User 쪽 페이지들은 접근이 불가능해진다.

Lazy Loading

React에서 lazy를 쓰는 이유

리액트는 기볹거으로 번들된 파일 전체가 불러와져야 웹앱이 실행되는 구조이다. 만약 파일 자체가 굉장히 커지게 되면, 처음 앱을 불러오는 시간이 굉장히 길어진다.

그래서 이러한 부작용을 줄이고자, 불러올파일 자체를 분할해놓는것이 중요하다.

가장 기본적인 방법이 바로 dynamic import이다. 바로 import 하는 과정 자체를 필요한 경우에 import를 해오도록 동적으로 구성하는 것이다.

리액트에서는 lazy와 suspense로 이를 구현한다.

const Posts = lazy(() => import('../pages/Posts'))

위처럼 작성하면 import 하는 과정 자체가 동적으로 진행이 되고 이후에 Suspense 라는 태그로 감싸주면, 해당 컴포넌트가 불러와지기 전까지는 fallback 에 명시한 내용이 보여지고, 불러와진 후에는 해당 컴포넌트가 보여지게 된다.

로딩 애니메이션을 만든 컴포넌트를 추가해서 불러와지는 동안 애니메이션을 보여주게 설정했다.
components/Common/Loading/style.js

import styled, { keyframes } from 'styled-components'

const spin = keyframes`
0%, 100% {
    animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5);
  }
  0% {
    transform: rotateY(0deg);
  }
  50% {
    transform: rotateY(1800deg);
    animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1);
  }
  100% {
    transform: rotateY(3600deg);
  }
`

export const Loading = styled.div`
    display: inline-block;
    transform: translateZ(1px);
`

export const circle = styled.div`
    display: inline-block;
    width: 64px;
    height: 64px;
    margin: 8px;
    border-radius: 50%;
    background: #e20909;

    animation: ${spin} 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite;
`

components/Common/Loading/index.jsx

import React from 'react'
import * as S from './style'

function Loading() {
    return (
        <S.Loading>
            <S.circle />
        </S.Loading>
    )
}

export default Loading
import React, { lazy, Suspense } from 'react'
import { Route, Routes } from 'react-router-dom'
import Layout from '../components/Common/Layout'
import Loading from '../components/Common/Loading'

import PostDetail from '../pages/Posts/PostDetail'
import Users from '../pages/Users'
import UserDetail from '../pages/Users/UserDetail'
import ProtectedRouter from './ProtectedRouter'

const Posts = lazy(() => import('../pages/Posts'))

function Router() {
    return (
        <Routes>
            <Route path="/" element={<Layout />}>
                <Route
                    path="posts"
                    element={
                        <Suspense
                            fallback={
                                <h1>
                                    <Loading />
                                </h1>
                            }
                        >
                            <Posts />
                        </Suspense>
                    }
                />
                <Route path="posts/:postId" element={<PostDetail />} />
                <Route element={<ProtectedRouter />}>
                    <Route path="users" element={<Users />} />
                    <Route path="users/:userId" element={<UserDetail />} />
                </Route>
                <Route path="*" element={<p>Not Found</p>} />
            </Route>
        </Routes>
    )
}

export default Router

이후에 Suspense 라는 태그로 감싸주면, 해당 컴포넌트가 불러와지기 전까지는

fallback 에 명시한 내용이 보여지고,

불러와진 후에는 해당 컴포넌트가 보여지게 된다.

profile
개발자 꿈나무

0개의 댓글