React Router

변승훈·2022년 8월 11일
0

1. React Router

1. 개념 및 설치

react-router는 특정한 주소와 컴포넌트를 연결시켜주고, 주소 이동을 통해서 다른 컴포넌트화면을 보여줄 수 있도록 하는 기능을 제공해주는 라이브 러리다.

단일 페이지 주소 뒤에 다른 주소가 추가 되면서 작업을 진행 할 것이다.
ex) 기존 로컬 주소에서 이동: localhost:3000 -> localhost:3000/posts

설치는 아래의 명령어로 실행해 주면 된다.

yarn add react-router-dom@6

먼저 index.js에서 app을 BrowserRouter로 감싸줘야한다. 그래야 해당 app에서 브라우저 라우팅 기능을 사용할 수 있다.

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

이후에는 components 폴더를 만들고 예시로 사용할 Posts와 Users 폴더를 각각 만들어 그 안에 index.jsx파일을 만들어 주자.

폴더 구조에 관련해서, routes 폴더를 만들고 그 안에 컴포넌트 파일을 넣어주기도 하는데 이는 프로젝트 등을 진행할 때 팀 내에서 폴더 구조를 협의하고 진행을 하는 것이 좋다.

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

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

위 처럼 작성을 하게 되면, 현 주소/posts로 접근 시, Posts 컴포넌트가 보이도록 설정하게 되는 것이다.

이후 해당 페이지로 접근할 때는 a태그가 아닌 Link 태그로 이동할 수 있다.

<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;

여기서 Routes는 포함하는 주소 변경을 감지하고 일치하는 Route를 찾아서 표시해주는 역할을 한다.

3. path관련(옛날의 exact)

버전 5에서는 "/"라는 route랑 "/posts"라는 route가 각각 존재할 경우, /posts에서 그냥 / 에 지정된 것 까지 같이 보이도록 만들어져있다.
그래서 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;

위와 같은 식으로 우리가 준비하지 않은 url에 대해서는 Not Found를 표시해줄 수 있다.

4. Nested Route / Outlet

constants 폴더를 만들어 postData.js, 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"
  }
]
// 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',
  }
]

우선 Posts 페이지에서는 글의 제목만을, 글의 제목을 누르면 PostDetail페이지로 넘어가면서 내용을 보여주도록 만들어 보자.

폴더 구조는 결국
Posts
-> PostDetail
    index.jsx
index.jsx
와 같을 것이다.

PostDetail 페이지의 URL은 posts/1과 같은 형태로 이루어지게 될 것이다.
posts/1 → 1번 글 세부 내용 표시
posts/2 → 2번 글 세부 내용 표시

이러한 구조를 만들기 위해서 app.js의 posts 부분을 아래와 같이 수정할 수 있다.

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

이러한 형태를 Nested Route라고 표현을 하며, 서브 라우트라고도 표현한다.
이렇게 작성을 하게 되면 URL구조가 주소/posts/:postId와 같이 계층적으로 이어지면서 설정이 된다.
또한 Nested Route를 쓰면 Outlet이라는 것을 통해서 부모 Route의 요소를 표현하면서 동시에 하위 Route요소를 표시하게 된다.

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

모두 작성을 하게 되면 app.js는 아래와 같은 전체 형태가 나온다.

import { Link, Route, Routes } from 'react-router-dom';
import Posts from './components/Posts'
import PostDetail from './components/Posts/PostDetail';
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=":postId" element={<PostDetail/>}/>
        </Route>
        <Route path="users" element={<Users/>}/>
        <Route path="*" element={<p>Not Found</p>}/>
      </Routes>
    </div>
  );
}

export default App;

이제 Posts 컴포넌트를 수정해 보자.
Link 태그를 통해서 posts/글번호로 넘어가면, PostDetail 페이지에서 해당 글을 볼 수 있도록 해주자.

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/>
    </div>
  )
}

export default Posts

여기서 Outlet 태그를 추가하지 않는다면 PostDetail 내용으로 가지 않을 것이다. Route 구조 상 상위 컴포넌트와 하위 컴포넌트가 이어져 있기 때문이며, 지금은 아직 Posts 컴포넌트 이기 때문이다.

만약 이렇게 꼐층적으로 표시하는 것이 아닌 완전히 전환을 시키고자 한다면 아래의 코드처럼 계층에서 아예 분리 시켜 Route를 설정해 놓으면 된다.

import { Link, Route, Routes } from 'react-router-dom';
import Posts from './components/Posts'
import PostDetail from './components/Posts/PostDetail';
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="posts/:postId" element={<PostDetail/>}/>
        <Route path="users" element={<Users/>}/>
        <Route path="*" element={<p>Not Found</p>}/>
      </Routes>
    </div>
  );
}

export default App;

하지만 이렇게 설정하는 것 보다는 계층은 유지하면서 조금 다른 방식으로 Nested Route를 설정하는 것이 더 좋다.

5. Index Route

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

예시로 PostIndex 컴포넌트를 하나 만들어 보자.
위에서 만든 Posts 내부에 PostIndex폴더와 그 안에 index.jsx 파일을 만들면 된다.

Posts
-> PostDetail
    index.jsx
-> PostIndex
    index.jsx
index.jsx

아래의 코드처럼 posts Nested Route안에서 index라고 명시하면서 Route를 하나 추가해주자

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

그러면 그냥 /posts로 접근했을 때, PostIndex 컴포넌트 내용이 보이게 된다.

전체의 코드는 아래와 같다.

import { Link, Route, Routes } from 'react-router-dom';
import Posts from './components/Posts'
import PostDetail from './components/Posts/PostDetail'
import PostIndex from './components/Posts/PostIndex'
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 index element={<PostIndex/>}/>
          <Route path=":postId" element={<PostDetail/>}/>
        </Route>
        <Route path="users" element={<Users/>}/>
        <Route path="*" element={<p>Not Found</p>}/>
      </Routes>
    </div>
  );
}

export default App;

6. URL Parameter(useParams)

useParams는 예전에 match.params 등의 형태러 썼었는데, 버전 6에서 새롭게 추가된 Hook이다.

이 함수를 사용하면 URL path에 명시된 파라미터 값을 받아올 수 있다.

먼저 app.js에서 postId라는 이름으로 값을 넘기도록 설정한다.

<Route path=":postId" element={<PostDetail/>}/>
const params = useParams();
console.log(params) // {postId:~}

그렇기 때문에 params 안에는 postId라는 키로 값이 담겨있다.

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

function PostDetail() {
  const params = useParams();
  const post = postData.find((post) => post.id === parseInt(params.postId))

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

export default PostDetail

2. Router Hooks 활용하기

1. useSearchParams

useSearchParams는 쿼리 파라미터를 사용하기 더 편하게 해주는 함수이다.
기본적인 형태는 아래와 같다.

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()

  useEffect(() => {
    setSearchParams({ filter: 1 })
  }, [])

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

export default Posts

일단 input없이, 주소에 filter를 명시해서 검색하도록 구현해보자.

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)

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

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

export default Posts

이제 검색을 할 수 있게 input을 추가해보자.

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

2. useLocation

지금 현재 유저가 어떤 url 위치에 있는지를 보여주고자 한다면 useLocation을 사용하면 된다.

const location = useLocation();
console.log(location)

console.log를 보면 pathname / search / state 등이 담겨있다.

  • search : 검색값
  • pathname : 주소값
  • state : 주소 이동하면서 전달하고자 하는 값

지금 유저가 어떤 것을 검색했는지 등을 보여주고 싶다면 아래와 같이 추가해주면 된다.

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

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

  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
    }))
    console.log(location.search)
  }, [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

postData 를 또 불러오는게 비효율적인 것 같은데 Link 를 타고 이동하면서 데이터를 넘겨주는 방법은 아래와 같다.

{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 를 명시하고,

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

function PostDetail() {
  const params = useParams();
  const location = useLocation();
  const {post} = location.state

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

export default PostDetail

이렇게 받아서 표시해주면 된다.

혹시나 이상한 주소로 들어올 경우를 대비해서, 예외 처리를 해주자.

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

function PostDetail() {
  const params = useParams();
  const location = useLocation();
  const {post} = location.state ? location.state : {post:''}

  if (!post) return <p>Not Found</p>
  return (
    <div>
        {post.title}
        {post.body}
    </div>
  )
}

export default PostDetail

3. useNavigate

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

import { useNavigate } from "react-router-dom"

const navigate = useNavigate()
navigate("주소",  옵션)

옵션으로는 총 2가지가 있는데

{ replace: true }

이동하면서 접근 이력을 유지할 지, 버릴 지 선택하는 방법과

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

주소 이동하면서 전달하고자 하는 값을 명시하는 방법이다.

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

function PostDetail() {
  const params = useParams();
  const location = useLocation();
  const navigate = useNavigate();
  const {post} = location.state ? location.state : {post:''}

  if (!post) return <p>Not Found</p>
  return (
    <div>
        {post.title}
        {post.body}
        <button onClick={() => navigate("/users")}>유저로 가기</button>
        <button onClick={() => navigate("/users", {replace: true, state: {data: 1}})}>유저로 가기 (옵션)</button>
    </div>
  )
}

export default PostDetail

데이터를 받을 때는, useLocation 을 통해서 받으면 된다.

import React from 'react'
import { useLocation } from 'react-router-dom';
import {userData} from '../../constants/userData'

function Users() {
  const location = useLocation();

  return (
    <div>
      Users
      {location.state && location.state.data}
    </div>
  )
}

export default Users

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

navigate(숫자)
import React from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom';

function PostDetail() {
  const params = useParams();
  const location = useLocation();
  const navigate = useNavigate();
  const {post} = location.state ? location.state : {post:''}

  if (!post) return <p>Not Found</p>
  return (
    <div>
        {post.title}
        {post.body}
        <button onClick={() => navigate(-2)}>뒤로 2번가기</button>
        <button onClick={() => navigate(-1)}>뒤로 1번가기</button>
        <button onClick={() => navigate(1)}>앞으로 1번가기</button>
        <button onClick={() => navigate(2)}>앞으로 2번가기</button>
    </div>
  )
}

export default PostDetail

3. Router 부가기능

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

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

검색한 후에 링크를 클릭했더니, 검색 파라미터가 다 사라져버린다.

이것을 유지하면서 링크로 넘어가고자 한다면 어떻게 해야 할까?

아래와 같은 새로운 파일을 만들어주자.

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

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

이제 해당 컴포넌트를 불러와서, 원래 NavLink 부분에 아래처럼 넣어주자.

{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>
  )
})}

이제 검색을 하고, 글의 세부 내용을 보아도 검색 파라미터가 사라지지 않게 된다.

3. routes 하나 더 활용하여 Modal만들기

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

4. Lazy Loading

React 에서 lazy 를 쓰는 이유

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

그래서 이러한 부작용을 줄이고자, 불러올 파일 자체를 분할해놓는 것이 중요한데, 가장 기본적인 방법이 바로 dynamic import 이다.

바로 import 하는 과정 자체를 필요한 경우에 import 를 해오도록 동적으로 구성하는 것이다.

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

const Posts = lazy(() => import("./components/Posts"))

위처럼 작성하면 import 하는 과정 자체가 동적으로 진행이 되고,

<Route path="posts" element={<Suspense fallback=		{<h1>Loading...</h1>}><Posts/></Suspense>}>
      <Route index element={<PostIndex/>}/>
      <Route path=":postId" element={<PostDetail/>}/>
</Route>

이후에 Suspense 라는 태그로 감싸주면, 해당 컴포넌트가 불러와지기 전까지는 fallback 에 명시한 내용이 보여지고, 불러와진 후에는 해당 컴포넌트가 보여지게 된다.

profile
잘 할 수 있는 개발자가 되기 위하여

0개의 댓글