[포스코x코딩온] KDT-Web-8 13주차 회고1 react-router-dom

Yunes·2023년 9월 26일
0

[포스코x코딩온]

목록 보기
33/47
post-thumbnail

서론

SPA 인 React 에서 화면을 전환하고 url 을 변경하는데 라우터를 사용한다. 이때 상태값이라던지 유지해야 하는 값이 있는데 a 태그로 전환을 하면 새로고침이 되기에 react-router-dom 의 Link 태그를 사용하곤 했었다.

그런데 정작 react-router-dom 에 대해 잘 모르던 찰나 오늘의 수업에서 대략적으로 이를 사용해서 client side routimg 을 어떻게 할 수 있는지 알게 되었기에 잊지 않기 위해 정리하고자 한다.

추가로 개인적으로 새로운 기술을 접하게 될때 공식 docs 를 자주 들여다보는 편인데 React Router docs 는 검색 기능이 없고 비교적 다른 사이트의 docs 들보다 불친절하다는 생각이 강하게 들었다. 예시 코드도 ts 관점에서 타입에 대해 조금더 강조하고 예시코드를 이것도 첨부해줄까? 아님말고! 이런 식으로 코드를 참고하기 어려운 점이 아쉬웠다.

Client Side Routing 이란?

React Routerclient side routing 을 가능하게 한다.

전통적인 웹사이트 에서 브라우저는 웹 서버로부터 문서를 요청하고 CSS 와 JavaScript 파일을 다운로드하고 평가하여 서버로부터 보내진 HTML 을 렌더링한다.

그런데 유저가 링크를 클릭하면 새 페이지에 대해 이 모든 과정을 반복한다.

Client side routing 은 서버로부터 또다른 문서에 대한 요청을 하지 않고 링크를 클릭하는것으로 부터 url 을 업데이트 할 수 있도록 한다.

그 대신 앱은 새로운 UI를 즉시 렌더링할 수 있고 요청을 통해 데이터를 가져와서 새 정보로 페이지를 업데이트할 수 있게 한다.

이는 브라우저가 다음 페이지를 위해 새로운 문서를 전부 요청할 필요도 없고 CSS 와 JavaScript 파일을 다시 읽어들일 필요도 없기에 더 향상된, 빠른 UX (user experience) 를 가능하게 한다. 또한 이는 애니메이션 같이 더 동적인 UX 를 가능하게 한다.

client side routingLink 컴포넌트와 Form 컴포넌트를 통해 페이지에 접속하거나 제출하기 위해 Router 를 생성하는 것을 통해 동작할 수 있다.

// Link 컴포넌트 예시
import * as React from "react";
import { createRoot } from "react-dom/client";
import {
  createBrowserRouter,
  RouterProvider,
  Route,
  Link,
} from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: (
      <div>
        <h1>Hello World</h1>
        <Link to="about">About Us</Link>
      </div>
    ),
  },
  {
    path: "about",
    element: <div>About</div>,
  },
]);

createRoot(document.getElementById("root")).render(
  <RouterProvider router={router} />
);

// Form 컴포넌트 예시
import { Form } from "react-router-dom";

function NewEvent() {
  return (
    <Form method="post" action="/events">
      <input type="text" name="title" />
      <input type="text" name="description" />
      <button type="submit">Create</button>
    </Form>
  );
}

위의 코드처럼 createBrowerRouter 를 통해 router 를 만들어주고 루트를 RouterProvider 로 감싸 props 로 앞에서 생성한 router 를 전달하는 것을 통해 client side routing 을 가능하게 한다.

Nested Routes

중첩 라우팅 은 URL 조각을 컴포넌트 계층 구조 및 데이터에 연결하는 일반적인 아이디어이다.

이 React Router 의 중첩구조는 2014 년 Ember.js circa 의 라우팅 구조로부터 영감을 받은것인데 Ember 팀은 거의 모든 경우에 URL 조각페이지에 렌더링할 레이아웃 과 그 레이아웃들의 데이터 의존성을 결정 한다는 사실을 알게되었다.

React Router 는 URL segmant 및 데이터와 결합된 중첩구졸르 생성하기 위한 API 로 이 중첩 라우팅 규칙을 수용했다.

// Configure nested routes with JSX
createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route path="contact" element={<Contact />} />
      <Route
        path="dashboard"
        element={<Dashboard />}
        loader={({ request }) =>
          fetch("/api/dashboard.json", {
            signal: request.signal,
          })
        }
      />
      <Route element={<AuthLayout />}>
        <Route
          path="login"
          element={<Login />}
          loader={redirectIfUser}
        />
        <Route path="logout" action={logoutUser} />
      </Route>
    </Route>
  )
);

// Or use plain objects
createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "contact",
        element: <Contact />,
      },
      {
        path: "dashboard",
        element: <Dashboard />,
        loader: ({ request }) =>
          fetch("/api/dashboard.json", {
            signal: request.signal,
          }),
      },
      {
        element: <AuthLayout />,
        children: [
          {
            path: "login",
            element: <Login />,
            loader: redirectIfUser,
          },
          {
            path: "logout",
            action: logoutUser,
          },
        ],
      },
    ],
  },
]);

위의 예시코드에서 두번째 plain objects 를 사용하는 사례로 실습을 진행했었는데 이렇게 비교해볼때 Nested Routes 를 아는데 두번째 plain object 를 사용하는 코드가 좀더 이해가 잘 되는것 같다.

그리고 docs 를 읽다보니 점점 ... 어라? 설명 잘해뒀네? 라는 생각이 든다. 지금까지 읽어봤던 docs 중 recoil 은 한글도 지원이되고 예시 코드도 한눈에 이해도 잘 돼서 가장 이해하기 좋은 docs 였는데 React Route docs 는 여전히 검색기능좀 넣어줬으면 하는 생각이 들긴해도 설명은 잘 해둔것 같다.

Nested Routes 에 대해 이해가 잘 되라고 이런 페이지도 보여주고 있었는데 각 계층구조에서 불러들일 컴포넌트들이 따로 존재하고 이걸 위의 예시코드와 같이 라우팅하면 되겠다는 생각이 들었다.

Dynamic Segments

URL segment 는 파싱되고 다양한 API 에 제공될 수 있는 동적 표시자가 될 수 있다.

<Route path="projects/:projectId/tasks/:taskId" />

위처럼 params 가 들어가는 경우를 말하는 것으로 보인다.

// If the current location is /projects/abc/tasks/3
<Route
  // sent to loaders
  loader={({ params }) => {
    params.projectId; // abc
    params.taskId; // 3
  }}
  // and actions
  action={({ params }) => {
    params.projectId; // abc
    params.taskId; // 3
  }}
  element={<Task />}
/>;

function Task() {
  // returned from `useParams`
  const params = useParams();
  params.projectId; // abc
  params.taskId; // 3
}

function Random() {
  const match = useMatch(
    "/projects/:projectId/tasks/:taskId"
  );
  match.params.projectId; // abc
  match.params.taskId; // 3
}

오.. 예시코드가 볼수록 찰떡이다.

여기서 알아야할 개념들을 살펴보자.

Route path

path 는 URL, 링크 href 혹은 form action 과 경로가 일치하는지 확인하기 위한 경로 패턴이다.

<Route
  // this path will match URLs like
  // - /teams/hotspur
  // - /teams/real
  path="/teams/:teamId"
  // the matching param will be available to the loader
  loader={({ params }) => {
    console.log(params.teamId); // "hotspur"
  }}
  // and the action
  action={({ params }) => {}}
  element={<Team />}
/>;

Splats

  • catchall 혹은 star segment 로도 불려져있는데 만약 path pattern 이 /* 로 쯭나면 / 뒤에 어떤 문자가 오든 매칭이 된다.

Route loader

route 는 렌더링되기 전에 루트 요소노드에 데이터를 제공하기 위해 loader 함수를 정의할 수 있다.

단, 이 기능은 data router 를 사용할때만 동작한다.

createBrowserRouter([
  {
    element: <Teams />,
    path: "teams",
    loader: async () => {
      return fakeDb.from("teams").select("*");
    },
    children: [
      {
        element: <Team />,
        path: ":teamId",
        loader: async ({ params }) => {
          return fetch(`/api/teams/${params.teamId}.json`);
        },
      },
    ],
  },
]);

loader 를 통해 리턴한 값은 useLoaderData 훅을 통해 가져올 수 있다.

import {
  createBrowserRouter,
  RouterProvider,
  useLoaderData,
} from "react-router-dom";

function loader() {
  return fetchFakeAlbums();
}

export function Albums() {
  const albums = useLoaderData();
  // ...
}

const router = createBrowserRouter([
  {
    path: "/",
    loader: loader,
    element: <Albums />,
  },
]);

ReactDOM.createRoot(el).render(
  <RouterProvider router={router} />
);

route 의 action 이 호출된 이후에 데이터는 자동으로 재평가될 것이고 loader 로부터 가장 최신 결과를 반환될 것이다.

주의
useLoaderData 는 fetch 를 유발하지 않고 그저 내부적으로 React Router 의 결과를 읽기만 해서 이 훅이 refetching 하고 재렌더링을 유발하게 될 거라고 걱정할 필요가 없다. 그래서 useEffect 훅의 dependency array 에 넣어도 상관없다. useLoaderData 는 action 이나 특정 navigation 이후에 loader 가 다시 호출될때만 변한다.

docs 를 쭉 읽다가 유용한 코드가 보여서 관련 섹션에 추가했다.

<Route
  path="/"
  loader={async ({ request }) => {
    // loaders can be async functions
    const res = await fetch("/api/user.json", {
      signal: request.signal,
    });
    const user = await res.json();
    return user;
  }}
  element={<Root />}
>
  <Route
    path=":teamId"
    // loaders understand Fetch Responses and will automatically
    // unwrap the res.json(), so you can simply return a fetch
    loader={({ params }) => {
      return fetch(`/api/teams/${params.teamId}`);
    }}
    element={<Team />}
  >
    <Route
      path=":gameId"
      loader={({ params }) => {
        // of course you can use any data store
        return fakeSdk.getTeam(params.gameId);
      }}
      element={<Game />}
    />
  </Route>
</Route>
function Root() {
  const user = useLoaderData();
  // data from <Route path="/">
}

function Team() {
  const team = useLoaderData();
  // data from <Route path=":teamId">
}

function Game() {
  const game = useLoaderData();
  // data from <Route path=":gameId">
}

Route action

와.. 액션 설명 하나도 이해가 안돼서 한참동안 노려보고 자료를 찾아봤다.

Route actions are the "writes" to route loader "reads". They provide a way for apps to perform data mutations with simple HTML and HTTP semantics while React Router abstracts away the complexity of asynchronous UI and revalidation. This gives you the simple mental model of HTML + HTTP (where the browser handles the asynchrony and revalidation) with the behavior and UX capabilities of modern SPAs.

이런 내용인데 나는 action 이 라우팅된 페이지에서 어떤 동작을 수행할 수 있는 방법을 제공하며 비동기 UI 와 revalidation 같은 복잡한 작업을 추상화해서 이런 측면을 신경쓰지 않고도 simple mental model 을 제공해서 모던 SPA 와 같은 동작과 UX 능력을 보여준다고 이해했다. 또한 액션은 이런 추상화를 하는 동안 간단한 HTML 과 HTML semantic 태그를 사용해서 데이터 mutation 을 ( 데이터 가공 및 변형 ) 할 수 있도록 한다.

그러니까 action 은 비동기 작업을 추상화해주기에 기존 HTML 을 사용하는 것과 같은 방식으로 모던 SPA 가 보여주는 높은 수준의 동작과 UX 를 가능하게 해주는 기능인 것 같다.

<Route
  path="/song/:songId/edit"
  element={<EditSong />}
  action={async ({ params, request }) => {
    let formData = await request.formData();
    return fakeUpdateSong(params.songId, formData);
  }}
  loader={({ params }) => {
    return fakeGetSong(params.songId);
  }}
/>

주의
action 은 get 을 제외한 post, put, patch, delete 에서 호출된다.

// forms
<Form method="post" action="/songs" />;
<fetcher.Form method="put" action="/songs/123/edit" />;

// imperative submissions
let submit = useSubmit();
submit(data, {
  method: "delete",
  action: "/songs/123",
});
fetcher.submit(data, {
  method: "patch",
  action: "/songs/123/edit",
});

useParams

수업시간에 처음 알게된 훅인데 param 을 추출해낼 수 있다. useParams 훅은 현재 URL 로부터 Route path 에 일치하는 동적 params 로부터 key / value 쌍의 객체 를 반환받는다. 그래서 아래의 예시코드에서 객체의 구조분해 할당을 사용하는 것이다.

import * as React from 'react';
import { Routes, Route, useParams } from 'react-router-dom';

function ProfilePage() {
  // Get the userId param from the URL.
  let { userId } = useParams();
  // ...
}

function App() {
  return (
    <Routes>
      <Route path="users">
        <Route path=":userId" element={<ProfilePage />} />
        <Route path="me" element={...} />
      </Route>
    </Routes>
  );
}

Ranked Route Matching

다음의 두 route 코드를 보자. 만약 url 이 http://example.com/teams/new. 라면 어떤 route 에 연결되어야 할까?

<Route path="/teams/:teamId" />
<Route path="/teams/new" />

React Router 는 segment, 정적 segment(user), 동적 segment (:userid), splats (/*) 등의 수에 따라 rank 를 정하고 가장 비슷한 것을 고른다.

url 이 두 route path 에 동시에 일치하기는 하나 직관적으로 위의 url 이 두번째 route 를 위한 것임은 알 수 있다. React Route 알고리즘도 그걸 알고 있으니 route 순서에 대해 걱정할 필요는 없다.

Active Links

대부분의 웹 앱은 navigation section 을 UI 상단, 사이드바 혹은 다수의 단계에서 갖고 있다. NavLink 를 사용해서 활성화된 navigation item 을 스타일링해서 사용자가 그들이 활성화 되어있는지 혹은 pending 중인지 알 수 있게 할 수 있다.

NavLink

  • 특별한 Link 로 active, pending 여부를 알 수 있다.
import { NavLink } from "react-router-dom";

<NavLink
  to="/messages"
  className={({ isActive, isPending }) =>
    isPending ? "pending" : isActive ? "active" : ""
  }
>
  Messages
</NavLink>;

<NavLink
  style={({ isActive, isPending }) => {
    return {
      color: isActive ? "red" : "inherit",
    };
  }}
  className={({ isActive, isPending }) => {
    return isActive ? "active" : isPending ? "pending" : "";
  }}
/>

Relative Links

HTML 의 a href 처럼 Link to, NavLink to 도 향상된 동작과 중첩된 routes 와 함께 relative path 를 가질 수 있다.

<Route path="home" element={<Home />}>
  <Route path="project/:projectId" element={<Project />}>
    <Route path=":taskId" element={<Task />} />
  </Route>
</Route>

위의 코드에서 url 이 만약 https://example.com/home/project/123 였다면 이는 계층구조의 컴포넌트를 렌더링한다.

<Home>
  <Project />
</Home>

Redirects

데이터를 로딩하거나 바꿀 때 사용자를 다른 route 로 redirect 하는 것은 흔한 일이다.

<Route
  path="dashboard"
  loader={async () => {
    const user = await fake.getUser();
    if (!user) {
      // if you know you can't render the route, you can
      // throw a redirect to stop executing code here,
      // sending the user to a new route
      throw redirect("/login");
    }

    // otherwise continue
    const stats = await fake.getDashboardStats();
    return { user, stats };
  }}
/>

<Route
  path="project/new"
  action={async ({ request }) => {
    const data = await request.formData();
    const newProject = await createProject(data);
    // it's common to redirect after actions complete,
    // sending the user to the new record
    return redirect(`/projects/${newProject.id}`);
  }}
/>
import { redirect } from "react-router-dom";

const loader = async () => {
  const user = await getUser();
  if (!user) {
    return redirect("/login");
  }
  return null;
};

데이터의 응답을 통해 redirect 를 할때는 컴포넌트에서 useNavigate 훅을 사용하는 것보다 loader 와 action 에서 redirect 할 것을 권장한다.

Loader 에서 에러 던지기

실습중 errorElement 의 동작을 위해 일부러 문법상 오류가 나도록 코드를 작성했었는데 loader 에서도 오류를 던질 수 있었다.

function loader({ request, params }) {
  const res = await fetch(`/api/properties/${params.id}`);
  if (res.status === 404) {
    throw new Response("Not Found", { status: 404 });
  }
  return res.json();
}

useNavigate

useNavigate 훅의 페이지에도 이 훅을 사용하기 보다 loader 와 action 에서 redirect 하는 것이 더 좋다고 명시되어 있다.

왜 그런지 구체적으로 설명이 되어 있지는 않았는데 자료를 찾기 어려워 gpt 에 물어봤을때 컴포넌트 의 처음에 useNavigate 훅을 사용하면 작업 순서를 예측하기 어려워질 수 있으니 렌더링한 다음 필요한 경우 loader 및 action 에서 redirect 를 처리하는 것이 더 깔끔하고 예측가능하며 유지 및 관리하기 쉽다는 답변을 받았다.

오.. stack overflow 에서 답을 찾은 것 같다.

A simple thing worth pointing out if you're not familiar with React Router v6: "loaders and actions" are features of Data Routers, which you can learn about in the docs. I found these terms late in my dev cycle on useNavigate's doc page, where it merely says "It's usually better to use redirect in loaders and actions than [useNavigate]." Trying to figure why redirect is better led me here, yet no one points this out for noobs.

나처럼 누군가도 redirect 와 useNavigate 에 대한 글을 읽고 왜 그런지 설명을 제대로 안하고 있다는 것에 불만을 표출했는데 답글에 해석이 잘 나와 있었다.

import React from 'react';
import useStore from '../../../store/store';
import { useEffect } from 'react';
import {useNavigate,redirect } from 'react-router-dom';


export default function (SpecificComponent, option, adminRoute = null) {

    const navigate = useNavigate();
    const {auth,accessToken}= useStore();
// option = null // pages taht anyone can access
// option = true // pages that only loginuser can access
// option = false // pages that loginuser can not access
// option = true ,adiminRoute = true // pages that only admin can access
function AuthenticationCheck(props){

    useEffect(()=>{
        auth(accessToken).then(res => {
            
            if (res.accessToken) {
             
            setAccessToken(res.accessToken)
             
            }
        //when authentication failed
        if (res.isAuth===false) {
            if (option) {
                //to sign in page
                navigate('/signin')
            }
            //when authentication successed
        } else { 
            //trying to get in admin page but user is not admin 
            if (adminRoute && res.role !== "Admin") {
                navigate('/landingpage')
                //trying to get in admin page and user is  admin  
            } else if (adminRoute && res.role === "Admin") {
                navigate('/admin')
            } else { 
                //trying to get in login page but user is signed in already
                if (option === false) {navigate('/landing')}
                
            }

        }
        //passed above, go to SpecificComponent
    });

},[])
return(
    <SpecificComponent {...props}  />
 )
  }


  return AuthenticationCheck
}

해당 글의 요점은 useEffect 에 useNavigate 를 사용시 무한 루프에 빠졌는데 redirect 를 사용하니 해결이 되었다. 그런데 docs 를 봐도 useNavigate 대신 redirect 를 사용하라는 말만 나와있고 나의 문제가 해결된 이유를 모르겠다 - 는 것이다.

그에 대한 누군가가 이유를 설명해줬는데 useNavigate 는 함수를 반환하는 고차함수이고 redirect 는 응답 객체를 반환하는 함수라는 것이다. 함수는 매 렌더링마다 주소값이 바뀌어서 함수가 바뀌지 않더라도 주소가 변경되어 useEffect 에 함수를 전달하면 특별히 바뀐게 없더라도 재렌더링을 유발해서 무한루프에 빠질 수 있다. 그런데 redirect 는 response 객체를 반환하며 이 경우 useEffect 훅이 다시 동작하지 않아서 redirect 가 위와 같이 동작한 것이라는 설명이었다. 만약 depenency array 에 useNavigate 혹은 redirect 의 반환값이 들어가 있었다면 둘다 같이 무한루프에 빠졌을 것 같은데 useEffect 의 effect 에만 들어가 있어서 그런 차이가 발생하지 않았나 싶었다.


아무튼 다시 돌아와서 useNavigate 훅은 프로그래밍 방식으로 navigate 할 수 있는 함수를 반환하는 훅이다.

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

function useLogoutTimer() {
  const userIsInactive = useFakeInactiveUser();
  const navigate = useNavigate();

  useEffect(() => {
    if (userIsInactive) {
      fake.logout();
      navigate("/session-timed-out");
    }
  }, [userIsInactive]);
}

navigate 함수는 두가지 signature 를 가지는데 Link to 에 전달되는 것처럼 to 에 전달될 값을 인자로 받을 수 있고 ( 위의 예시코드 ) 혹은 delta 값을 전달해서 history stack 상에 원하는 곳으로 갈 수 있다. 예를 들어 -1 을 전달하면 뒤로가기와 같다.

useNavigate 는 특정한 코드의 실행이 끝나고 나서 페이지를 이동시키고 싶을 때 사용하고 Navigate 는 특정 경로에서 렌더링 시점에 다른 페이지로 이동시키고 싶을 때 사용한다. 다음 velog 글을 참고하자.

만약 replace: true 옵션을 주면 history stack 에서 새로 추가하는 것 대신 현재 entry 로 교체하기에 뒤로갈 수 없게 된다.

history stack : 유저가 navigate 할때 브라우저가 각 location 을 stack 에 유지한다. 만약 뒤로가기 버튼을 클릭하면 브라우저의 history stack 을 볼 수 있다.

Pending Navigation UI

사용자가 앱에서 navigate 할 때 다음 페이지가 렌더링되기 전에 다음 페이지에 필요한 데이터가 로드된다. 이렇게 로드될 때 앱이 반응하지 않다고 느껴지지 않도록 하기 위해 user feedback 을 제공하는 것은 중요하다. 이때 useNavigation 을 사용한다.

function Root() {
  const navigation = useNavigation();
  return (
    <div>
      {navigation.state === "loading" && <GlobalSpinner />}
      <FakeSidebar />
      <Outlet />
      <FakeFooter />
    </div>
  );
}

useNavigation 훅은 페이지 이동에 대한 정보 ( 보류중인 페이지 이동 및 데이터 변형에 따른 optimistic UI 등을 구현하기 위한 정보 ) 를 제공한다.

두가지 방식의 Router 사용법

BrowserRouter

루트 컴포넌트를 BrowserRouter 로 감싸고 그 내부에서 여러 그룹의 Routes 와 각 Routes 내에 Route 를 통해 라우팅을 관리하는 방법이다.

import * as React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";

const root = createRoot(document.getElementById("root"));

root.render(
  <BrowserRouter>
    {/* The rest of your app goes here */}
  </BrowserRouter>
);

basename

basename 을 지정하면 url 에서 해당 basename 에 이어 다음 path 가 붙은 형태로 동작한다.

function App() {
  return (
    <BrowserRouter basename="/app">
      <Routes>
        <Route path="/" /> {/* 👈 Renders at /app/ */}
      </Routes>
    </BrowserRouter>
  );
}

createBrowserRouter

앞에서 계속 소개하던 코드에서 사용했던 방식이다. React Router 는 이 라우터를 모든 React Router web project 에서 사용할 것을 추천하고 있다. 이를 사용해야 loader, action, fetcher 같이 v6.4 의 data API 를 사용할 수 있다.

import * as React from "react";
import * as ReactDOM from "react-dom";
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";

import Root, { rootLoader } from "./routes/root";
import Team, { teamLoader } <from "./routes/team";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    children: [
      {
        path: "team",
        element: <Team />,
        loader: teamLoader,
      },
    ],
  },
]);

ReactDOM.createRoot(document.getElementById("root")).render(
  <RouterProvider router={router} />
);

Outlet

위의 코드에서 루트 컴포넌트의 자식컴포넌트로 Team 을 사용할 거라고 코드를 짜뒀는데 이때 Root 컴포넌트에서 Outlet 컴포넌트를 사용하여 Team 컴포넌트를 사용할 수 있게 한다. 이때 context 를 전달하여 마치 props 를 전달하는 것처럼 데이터를 전달할 수 있다.

Outlet 은 자식 라우트 요소를 렌더링하기 위해 부모 라우트 요소에서 사용되어야 한다.

Outlet 은 자식 라우트가 렌더링될때 중첩된 UI 를 보여주게 한다. 만약 부모 라우트가 제대로 일치한다면 자식 인덱스 라우트를 렌더링할 것이다.

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* This element will render either <DashboardMessages> when the URL is
          "/messages", <DashboardTasks> at "/tasks", or null if it is "/"
      */}
      <Outlet />
    </div>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />}>
        <Route
          path="messages"
          element={<DashboardMessages />}
        />
        <Route path="tasks" element={<DashboardTasks />} />
      </Route>
    </Routes>
  );
}

앞에서 언급한 것처럼 context 를 통해 데이터를 자식 컴포넌트 children 전체에 보낼 수 있고 ( 자손 컴포넌트가 아니다. 1단계 depth 로만 전달한다. ) 자식 컴포넌트에선 useOutletContext() 훅을 통해 해당 데이터를 사용할 수 있다.

function Parent() {
  const [count, setCount] = React.useState(0);
  return <Outlet context={[count, setCount]} />;
}
import { useOutletContext } from "react-router-dom";

function Child() {
  const [count, setCount] = useOutletContext();
  const increment = () => setCount((c) => c + 1);
  return <button onClick={increment}>{count}</button>;
}

plus

useParams

앞에서 정리하긴 했는데 이해를 더 잘 하기위해 다시 요약했다.

useParams 훅은 Routh path 에 일치하는 현재 URL 로부터 동적 params 의 key / value 쌍의 객체를 반환한다.

import * as React from 'react';
import { Routes, Route, useParams } from 'react-router-dom';

function ProfilePage() {
  // Get the userId param from the URL.
  let { userId } = useParams();
  // ...
}

function App() {
  return (
    <Routes>
      <Route path="users">
        <Route path=":userId" element={<ProfilePage />} />
        <Route path="me" element={...} />
      </Route>
    </Routes>
  );
}

useSearchParams

useSearchParams 훅은 현재 location 의 URL 에서 query string 을 읽고 수정하기 위해 사용된다. React 의 useState 훅처럼 searchParams, setSearchParams 값을 갖는 배열을 반환한다.

import * as React from "react";
import { useSearchParams } from "react-router-dom";

function App() {
  let [searchParams, setSearchParams] = useSearchParams();

  function handleSubmit(event) {
    event.preventDefault();
    // The serialize function here would be responsible for
    // creating an object of { key: value } pairs from the
    // fields in the form that make up the query.
    let params = serializeFormQuery(event.target);
    setSearchParams(params);
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>{/* ... */}</form>
    </div>
  );
}

결론

양이 너무 많아서 한번에 다 정리할 수 없고 여러 편으로 나누어서 React Router 에 대해 알아봐야겠다.

React Router 는 React Native 에서도 사용했었다. 그런데 제대로 알지 못하고 navigation 하는데 React Router 라는걸 사용한다고만 알고 제대로 알아보지 않았었다... 그때의 기억 때문이라도 지금은 내가 사용하는 기술에 대해서는 왜 사용하고 어떻게 쓰는 것인지 좀더 제대로 알아가고자 노력하고 있다.

React Router 에 훅이 정말 많다. 나는 이전엔 BrowserRouter 를 사용하는 방식으로 React Router 를 사용했었는데 v6.4 의 createBrowserRouter 가 은근히 편리한 기능들을 많이 제공해주는 것 같다.

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글