원티드 프리온보딩으로 react-router를 가볍게 구현하고 난 후, 실제 react router는 어떻게 구현되어 있을지, 깃헙에 들어가서 살펴보자.
간단하게 Router를 구성하는 코드만 가져와보았다. 타입이나 별도의 코드는 빼고 핵심 코드로 보이는 것만 첨부하겠다.
export function Router({
basename: basenameProp = "/",
children = null,
location: locationProp,
navigationType = NavigationType.Pop,
navigator,
static: staticProp = false,
}: RouterProps): React.ReactElement | null {
invariant(
!useInRouterContext(),
`You cannot render a <Router> inside another <Router>.` +
` You should never have more than one in your app.`
);
// Preserve trailing slashes on basename, so we can let the user control
// the enforcement of trailing slashes throughout the app
let basename = basenameProp.replace(/^\/*/, "/");
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
if (typeof locationProp === "string") {
locationProp = parsePath(locationProp);
}
let {
pathname = "/",
search = "",
hash = "",
state = null,
key = "default",
} = locationProp;
let location = React.useMemo(() => {
let trailingPathname = stripBasename(pathname, basename);
if (trailingPathname == null) {
return null;
}
return {
pathname: trailingPathname,
search,
hash,
state,
key,
};
}, [basename, pathname, search, hash, state, key]);
warning(
location != null,
`<Router basename="${basename}"> is not able to match the URL ` +
`"${pathname}${search}${hash}" because it does not start with the ` +
`basename, so the <Router> won't render anything.`
);
if (location == null) {
return null;
}
return (
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
);
}
Router를 구성하는 코드이다.
이것을 보기 쉽게 쪼개어 살펴보겠다.
invariant(
!useInRouterContext(),
`You cannot render a <Router> inside another <Router>.` +
` You should never have more than one in your app.`
);
제일 먼저, invariant
라는 것이 보이는데, @remix-run/router
에서 import 하고 있다. 그리고 router
에서는 .untils
에서 import를 하고 있다. 그러면 대체 invariant
라는 녀석은 무엇일까?
간단하게 말해서, 에러 메세지를 띄우는 함수인 거 같다. 훅이나 컴포넌트를 잘못 사용했을 때, 브라우저 화면에 검은색 에러 창이 뜨면서 block 되는 현상을 한번쯤은 겪어봤을 것이다. 그런 에러 메세지를 띄우는 용도로 사용하는 함수라고 추측된다.
위의 에러 메세지는
Router
내부에Router
를 중복해서 사용할 수 없다는 내용인 듯 하다.
다음 코드를 봐보자.
let basename = basenameProp.replace(/^\/*/, "/");
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
if (typeof locationProp === "string") {
locationProp = parsePath(locationProp);
}
let {
pathname = "/",
search = "",
hash = "",
state = null,
key = "default",
} = locationProp;
basename
이라는 변수에 basenameProp
를 replace
했다. 단순하게 basename 이 될 url 앞에 /
를 붙이는 것이다. 이러한 이유는 절대 경로를 만들어 예기치 못한 오류를 피하기 위함인 것 같다.
basename = http://localhost:3000
// ex). /http://localhost:3000
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
그 후, navigationContext
객체를 생성하게 되는데, useMemo
라는 훅을 사용해 첫번째 인자로 쓴 함수를 재사용하게 된다.
그리고 위의 세 가지 중 하나라도 변경된다면, useMemo
가 새로운 값을 반환하게 된다.
(변경되지 않으면 기존 값을 참조함)
if (typeof locationProp === "string") {
locationProp = parsePath(locationProp);
}
locationProp
의 값이 string인지 확인한다. 문자열 타입이 맞다면 parsePath
를 호출해 인자로 넣게 되는데, parsePath
가 하는 일은 locationProp
에 #
가 붙어있는지, ?
가 붙어있는지 확인하는 일이다. #
,?
둘 중 무엇도 붙어있지 않다면, 다시 locationProp
을 반환한다.
let {
pathname = "/",
search = "",
hash = "",
state = null,
key = "default",
} = locationProp;
구조 분해 할당을 통해, 문자열을 객체로 바꿔준다.
let location = React.useMemo(() => {
let trailingPathname = stripBasename(pathname, basename);
if (trailingPathname == null) {
return null;
}
return {
pathname: trailingPathname,
search,
hash,
state,
key,
};
}, [basename, pathname, search, hash, state, key]);
다음 코드를 보면 stripBasename
함수를 호출하는데, stripBasename 가 하는 일은 pathname 에서 basename을 제거하는 일이다. 다음을 보자.
function stripBasename(
pathname: string,
basename: string
): string | null {
if (basename === "/") return pathname;
if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
return null;
}
// We want to leave trailing slash behavior in the user's control, so if they
// specify a basename with a trailing slash, we should support it
let startIndex = basename.endsWith("/")
? basename.length - 1
: basename.length;
let nextChar = pathname.charAt(startIndex);
if (nextChar && nextChar !== "/") {
// pathname does not start with basename/
return null;
}
return pathname.slice(startIndex) || "/";
}
basename
과 pathname
이 같다면, /
반환한다. pathname
에 basename
이 포함되어 있다면, basename
만 제거하고 pathname
을 반환한다. 만약, 전혀 다른 basename
,과 pathname
이라면 null
을 반환하고 종료한다.
let location = React.useMemo(() => {
let trailingPathname = stripBasename(pathname, basename);
if (trailingPathname == null) {
return null;
}
return {
pathname: trailingPathname,
search,
hash,
state,
key,
};
}, [basename, pathname, search, hash, state, key]);
다시 원래의 코드로 돌아와서 보자면, trailingPathname
에는 pathname
이 들어있을 것이다. 이걸 객체의pathname
속성에 할당하고 return
시킨다. 또한, 객체 내부 속성 중 하나라도 변경된다면 새로운 값을 참조할 수 있게 useMemo
를 사용하였다.
warning(
location != null,
`<Router basename="${basename}"> is not able to match the URL ` +
`"${pathname}${search}${hash}" because it does not start with the ` +
`basename, so the <Router> won't render anything.`
);
if (location == null) {
return null;
}
warning
이라는 함수를 호출하는데, location
이 null
일 때만 발생하게 된다. 경로가 basename
으로 시작되지 않을 때 발생하는 에러 메세지라고 이해하면 될 거 같다.
return (
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
);
그리고 context.provider
에 값을 전달한다.
NavigationContext
로 전달되는 값에는 basename
, navigator
, staticProp
가 들어있다.
LocationContext
로 전달되는 값은 두 가지가 있는데, children
의 경우, 값을 사용할 하위 컴포넌트를 전달한다.
(NavigationContext, LocationContext 사용)
value
로는 location
값과 navigationType
이 들어가게 된다.
Router 컴포넌트에서는
하위 컴포넌트에서 location, navigation 에 관련된 정보를 쉽게 가져올 수 있도록 처리하고 있다.
내가 구현했던 Router와 구현 방식이 180도 다른 것 같아 놀라웠다. 우선, useState를 사용하지 않고, 함수를 이용해 값을 return하였다는 것. url의 경우 변경되는 일이 적기 때문에 useMemo를 사용했을 거라는 점, 예외처리 등 여러 방면에서 다르다는 것을 확인할 수 있었다.
어라?
여기서 궁금한 점이 생겼다. 왜 useMemo hook은 사용하지만 useState를 사용하지 않은 걸까? 내가 놓친 부분이 있는지 다시 확인해봐야겠다.
useMemo를 사용하지만 useState를 사용하지 않는 것에 대한 해답을 생각해보았다. useState는 상태관리를 할 때 사용하는 hook이다. 그렇다면, Router에 상태관리가 필요한 값이 있을까?
없다.
나는 라우팅 링크를 useState로 관리했는데, 여기서 착안한 오해였다. 리액트의 Router에서는 props로 라우팅의 정보값을 받는다. 따라서 라우팅이 변경될 때마다 새로운 props를 내려받을 것이고 useState를 사용하여 상태관리를 할 필요가 없는 것이다.