https://reactrouter.com/docs/en/v6/getting-started/concepts#main-concepts
요즘은 대부분의 회사들이 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 routes와 layout routes를 잘 활용하면 공수를 덜 수 있다.
Main Concepts
이 문서는 React Router에 구현된 라우팅의 핵심 개념에 대해 자세히 설명합니다.
꽤 길기 때문에 보다 실용적인 가이드를 찾고 있다면 quick start tutorial을 확인하십시오.
React Router가 정확히 무슨 일을 하는지 궁금할 것입니다.
이 문서에는 React Router에 구현된 라우팅 이면의 모든 핵심 개념에 대한 자세한 설명이 포함되어 있습니다.
일상적인 케이스의 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
- 동적 세그먼트와 일치하는 URL의 구문 분석된 값입니다.
- 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 매개변수 및 경로 이름).
- Route가 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
- clicks a link to /dashboard
- clicks a link to /accounts
- clicks a link to /customers/123
- clicks the back button
- clicks a link to /dashboard
- /dashboard
- /dashboard, /accounts
- /dashboard, /accounts, /customers/123
- /dashboard, /accounts, /customers/123
- /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를 직접 사용하지 마십시오
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
});
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으로 작업하지 않습니다.
{
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
Location Pathname
이것은 출처(origin) 뒤의 URL 부분이므로 https://example.com/teams/hotspurs의 경우
pathname은 /teams/hotspurs입니다. 이것은 단지 routes에 대해 path가 일치하는 location의 일부분입니다.
Location Search
- location search
- search params
- URL search params
- query string
// 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
Location State
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를 직접 읽지 않습니다.
- 리스트의 일부 레코드를 다음 화면으로 전송하여 부분 데이터를 즉시 렌더링하고, 나중에 나머지 데이터를 가져올 수 있습니다.
- 사용자가 어디에서 왔는지 다음 페이지에 알리고 UI를 분기합니다.
- 여기에서 가장 널리 사용되는 구현은 사용자가 그리드 보기에서 항목을 클릭한 경우 모달로 레코드를 표시합니다.
- URL로 직접 접근하는 경우, 자체 레이아웃으로 레코드를 표시합니다. (ex pinterest, 이전의 인스타그램)
<Link to="/pins/123" state={{ fromDashboard: true }} />;
let navigate = useNavigate();
navigate("/users/123", { state: partialUser });
let location = useLocation();
location.state;
Location Key
예를 들어, 매우 기본적인 클라이언트 측 데이터 캐시는 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 객체의 트리입니다.
<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>
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",
},
];
match params
[
"/",
"/teams",
"/teams/:teamId",
"/teams/:teamId/edit",
"/teams/new",
"/privacy",
"/tos",
"/contact-us",
];
아래의 두 개입니다.
/teams/new
/teams/:teamId
pathless Routes
이전에 이상한 경로를 본 적이 있을 겁니다.
<Route index element={<Home />} />
<Route index element={<LeagueStandings />} />
<Route element={<PageLayout />} />
Route Matches
route가 URL과 일치하면 match object로 표시됩니다.{
pathname: "/teams/firebirds",
params: {
teamId: "firebirds"
},
route: {
element: <Team />,
path: ":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>
);
<App>
<Teams>
<Team />
</Teams>
</App>
- 상자 안의 상자, 페이지의 하위 섹션을 변경하는 탐색 섹션이 있는 각각의 상자.
Outlets
이 중첩 요소 트리는 자동으로 만들어지지 않습니다.
<Routes>는 첫 번째 일치 요소를 렌더링합니다(이 경우 <App/>). 다음 경기의 요소는 <Teams>입니다.
이를 렌더링하려면 앱에서 <Outlet/>를 렌더링해야 합니다.
function App() {
return (
<div>
<GlobalNav />
<Outlet />
<GlobalFooter />
</div>
);
}
<App>
<Teams>
<EditTeam />
</Teams>
</App>
Outlet은 일치하는 새 자식으로 자식을 교체하지만 부모 레이아웃은 유지됩니다.
컴포넌트를 정리하는 데 매우 효과적입니다.
Index Routes
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
<App>
<Teams>
<Team />
</Teams>
</App>
<App>
<Teams>
<LeagueStandings />
</Teams>
</App>
<App>
<Teams />
</App>
Layout Routes
<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>
- 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>
Navigating
- <Link>
- navigate
Link
- 여전히 모든 기본 접근성 문제(예: 키보드, 포커스 가능성, SEO 등)가 충족되도록 <a href>를 렌더링합니다.
- "새 탭에서 열기"를 마우스 오른쪽 버튼으로 클릭하거나 명령/제어를 클릭하는 경우 브라우저의 기본 동작을 방지하지 않습니다.
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
</Route>
<Link to="psg" />
<Link to="new" />
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
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의 주요 개념을 더 깊이 이해하는 데 도움이 되었기를 바랍니다.
'FrontEnd' 카테고리의 다른 글
React Query에는 Debounce와 Throttling이 없다? (0) | 2022.06.13 |
---|---|
대규모 프로젝트 React Query 아키텍처 (1) | 2022.06.11 |
프론트엔드 지식 : Javascript Critical Rendering Path (0) | 2022.06.08 |
리액트 성능 최적화 : Death By a Thousand Cuts (천 번 베이면 죽는다.) (0) | 2022.06.05 |
리액트 성능 최적화 : Production Monitoring (0) | 2022.06.05 |