React에서 라우팅을 하려면 useNavigate 혹은 Navigation 컴포넌트를 활용한다. 기존에 vanila.js에서는 window.location을 통해서 이동하였는데 그 차이가 뭘까?
만약 Pick, Partial과 같은 Utility Type에 익숙하지 않다면 아래 포스팅을 먼저 읽고오자!
history.pushstate vs window.location
결론부터 말하자면 react-router의 useNavigate은 history.pushState를 사용한다.
•
history.pushstate는 HTTP 요청을 호출하지 않는다!
•
window.location.herf = “url”은 새 HTTP요청을 호출한다!
만약 url이 hash fragment이면 새 HTTP요청을 하지 않고 연관된 앵커로 스크롤된다.
history.pushstate | window.location.herf | |
HTTP 요청 | ||
페이지 새로고침 | ||
Application 상태 유지 |
위 특징을 보면 알 수 있듯이, 무조건 useNavigate를 사용할 필요는 없을 것 같다. 만약 이동과 동시에 새로고침이 필요하다면 window.locaiton.herf를 적절히 사용하면 되겠다.
react-router코드 살펴보기
그렇다면 react-router의 useNavigate가 어떻게 구현되어 있는지 살펴보자. 아래 코드는 useNavigate의 전체 코드이다.
코드가 너무 길어서 핵심만 보고싶다면 하이라이팅 된 부분만 살펴보자.
// context.ts
import type { History, Location } from "history";
import { Action as NavigationType } from "history";
import type { RouteMatch } from "./router";
/**
* A Navigator is a "location changer"; it's how you get to different locations.
*
* Every history instance conforms to the Navigator interface, but the
* distinction is useful primarily when it comes to the low-level <Router> API
* where both the location and a navigator must be provided separately in order
* to avoid "tearing" that may occur in a suspense-enabled app if the action
* and/or location were to be read directly from the history instance.
*/
export type Navigator = Pick<History, "go" | "push" | "replace" | "createHref">;
interface NavigationContextObject {
basename: string;
navigator: Navigator;
static: boolean;
}
export const NavigationContext = React.createContext<NavigationContextObject>(
null!
);
interface LocationContextObject {
location: Location;
navigationType: NavigationType;
}
TypeScript
복사
// hooks.tsx
import type { Location, Path, To } from "history";
import { LocationContext, NavigationContext, RouteContext } from "./context";
export interface NavigateFunction {
(to: To, options?: NavigateOptions): void;
(delta: number): void;
}
export interface NavigateOptions {
replace?: boolean;
state?: any;
}
export function useNavigate(): NavigateFunction {
invariant(
useInRouterContext(),
// TODO: This error is probably because they somehow have 2 versions of the
// router loaded. We can help them understand how to avoid that.
`useNavigate() may be used only in the context of a <Router> component.`
);
let { basename, navigator } = React.useContext(NavigationContext);
let { matches } = React.useContext(RouteContext);
let { pathname: locationPathname } = useLocation();
let routePathnamesJson = JSON.stringify(
matches.map((match) => match.pathnameBase)
);
let activeRef = React.useRef(false);
React.useEffect(() => {
activeRef.current = true;
});
let navigate: NavigateFunction = React.useCallback(
(to: To | number, options: NavigateOptions = {}) => {
warning(
activeRef.current,
`You should call navigate() in a React.useEffect(), not when ` +
`your component is first rendered.`
);
if (!activeRef.current) return;
if (typeof to === "number") {
navigator.go(to);
return;
}
let path = resolveTo(
to,
JSON.parse(routePathnamesJson),
locationPathname
);
if (basename !== "/") {
path.pathname = joinPaths([basename, path.pathname]);
}
(!!options.replace ? navigator.replace : navigator.push)(
path,
options.state
);
},
[basename, navigator, routePathnamesJson, locationPathname]
);
return navigate;
}
TypeScript
복사
결과적으로 useNavigation은 history api를 사용한다. 아래는 history 전체 소스코드이다.
그렇다면 의존하고 있는 history 모듈을 살펴보자. history api에서 살펴봐야할 것은 History, To와 같은 것들이다. useNavigate에서 라우팅을 해야할 때 사용하고 있는 push함수를 집중적으로 살펴 보자.
// index.ts
export interface Path {
pathname: Pathname;
search: Search;
hash: Hash;
}
export type To = string | Partial<Path>;
export interface History {
readonly action: Action;
readonly location: Location;
createHref(to: To): string;
push(to: To, state?: any): void;
replace(to: To, state?: any): void;
go(delta: number): void;
back(): void;
forward(): void;
listen(listener: Listener): () => void;
block(blocker: Blocker): () => void;
}
let globalHistory = window.history;
function push(to: To, state?: any) {
let nextAction = Action.Push;
let nextLocation = getNextLocation(to, state);
function retry() {
push(to, state);
}
if (allowTx(nextAction, nextLocation, retry)) {
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
// TODO: Support forced reloading
// try...catch because iOS limits us to 100 pushState calls :/
try {
globalHistory.pushState(historyState, "", url);
} catch (error) {
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
}
applyTx(nextAction);
}
}
TypeScript
복사
위 함수를 살펴보면 결과적으로 useNavigate가 window.history.pushState를 사용한다는 것을 확인할 수 있다.