본문 바로가기

FrontEnd

리액트 라우터 v6(React Router v6) 딥 다이브

반응형

https://reactrouter.com/docs/en/v6/getting-started/concepts#main-concepts

 

React Router | Main Concepts

Declarative routing for React apps at any scale

reactrouter.com

 

요즘은 대부분의 회사들이 CRA 대신 next.js를 디폴트로 사용하는것 같지만,

클라이언트 사이드 렌더링 기반 프로젝트를 제로부터 구축해야 하는 경우, 라우팅은 필수다.

직접 구축할 수도 있지만, (useEffect로 간단하게 할 수 있음... 과연 간단할까? 좀비 차일드 문제를 검색해보자) 대부분 리액트 라우터를 사용한다.

 

리액트 라우터는 6 버전부터, <Outlet/>이라는 컴포넌트와 함께, 컴포넌트 트리의 레이아웃에 직접적으로 관여한다.

(그리고 개발자는 Remix.js를 개발하러 갔다는...)

이 개념을 그냥 엔트리 포인트 정도로 사용하는 사람들도 꽤 있는것 같다만, 잘 사용하면 꽤 강력한 효과를 발휘한다.

나 또한 리액트 라우터 6로 신규 프로젝트를 시작하는 겸, 메인 컨셉을 복습해는 차원에서 공식 문서를 한번 정리하고 가기로 했다.

TL;DR 

  • BrowserRouter에는 history객체가 있다. 해당 객체는 location, action state를 통해 url 변화에 반응한다.
  • Route는 해당 path에서 렌더링할 엘리먼트에 대한 정보를 담고있는 객체다.
  • Routes Route 트리인 route config를 이용하여, 해당 경로와 match하는 Route 객체 배열인 matches를 만든다.
    • 그리고 해당 matches에 포함된 정보로 엘리먼트를 렌더링하여 컴포넌트 트리를 만든다.
  • index routeslayout routes를 잘 활용하면 공수를 덜 수 있다.

Main Concepts

이 문서는 React Router에 구현된 라우팅의 핵심 개념에 대해 자세히 설명합니다.
꽤 길기 때문에 보다 실용적인 가이드를 찾고 있다면 quick start tutorial을 확인하십시오.

 

React Router가 정확히 무슨 일을 하는지 궁금할 것입니다.

이 문서에는 React Router에 구현된 라우팅 이면의 모든 핵심 개념에 대한 자세한 설명이 포함되어 있습니다.

일상적인 케이스의 React Router 사용법은 매우 간단합니다.
굳이 이렇게 깊게 들어갈 필요는 없습니다.
 
React Router는 단순히 URL을 함수나 컴포넌트에 일치시키는 것이 아닙니다.
URL에 매핑되는 전체 사용자 인터페이스를 구축하는 것이므로 생각한 것보다 더 많은 개념을 포함할 수 있습니다.
React Router의 세 가지 주요 작업에 대해 자세히 설명합니다.
    • 히스토리 스택 구독 및 조작
    • URL을 경로에 일치시키기
    • 경로 일치(route matches)를 통한 중첩된 UI 렌더링

Definitions

그러나 먼저, 몇 가지 정의가 필요합니다.

백 엔드 프레임워크와 프런트 엔드 프레임워크의 라우팅 개념은 약간 다를 수 있습니다.

즉, 문맥에 따라 다른 의미를 가질 수 있습니다.

 

다음은 React Router에 대해 이야기할 때 많이 사용하는 단어입니다.

이 가이드의 나머지 부분에서 각각에 대해 더 자세히 설명합니다.

  • URL - 주소 표시줄의 URL입니다. 많은 사람들이 "URL"과 "route(경로)"라는 용어를 같은 의미로 사용하지만 이것은 React Router의 경로가 아니라 URL일 뿐입니다.
  • Location - 브라우저의 window.location 객체를 기반으로 하는 React Router 객체입니다.
    • "사용자가 있는 위치"를 나타냅니다. 그것은 대개 URL의 객체 표현이지만 그보다 조금 더 많은 정보를 갖고 있습니다.
  • Location State
    • URL에 인코딩되지 않는, location에서 지속되는 값입니다.
    • 해시 또는 검색 매개변수(search param - URL에 인코딩된 데이터)와 비슷하지만, 브라우저의 메모리에 보이지 않게 저장됩니다.
  • History Stack
    • 사용자가 웹을, 탐색할 때 브라우저는 스택 안의 location을 추적합니다. 브라우저에서 뒤로 버튼을 길게 클릭하면 브라우저의 히스토리 스택을 바로 볼 수 있습니다.
  • Client Side Routing (CSR)
    • 평범한 HTML 문서는 다른 문서에 링크할 수 있으며, 브라우저는 히스토리 스택 자체를 처리합니다.
    • 클라이언트 측 라우팅을 사용하면 개발자가 서버에 문서를 요청하지 않고도 브라우저 히스토리 스택을 조작할 수 있습니다.
  • History
    • React Router가 URL의 변경 사항을 구독할 수 있게 하며, 프로그래밍으로 브라우저 히스토리 스택을 조작하기 위한 API를 제공하는 객체입니다.
  • History Action
    • POP, PUSH 또는 REPLACE 중 하나입니다.
    • 사용자는 다음 세 가지 이유 중 하나로 URL에 도달할 수 있습니다.
      • PUSH : 새 항목이 기록 스택에 추가(일반적으로 링크 클릭 또는 프로그래머 강제 탐색).
      • Replace : 새 항목을 푸시하는 대신 스택의 현재 항목을 교체합니다.
      • POP : 사용자가 브라우저 크롬에서 뒤로 또는 앞으로 버튼을 클릭하면 POP이 발생합니다.
    • 히스토리 스택에 쌓이는 것은 location 객체입니다.
  • Segment
    • / 문자 사이의 URL 또는 경로 패턴(path pattern) 부분. 예를 들어 "/users/123"에는 두 개의 세그먼트가 있습니다.
  • Path Pattern
    • URL처럼 보이지만 동적 세그먼트("/users/:userId") 또는 별표 세그먼트("/docs/*")와 같이 URL을 경로에 일치시키기 위한 특수 문자가 있을 수 있습니다.
    • URL이 아니라 React Router가 일치시킬 패턴입니다.
  • Dynamic Segment
    • 동적 경로 패턴의 세그먼트로, 세그먼트의 모든 값과 일치할 수 있습니다.
    • 예를 들어 /users/:userId 패턴은 /users/123과 같은 URL과 일치합니다.
  • URL Params
    • 동적 세그먼트와 일치하는 URL의 구문 분석된 값입니다.
      • ex) if /users/:userId and /users/123 then URL params is 123
  • Router
    • 다른 모든 컴포넌트와 훅이 동작하도록 하는 stateful한 최상위 컴포넌트 입니다.
  • Router Config
    • 경로 일치(route match)의 분기를 생성하기 위해 현재 location에 대해 순위를 매기고 일치(중첩 포함)할 Route 객체 트리입니다.
  • Route
    • 일반적으로 { path, element } 또는 <Routh path element/>의 모양을 가진 객체 또는 Route Element입니다.
    • path는 path pattern입니다. path pattern이 현재 URL과 일치하면 엘리먼트가 렌더링됩니다.
  • Nested Routes
    • Route는 자식을 가질 수 있고 각 Route는 세그먼트를 통해 URL의 일부를 정의하므로 단일 URL은 트리의 중첩 "분기(branch)"에 있는 여러 Route와 일치할 수 있습니다.
    • 이렇게 하면 outlet, 상대 링크(relative link) 등을 통해 자동 레이아웃 중첩이 가능합니다.
  • Relative links
    • /로 시작하지 않는 링크는 렌더링되는 가장 가까운 경로를 상속합니다.
    • 이렇게 하면 전체 경로를 알 필요 없이, 깊은 URL에 쉽게 연결할 수 있습니다.
  • Match
    • Route가 URL과 일치할 때의 정보를 보유하는 객체 입니다.
      • (예: 일치하는 URL 매개변수 및 경로 이름).
  • Matches
    • 현재 location와 일치하는 route의 배열(또는 route config의 branch)입니다.
    • 이 구조는 nested routes를 활성화합니다.
  • Parent Route
    • Child Route가 있는 Route 입니다.
  • Outlet
    • match 항목 집합에서 다음 match 항목을 렌더링하는 컴포넌트 입니다.
  • Index Route
    • 부모 URL의 부모 Outlet에서 렌더링되는 Route가 없는 Child Route입니다.
  • Layout Route
    • path가 없는 Parent Route로, 특정 레이아웃 내에서 하위 Route를 그룹화하는 데만 사용됩니다.

개념이 정말 많았네요, 혹시 빠진게 있으면 댓글로 알려주시길 부탁드립니다.

History and Locations

React Router가 무엇이든 할 수 있으려면, 브라우저 히스토리 스택의 변경 사항을 구독할 수 있어야 합니다.
브라우저는 사용자 탐색 시 고유한 기록 스택을 유지합니다.
이것이 뒤로가기 및 앞으로 가기 버튼이 작동하는 방식입니다.
기존 웹사이트(JavaScript가 없는 HTML 문서)에서
브라우저는 사용자가 링크를 클릭하거나 양식을 제출하거나 뒤로 및 앞으로 버튼을 클릭할 때마다 서버에 요청을 보냅니다.
예를 들어 다음과 같은 사용 사례를 고려하십시오.
  1. clicks a link to /dashboard
  2. clicks a link to /accounts
  3. clicks a link to /customers/123
  4. clicks the back button
  5. clicks a link to /dashboard
히스토리 스택굵게 표시된 항목이 현재 URL을 나타내면서, 다음과 같이 변경됩니다.
  1. /dashboard
  2. /dashboard, /accounts
  3. /dashboard, /accounts, /customers/123
  4. /dashboard, /accounts, /customers/123
  5. /dashboard, /accounts, /dashboard

History Object

클라이언트 측 라우팅을 통해 개발자는 브라우저 히스토리 스택프로그래밍으로 조작할 수 있습니다.

예를 들어, 서버에 요청하는 브라우저의 기본 동작 없이 URL을 변경하기 위해 다음과 같은 코드를 작성할 수 있습니다.

<a
  href="/contact"
  onClick={(event) => {
    // stop the browser from changing the URL and requesting the new document
    event.preventDefault();
    // push an entry into the browser history stack and change the URL
    window.history.pushState({}, undefined, "/contact");
  }}
/>
React Router에서 window.history.pushState를 직접 사용하지 마십시오
이 코드는 URL을 변경하지만 UI에 대해서는 아무 작업도 하지 않습니다.
UI가 연락처 페이지로 변경되도록 하려면 어딘가에서 일부 상태를 변경하는 코드를 더 작성해야 합니다.
문제는 브라우저가 "URL을 리스닝", 즉 변경 사항을 구독하는 방법을 제공하지 않는다는 것입니다.
 
글쎄요, 사실이 아닙니다. POP 이벤트를 통해 URL 변경 사항을 수신할 수 있습니다.
window.addEventListener("popstate", () => {
  // URL changed!
});
그러나 이는 사용자가 뒤로 또는 앞으로 버튼을 클릭할 때만 발생합니다.

프로그래머가 window.history.pushState 또는 window.history.replaceState를 호출했을 때의 이벤트는 없습니다.

바로 여기에서 React Router 특정 History 객체가 작동합니다.

히스토리 액션이 PUSH, POP 또는 REPLACE인지 여부에 따라 "URL 리스닝" 변경 방법을 제공합니다.

let history = createBrowserHistory();
history.listen(({ location, action }) => {
  // this is called whenever new locations come in
  // the action is POP, PUSH, or REPLACE
});
즉 사용자 자체적으로 히스토리 객체를 설정할 필요가 없습니다.
 
이것이 <Router>의 역할입니다.
히스토리 객체 중 하나를 설정하고 히스토리 스택의 변경 사항을 구독하며 마지막으로 URL이 변경되면 상태를 업데이트합니다.
이로 인해 앱이 다시 렌더링되고 올바른 UI가 표시됩니다.
상태를 설정하는 데 필요한 유일한 것은 location이며 다른 모든 것은 해당 단일 객체에서 작동합니다.
History : React Router가 URL의 변경 사항을 구독할 수 있게 하고 프로그래밍으로 브라우저 기록 스택을 조작하기 위한 API를 제공하는 객체입니다.

Locations

브라우저는 window.location에 location 객체를 가지고 있습니다.

이 객체는, URL에 대한 정보를 알려주지만 URL을 변경하는 몇 가지 메소드도 제공합니다.

window.location.pathname; // /getting-started/concepts/
window.location.hash; // #location
window.location.reload(); // force a refresh w/ the server
// and a lot more
일반적으로 React Router 앱에서 window.location으로 작업하지 않습니다.
window.location을 직접 사용하는 대신,
React Router는 window.location 을 이용해 패턴화한 location 개념을 사용합니다.
{
  pathname: "/bbq/pig-pickins",
  search: "?campaign=instagram",
  hash: "#menu",
  state: null,
  key: "aefz24ie"
}

처음 세 개: { pathname, search, hash }는 정확히 window.location과 같습니다.

이 세 가지만 더하면 사용자가 브라우저에서 보는 URL을 얻을 수 있습니다.

location.pathname + location.search + location.hash;
// /bbq/pig-pickins?campaign=instagram#menu
마지막 두 개 { state, key }는 React Router에만 존재합니다.

Location Pathname

이것은 출처(origin) 뒤의 URL 부분이므로 https://example.com/teams/hotspurs의 경우

pathname은 /teams/hotspurs입니다. 이것은 단지 routes에 대해 path가 일치하는 location의 일부분입니다.

 

Location Search

사람들은 URL의 이 부분에 대해 다양한 용어를 사용합니다.
  • location search
  • search params
  • URL search params
  • query string
React Router에서는 이를 "location search"라고 합니다.
location searchURLSearchParams의 직렬화된 버전입니다.
따라서 때로는 "URLSearchParams"이라고도 부를 수 있습니다.
// given a location like this:
let location = {
  pathname: "/bbq/pig-pickins",
  search: "?campaign=instagram&popular=true",
  hash: "",
  state: null,
  key: "aefz24ie",
};

// we can turn the location.search into URLSearchParams
let params = new URLSearchParams(location.search);
params.get("campaign"); // "instagram"
params.get("popular"); // "true"
params.toString(); // "campaign=instagram&popular=true",

정확히 말하자면, 직렬화된 문자열 버전을 "search"로,

파싱된 버전을 "search params"로 부르는게 맞지만,

정밀도가 중요하지 않을 때 용어를 서로 바꿔서 사용하는 것이 일반적입니다.

Location Hash

URL의 해시는 현재 페이지의 스크롤 위치를 나타냅니다.
window.history.pushState API가 도입되기 전에 웹 개발자는 URL의 해시 부분으로만 클라이언트 측 라우팅을 수행했으며
이 부분은 서버에 새 요청 없이 조작할 수 있는 유일한 부분이었습니다.
그러나 오늘날 우리는 그것을 설계된 목적으로 사용할 수 있습니다.

Location State

window.history.pushState() API가 "pushState"라고 불리는 이유가 궁금할 것입니다.
상태? 우리는 단지 URL을 변경하고 있지 않습니까? history.push가 아니어야합니까?
글쎄요, 우리는 API가 설계되었을 때 그 자리에 없었기 때문에 "상태"가 왜 초점이 되었는지 확신할 수 없지만
그럼에도 불구하고 브라우저의 멋진 기능입니다.
 
브라우저에서 pushState에 값을 전달하여 전환에 대한 정보를 유지할 수 있습니다.
사용자가 뒤로 가기를 클릭하면 history.state의 값이 이전에 "push"된 값으로 변경됩니다.
window.history.pushState("look ma!", undefined, "/contact");
window.history.state; // "look ma!"
// user clicks back
window.history.state; // undefined
// user clicks forward
window.history.state; // "look ma!"
React Router 앱에서 당신은 history.state를 직접 읽지 않습니다.
React Router는 이 브라우저 기능을 약간 추상화하고, history 대신 location에 값을 표시합니다.
프로그래머만 알고 있는 URL의 초 비밀 부분처럼 숨겨진 URL에 값을 넣는 대신
location.hash 또는 location.search와 마찬가지로 location.state를 상태처럼 사용할 수 있습니다.
location state의 몇 가지 훌륭한 사용 사례는 다음과 같습니다.
  • 리스트의 일부 레코드를 다음 화면으로 전송하여 부분 데이터를 즉시 렌더링하고, 나중에 나머지 데이터를 가져올 수 있습니다.
  • 사용자가 어디에서 왔는지 다음 페이지에 알리고 UI를 분기합니다.
    • 여기에서 가장 널리 사용되는 구현은 사용자가 그리드 보기에서 항목을 클릭한 경우 모달로 레코드를 표시합니다.
    • URL로 직접 접근하는 경우, 자체 레이아웃으로 레코드를 표시합니다. (ex pinterest, 이전의 인스타그램)
location state는 <Linke>에서 설정하거나 탐색(navigate)하는 두 가지 방법으로 설정합니다.
<Link to="/pins/123" state={{ fromDashboard: true }} />;

let navigate = useNavigate();
navigate("/users/123", { state: partialUser });
다음 페이지에서 useLocation을 사용하여 액세스할 수 있습니다.
let location = useLocation();
location.state;
location state 값은 직렬화되므로 new Date()와 같은 것이 문자열로 바뀝니다.

Location Key

각 location에는 고유한 키가 있습니다.
이는 위치 기반 스크롤 관리, 클라이언트 측 데이터 캐싱 등과 같은 고급 사례에 유용합니다.
각각의 새로운 location은 고유한 키를 갖기 때문에
일반 객체, new Map() 또는 locationStorage에 정보를 저장하는 추상화를 구축할 수 있습니다.

예를 들어, 매우 기본적인 클라이언트 측 데이터 캐시는 location key(및 fetch URL)별로 값을 저장하고,

사용자가 다시 클릭할 때 데이터 가져오기를 건너뛸 수 있습니다.

let cache = new Map();

function useFakeFetch(URL) {
  let location = useLocation();
  let cacheKey = location.key + URL;
  let cached = cache.get(cacheKey);

  let [data, setData] = useState(() => {
    // initialize from the cache
    return cached || null;
  });

  let [state, setState] = useState(() => {
    // avoid the fetch if cached
    return cached ? "done" : "loading";
  });

  useEffect(() => {
    if (state === "loading") {
      let controller = new AbortController();
      fetch(URL, { signal: controller.signal })
        .then((res) => res.json())
        .then((data) => {
          if (controller.signal.aborted) return;
          // set the cache
          cache.set(cacheKey, data);
          setData(data);
        });
      return () => controller.abort();
    }
  }, [state, cacheKey]);

  useEffect(() => {
    setState("loading");
  }, [URL]);

  return data;
}

Matching

초기 렌더링 시, 그리고 히스토리 스택이 변경되면,

React Router는 route config에 대해 location를 일치시켜 렌더링을 위한 matches를 제공합니다.

Defining Routes

route config은 다음과 같은 route tree입니다.
route config - 경로 일치의 분기를 생성하기 위해 현재 위치에 대해 랭킹를 매기고 일치(중첩 포함)할 route 객체의 트리입니다.
<Routes>
  <Route path="/" element={<App />}>
    <Route index element={<Home />} />
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
      <Route path=":teamId/edit" element={<EditTeam />} />
      <Route path="new" element={<NewTeamForm />} />
      <Route index element={<LeagueStandings />} />
    </Route>
  </Route>
  <Route element={<PageLayout />}>
    <Route path="/privacy" element={<Privacy />} />
    <Route path="/tos" element={<Tos />} />
  </Route>
  <Route path="contact-us" element={<Contact />} />
</Routes>
<Routes> 컴포넌트는 props.children을 통해 재귀하여, props를 제거하고 다음과 같은 객체(routes)를 생성합니다.
Routes는 아래 객체를 통해 랭킹을 매기고 일치하는 로직을 수행하고, matchs 배열의 route 내부 엘리먼트들을 렌더링합니다.
let routes = [
  {
    element: <App />,
    path: "/",
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: "teams",
        element: <Teams />,
        children: [
          {
            index: true,
            element: <LeagueStandings />,
          },
          {
            path: ":teamId",
            element: <Team />,
          },
          {
            path: ":teamId/edit",
            element: <EditTeam />,
          },
          {
            path: "new",
            element: <NewTeamForm />,
          },
        ],
      },
    ],
  },
  {
    element: <PageLayout />,
    children: [
      {
        element: <Privacy />,
        path: "/privacy",
      },
      {
        element: <Tos />,
        path: "/tos",
      },
    ],
  },
  {
    element: <Contact />,
    path: "/contact-us",
  },
];

 

사실, <Routes> 대신에 useRoutes(routesGoHere) 후크를 대신 사용할 수 있습니다. 그것이 <Routes>가 하는 전부입니다.
보시다시피 경로는 :teamId/edit와 같은 멀티플 세그먼트 또는 :teamId와 같은 세그먼트를 정의할 수 있습니다.
route config의 분기 아래에 있는 모든 세그먼트는 route에 대한 최종 path pattern을 만들기 위해 함께 추가됩니다.

match params

:teamId 세그먼트에 유의하십시오.
이것은 우리가 path pattern의 동적 세그먼트라고 부르는 것입니다.
즉, 정적으로 URL(실제 문자)과 일치하지 않고, 동적으로 일치합니다.
:teamId에 대해 어떠한 값이나 채울 수 있습니다.
/teams/123 또는 /teams/cupcakes 모두 일치합니다.
파싱된 값을 URL 매개변수(params)라고 합니다.
따라서 이 경우 teamId params는 "123" 또는 "cupcakes"가 됩니다.
렌더링 섹션에서 해당 url maram을 앱에서 사용하는 방법을 살펴보겠습니다.
 
Ranking Routes
 
route config의 모든 branch의 모든 segment를 더하면 앱이 응답가능한 다음과 같은 path pattern 목록이 완성됩니다.
[
  "/",
  "/teams",
  "/teams/:teamId",
  "/teams/:teamId/edit",
  "/teams/new",
  "/privacy",
  "/tos",
  "/contact-us",
];​
URL /teams/new를 보세요.
해당 목록의 어떤 패턴이 URL과 일치합니까?

아래의 두 개입니다.

/teams/new
/teams/:teamId
React Router는 여기에서 결정을 내려야 합니다. 단 하나만 사용할 수 있습니다.
클라이언트 측과 서버 측 모두에서 많은 라우터는 정의된 순서대로 패턴을 처리합니다.
즉, 먼저 일치하는 패턴이 승리합니다.
이 경우 /를 일치시키고 <Home/> 구성요소를 렌더링합니다.
?? 확실히 우리가 원하는 것이 아닙니다.
이러한 종류의 라우터는 예상 결과를 얻기 위해 경로를 완벽하게 정렬해야 합니다.
이것이 React Router가 v6 이전까지 작동한 방식이지만, 지금은 훨씬 더 똑똑해졌습니다.
 
이러한 패턴을 보면 /teams/new가 URL /teams/new와 일치하기를 원한다는 것을 직관적으로 알 수 있습니다.
React Router도 그것을 알고 있습니다.
세그먼트, 정적 세그먼트, 동적 세그먼트, * 패턴 등의 수에 따라 route의 순위를 지정하고 가장 구체적인 일치 항목을 선택합니다.
route들의 순서에 대해 생각할 필요가 없습니다.
 
 

pathless Routes

이전에 이상한 경로를 본 적이 있을 겁니다.

<Route index element={<Home />} />
<Route index element={<LeagueStandings />} />
<Route element={<PageLayout />} />
 
path도 없는데 어떻게 route가 될까요?
이것은 React Router에서 "route"라는 단어가 꽤 느슨하게 사용되는 곳입니다.
<Home/> 및 <LeagueStandings/>는 index route이고 <PageLayout/>은 layout route입니다.
렌더링 섹션에서 작동 방식에 대해 논의할 것입니다. 어느 쪽도 실제로 match와 관련이 없습니다.
 

Route Matches

route가 URL과 일치하면 match object로 표시됩니다.
<Route path=":teamId" element={<Team/>}/>에 대한 일치는 다음과 같습니다.
{
  pathname: "/teams/firebirds",
  params: {
    teamId: "firebirds"
  },
  route: {
    element: <Team />,
    path: ":teamId"
  }
}
 
pathname은 이 경로와 일치하는 URL 부분을 보유합니다. (이 경우 전체 경로).
params는 일치하는 모든 동적 세그먼트에서 구문 분석된 값을 보유합니다.
param의 객체의 키는 세그먼트 이름에 직접 매핑됩니다.
즉, :teamId의 값은 params.teamId가 됩니다.

우리의 routes는 트리이기 때문에 단일 URL은 트리의 전체 분기와 일치할 수 있습니다.

URL /teams/firebirds를 고려하면 다음 route branch가 됩니다

<Routes>
  <Route path="/" element={<App />}>
    <Route index element={<Home />} />
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
      <Route path=":teamId/edit" element={<EditTeam />} />
      <Route path="new" element={<NewTeamForm />} />
      <Route index element={<LeagueStandings />} />
    </Route>
  </Route>
  <Route element={<PageLayout />}>
    <Route path="/privacy" element={<Privacy />} />
    <Route path="/tos" element={<Tos />} />
  </Route>
  <Route path="contact-us" element={<Contact />} />
</Routes>​

 

React Router는 이러한 routes와 URL을 통해 matches 배열을 형성하여,

route 중첩과 일치하는 중첩 UI를 렌더링할 수 있습니다.

matches - 현재 위치와 일치하는 route(또는 route config의 branch) 배열입니다. 이 구조는 중첩 경로를 활성화합니다.
[
  {
    pathname: "/",
    params: null,
    route: {
      element: <App />,
      path: "/",
    },
  },
  {
    pathname: "/teams",
    params: null,
    route: {
      element: <Teams />,
      path: "teams",
    },
  },
  {
    pathname: "/teams/firebirds",
    params: {
      teamId: "firebirds",
    },
    route: {
      element: <Team />,
      path: ":teamId",
    },
  },
];​

Rendering

마지막 개념은 렌더링입니다. 당신의 앱의 엔트리가 다음과 같이 생겼다고 생각해 봅시다.
const root = ReactDOM.createRoot(
  document.getElementById("root")
);
root.render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<App />}>
        <Route index element={<Home />} />
        <Route path="teams" element={<Teams />}>
          <Route path=":teamId" element={<Team />} />
          <Route path="new" element={<NewTeamForm />} />
          <Route index element={<LeagueStandings />} />
        </Route>
      </Route>
      <Route element={<PageLayout />}>
        <Route path="/privacy" element={<Privacy />} />
        <Route path="/tos" element={<Tos />} />
      </Route>
      <Route path="contact-us" element={<Contact />} />
    </Routes>
  </BrowserRouter>
);
/teams/firebirds URL을 다시 예로 들어 보겠습니다.
<Routes>는 위치를 route config와 일치시키고, 일치 세트를 가져온 다음 다음과 같이 React 요소 트리를 렌더링합니다.
<App>
  <Teams>
    <Team />
  </Teams>
</App>
부모 route의 element 내에서 렌더링된 각 match는 정말 강력한 추상화입니다.
대부분의 웹사이트와 앱은 다음과 같은 특징을 공유합니다.
  • 상자 안의 상자, 페이지의 하위 섹션을 변경하는 탐색 섹션이 있는 각각의 상자.

Outlets

이 중첩 요소 트리는 자동으로 만들어지지 않습니다.

<Routes>는 첫 번째 일치 요소를 렌더링합니다(이 경우 <App/>). 다음 경기의 요소는 <Teams>입니다.

이를 렌더링하려면 앱에서 <Outlet/>를 렌더링해야 합니다.

function App() {
  return (
    <div>
      <GlobalNav />
      <Outlet />
      <GlobalFooter />
    </div>
  );
}
Outlet 컴포넌트는 항상 다음 일치 항목을 렌더링합니다.
즉, <Teams/>도 <Team/>을 렌더링할 콘센트가 필요합니다. URL이 /contact-us인 경우 엘리먼트 트리는 다음과 같이 변경됩니다.
<App>
  <Teams>
    <EditTeam />
  </Teams>
</App>

Outlet은 일치하는 새 자식으로 자식을 교체하지만 부모 레이아웃은 유지됩니다.

컴포넌트를 정리하는 데 매우 효과적입니다.

 

Index Routes

/teams에 대한 route configs를 기억하시나요?
<Route path="teams" element={<Teams />}>
  <Route path=":teamId" element={<Team />} />
  <Route path="new" element={<NewTeamForm />} />
  <Route index element={<LeagueStandings />} />
</Route>
URL이 /teams/firebirds인 경우 엘리먼트 트리는 다음과 같습니다.
<App>
  <Teams>
    <Team />
  </Teams>
</App>
그러나 URL이 /teams인 경우 엘리먼트 트리는 다음과 같습니다.
<App>
  <Teams>
    <LeagueStandings />
  </Teams>
</App>
 
<Route index element={<LeagueStandings>}/> 이 왜 렌더링 되었을까요?
그 이유는 인덱스 경로이기 때문입니다. 인덱스 경로는 parent route의 path에 위치한  <Outlet/>에서 렌더링 됩니다.
자식 route의 path 중 하나에 있지 않은 경우 <Outlet/>은 UI에 아무 것도 렌더링하지 않습니다.
<App>
  <Teams />
</App>
팀 데이터가 전부 왼쪽에 있다면, Outlet이 위치한 오른쪽에 빈 페이지가 있음을 의미합니다!
UI는 공간을 채울 무언가가 필요합니다. index routes가 해법입니다.
index routes를 생각하는 또 다른 방법은 부모가 일치하지만 자식이 일치하지 않는 경우의 default child route라는 것입니다.
사용자 인터페이스에 따라 index routes가 필요하지 않을 수도 있지만
상위 경로에 일종의 영속적인 탐색이 있는 경우, 사용자가 항목 중 하나를 클릭하지 않았을 때 공간을 채우기 위해 index routes를 원할 가능성이 큽니다.

Layout Routes

다음은 아직 일치하지 않은 경로 구성의 일부입니다.
/privacy.
일치하는 경로를 강조 표시하여 route config를 다시 살펴보겠습니다.
<Routes>
  <Route path="/" element={<App />}> // here!!!!!!
    <Route index element={<Home />} />
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
      <Route path=":teamId/edit" element={<EditTeam />} />
      <Route path="new" element={<NewTeamForm />} />
      <Route index element={<LeagueStandings />} />
    </Route>
  </Route>
  <Route element={<PageLayout />}>  // here!!!!!!
    <Route path="/privacy" element={<Privacy />} />  // here!!!!!!
    <Route path="/tos" element={<Tos />} />
  </Route>
  <Route path="contact-us" element={<Contact />} />
</Routes>
그리고 렌더링된 결과 엘리먼트 트리는 다음과 같습니다.
<App>
  <PageLayout>
    <Privacy />
  </PageLayout>
</App>
PageLayout 경로는 이상하게 느껴질 수 있습니다.
매칭에 전혀 참여하지 않기 때문에 우리는 이것을 레이아웃 라우트라고 부릅니다.(자식들은 참여하지만).
동일한 레이아웃에서 여러 자식 경로를 더 간단하게 래핑하기 위해서만 존재합니다.
우리가 이것을 허용하지 않았다면 당신은 두 가지 다른 방식으로 레이아웃을 처리해야 할 것입니다:
  • Routes가 당신을 위해 레이아웃을 처리합니다.
  • 직접 앱 전체에 걸쳐 많은 레이아웃 컴포넌트를 반복하면서 수동으로 그것을 처리합니다:
당신이 직접 할 수도 있지만 레이아웃 route를 사용하는 것이 좋습니다.
// bad example
<Routes>
  <Route path="/" element={<App />}>
    <Route index element={<Home />} />
    <Route path="teams" element={<Teams />}>
      <Route path=":teamId" element={<Team />} />
      <Route path=":teamId/edit" element={<EditTeam />} />
      <Route path="new" element={<NewTeamForm />} />
      <Route index element={<LeagueStandings />} />
    </Route>
  </Route>
  <Route
    path="/privacy"
    element={
      <PageLayout>
        <Privacy />
      </PageLayout>
    }
  />
  <Route
    path="/tos"
    element={
      <PageLayout>
        <Tos />
      </PageLayout>
    }
  />
  <Route path="contact-us" element={<Contact />} />
</Routes>
예, 레이아웃 "route"의 의미는 URL match와 아무 관련이 없기 때문에 약간 바보같을 수 있지만, 허용하지 않기에는 너무 편리합니다.

Navigating

URL이 변하는 것을 "navigation"이라고 합니다. React Router에서 navigation 하는 방법에는 두 가지가 있습니다.
  • <Link>
  • navigate

Link

이것은 navigation의 기본 수단입니다.
<Link>를 렌더링하면 사용자 클릭 시 URL을 변경할 수 있습니다.
React Router는 브라우저의 기본 동작을 방지하고 history stack에 새 항목을 푸시하도록 history 객체에 알립니다.
위치가 변경되고 새 match 항목이 렌더링됩니다.
 
 
Link는 다음과 같이 접근성 항목을 충족합니다.
  • 여전히 모든 기본 접근성 문제(예: 키보드, 포커스 가능성, SEO 등)가 충족되도록 <a href>를 렌더링합니다.
  • "새 탭에서 열기"를 마우스 오른쪽 버튼으로 클릭하거나 명령/제어를 클릭하는 경우 브라우저의 기본 동작을 방지하지 않습니다.
Nested routes는 단순히 레이아웃을 렌더링하는 것이 아닙니다.
그들은 또한 "relative link"를 활성화합니다. 이전의 teams route를 떠올려봅니다.
<Route path="teams" element={<Teams />}>
  <Route path=":teamId" element={<Team />} />
</Route>
<Teams> 컴포넌트는 다음과 같은 링크를 렌더링할 수 있습니다.
<Link to="psg" />
<Link to="new" />
링크되는 전체 경로는 /teams/psg 및 /teams/new입니다.
즉, 그들이 렌더링되는 경로를 상속합니다.
이렇게 하면 route 컴포넌트가 앱의 나머지 경로에 대해 실제로 알 필요가 없습니다.
매우 많은 양의 링크는 단지 한 세그먼트 더 깊이 이동합니다.
 
전체 route config를 재정렬할 수 있으며 이러한 링크는 여전히 잘 작동할 것입니다.
이는 처음에 사이트를 구축할 때, 그리고 디자인과 레이아웃이 바뀔 때 매우 중요합니다.
 
 

Navigate Function

이 함수는 useNavigate hooks에서 반환되며 프로그래머가 원할 때마다 URL을 변경할 수 있도록 합니다.

 

ex : 타임아웃

let navigate = useNavigate();
useEffect(() => {
  setTimeout(() => {
    navigate("/logout");
  }, 30000);
}, []);​

ex : 양식 제출

<form onSubmit={event => {
  event.preventDefault();
  let data = new FormData(event.target)
  let urlEncoded = new URLSearchParams(data)
  navigate("/create", { state: urlEncoded })
}}>

Link와 마찬가지로 navigate는 중첩된 "to" 값에서도 작동합니다. (relative link)

navigate("psg");

<Link> 대신 navigate를 사용할 때에는 충분한 이유가 있어야 합니다.

이것은 우리를 매우 슬프게 합니다.

<li onClick={() => navigate("/somewhere")} />

Link와 Form을 제외하고 URL을 변경해야 하는 상호 작용은 거의 없습니다.

URL은 접근성 및 사용자 예상 행동을 둘러싼 복잡성을 유발하기 때문입니다.

 

Data Access

마지막으로 애플리케이션은 전체 UI를 구축하기 위해 React Router에 몇 가지 정보를 요청하려고 합니다.
이를 위해 React Router는 몇몇 hooks를 제공합니다.
let location = useLocation();
let urlParams = useParams();
let [urlSearchParams] = useSearchParams();
 

Review

const root = ReactDOM.createRoot(
  document.getElementById("root")
);
root.render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<App />}>
        <Route index element={<Home />} />
        <Route path="teams" element={<Teams />}>
          <Route path=":teamId" element={<Team />} />
          <Route path="new" element={<NewTeamForm />} />
          <Route index element={<LeagueStandings />} />
        </Route>
      </Route>
      <Route element={<PageLayout />}>
        <Route path="/privacy" element={<Privacy />} />
        <Route path="/tos" element={<Tos />} />
      </Route>
      <Route path="contact-us" element={<Contact />} />
    </Routes>
  </BrowserRouter>
);

1. 앱을 렌더링합니다.

2. <BrowserRouter>history를 생성하고, 초기 location를 history의 상태로 설정하고 URL을 구독합니다.

3. <Routes>는 하위 route를 재귀하며

  • route config을 빌드하고
  • 해당 routes를 location와 일치시키고
  • route matches를 만들고
  • 첫 번째 매치 항목의 route 엘리먼트를 렌더링합니다.

4. 각 parent route에서 <Outlet/>을 렌더링합니다.

5. <Outlet/>는 경로 일치에서 다음 일치를 렌더링합니다.

6. 사용자가 Link를 클릭합니다.

7. Link는 navigation()을 호출합니다.

8. history는 URL을 변경하고 <BrowserRouter>에 알립니다.

9. <BrowserRouter>가 다시 렌더링되고 (2)에서 다시 시작합니다!

 

이 가이드가 React Router의 주요 개념을 더 깊이 이해하는 데 도움이 되었기를 바랍니다.

 
반응형