의문점

react에서는 react router의 useNavigate를 사용해 라우팅을 한다. useNavigate는 실제로 내부에서 SPA를 위한 History API를 사용했을까? 사용했다면 어떤 방식으로 사용했을까?

https://github.com/remix-run/react-router 세계로!

  1. 모노레포 구조 파악하기 : pnpm-workspace.yaml

    packages:
      - "integration"
      - "integration/helpers/*"
      - "packages/*"
      - "playground/*"
    

    packages에 각 패키지가 있을 것만같은 직감이 든다.

  2. react-router/packages/react-router/package.json

      "exports": {
        ".": {
          "node": {
            "types": "./dist/development/index.d.ts",
            "module-sync": "./dist/development/index.mjs",
            "default": "./dist/development/index.js"
          },
          "import": {
            "types": "./dist/development/index.d.mts",
            "default": "./dist/development/index.mjs"
          },
          "default": {
            "types": "./dist/development/index.d.ts",
            "default": "./dist/development/index.js"
          }
        },
    

    react-router패키지의 package.json을 살펴보면, 빌드 후 index.mjs에서 모든 파일을 내보내고 있다.

    .mjs확장자인 것은 빌드 후 생성된 파일이며 우리는 .ts파일을 확인하면 된다.

  3. react-router/packages/react-router/index.ts

    export {
      useNavigate,
    	// ..
    } from "./lib/hooks";
    

    useNavigate, 찾았다 요놈! hooks폴더로 찾아가자.

  4. react-router/packages/react-router/lib/hooks.tsx

    export function useNavigate(): NavigateFunction {
      let { isDataRoute } = React.useContext(RouteContext);
      return isDataRoute ? useNavigateStable() : useNavigateUnstable();
    }
    

    isDataRoute의 여부에 따라 useNavigateStable 혹은 useNavigateUnstable를 사용한다.

    RouteContext가 뭘까?

  5. react-router/packages/react-router/lib/context.ts

    export const RouteContext = React.createContext<RouteContextObject>({
      outlet: null,
      matches: [],
      isDataRoute: false,
    });
    

    RouteContext은 기본적으로 isDataRoute이 false인 객체이다. 그럼 isDataRoute이 false인 경우의 훅인 useNavigateUnstable훅을 먼저 살펴보자.

  6. react-router/packages/react-router/lib/hooks.tsx

    function useNavigateUnstable() {
      let navigate = 
        (to: To | number) => {
          if (typeof to === "number") {
            navigator.go(to);
            return;
          }
    			// ..
          (!!options.replace ? navigator.replace : navigator.push)(
            path,
            options.state,
            options
          );
        },
        // ..
      return navigate;
    }
    

    useNavigateUnstable은 내부적으로 navigate함수를 만들고, 이를 반환한다. 여기서 핵심 로직을 확인할 수 있다.

    if (typeof to === "number") {
      navigator.go(to);
      return;
    }
    

    to로 전달한 값이 숫자라면 navigator.go의 인자로 넘겨 실행한다.

    (!!options.replace ? navigator.replace : navigator.push)(
      path,
      options.state,
      options
    );
    

    options.replace 값이 true면 navigotor.replace를, options.replace값이 false이면 navigator.push를 실행한다.

    그럼 핵심은 navigator객체에 있다는 것을 확인할 수 있다. navigator객체를 찾아보자.

    <aside> <img src="/icons/dialogue_lightgray.svg" alt="/icons/dialogue_lightgray.svg" width="40px" />

    그 전에 navigate의 인자로 넘기는 To 타입이 어떤 타입인지 궁금해서 찾아보았다.

    </aside>

  7. react-router/packages/react-router/lib/hooks.tsx

    let { basename, navigator } = React.useContext(NavigationContext);
    

    navigator는 NavigationContext의 value로 전달받은 객체이다. 이 NavigationContext의 값을 결정하는 곳을 찾아보자.

  8. react-router/packages/react-router/lib/context.ts

    export const NavigationContext = React.createContext<NavigationContextObject>(
      null!
    );
    

    여기에는 별 내용이 없다. null로 기본값을 설정하고 있다.

  9. react-router/packages/react-router/lib/components.tsx

    export function Router({
      navigator,
      // ..
    }){
      let navigationContext = React.useMemo(
        () => ({
          basename,
          navigator,
          static: staticProp,
          future: {},
        }),
        [basename, navigator, staticProp]
      );
      // ..
    
    	// ..
      return (
        <NavigationContext.Provider value={navigationContext}>
          <LocationContext.Provider children={children} value={locationContext} />
        </NavigationContext.Provider>
      );
    

    NavigationContext의 Provider를 이용해 navigationContext값을 뿌려주고 있음을 확인할 수 있다. navigationContext는 객체는 Router의 props로 전달받고 있다. 이 Router는 어디서 사용할까?

  10. react-router/packages/react-router/lib/dom/lib.tsx

    export function BrowserRouter() {
      let historyRef = React.useRef<BrowserHistory>();
      historyRef.current = createBrowserHistory({ window, v5Compat: true });
      let history = historyRef.current;
      
      let [state, setStateImpl] = React.useState({
        action: history.action,
        location: history.location,
      });
      let setState = React.useCallback(
        (newState: { action: NavigationType; location: Location }) => {
          React.startTransition(() => setStateImpl(newState));
        },
        [setStateImpl]
      );
    
      React.useLayoutEffect(() => history.listen(setState), [history, setState]);
    
      return (
        <Router
          basename={basename}
          children={children}
          location={state.location}
          navigationType={state.action}
          navigator={history}
        />
      );
    }
    

    BrowserRouter에서 Router컴포넌트를 사용하고 있다. 이 BrowserRouter가 바로 React Router를 사용하기 위해서 페이지 전체를 감싸저야하는 컴포넌트이다 !

    다 찾은 듯하다. 이제 여기서 우리가 찾고 있던 navigator를 찾아보자. BrowserRouter는 navigator에 history 객체를 넘기고 있다.

    export function BrowserRouter() {
    	// ..
      return (
        <Router
    			/* .. */
          navigator={history}
        />
    

    history는 historyRef.current의 참조를 가진 값이다.

    export function BrowserRouter() {
      let historyRef = React.useRef<BrowserHistory>();
      historyRef.current = createBrowserHistory({ window, v5Compat: true });
      let history = historyRef.current;
    

    history.current는 createBrowserHistory함수의 반환값이다. 그럼 이제 createBrowserHistory함수를 찾으러 가보자 !

  11. react-router/packages/react-router/lib/router/history.ts

    export function createHashHistory(
      options: HashHistoryOptions = {}
    ): HashHistory {
    	// ..
      return getUrlBasedHistory(
        createHashLocation,
        createHashHref,
        validateHashLocation,
        options
      );
    }
    

    createHashHistory는 getUrlBasedHistory함수의 반환값을 반환하는 함수이다.getUrlBasedHistory을 따라가보자!

    function getUrlBasedHistory(){
      let globalHistory = window.history;
      // ..
      function push(to: To, state?: any) {
    		// ..
        globalHistory.pushState(historyState, "", url);
      }
      function replace(to: To, state?: any) {
    		// ..
        globalHistory.replaceState(historyState, "", url);
      }
    	
    	// ..
    	
      let history: History = {
        get action() {
          return action;
        },
        get location() {
          return getLocation(window, globalHistory);
        },
        listen(fn: Listener) {
    			// ..
        },
        createHref(to) {
          // ..
        },
        createURL,
        encodeLocation(to) {
          // ..
        },
        push,
        replace,
        go(n) {
          return globalHistory.go(n);
        },
      };
      return history;
    }
    

    찾았다, 요놈 !

    push함수는 globalHistory.pushState(historyState,””,url);

    replace함수는 globalHistory.replaceState(historyState,””,url);

    였다.

정리

역시, useNavigate는 내부적으로 SPA에 적합한 History API를 사용하고 있었다. BrowserRouter에서 생성한 history 객체를 Router 컴포넌트를 통해 NavigationContext로 전달하고, useNavigate 훅에서 이를 사용한다. history 객체는 내부적으로 window.history의 pushState와 replaceState 메서드를 사용해 URL을 조작하고 있었다.

위의 코드들만 보면 간단해보이지만 설명을 위해 간략환한 코드가 정말 많았다. 잘하는 개발자는 이렇게 세심한 것 하나하나까지 고려를 하는구나 .. 느꼈다. 이런 코드를 많이 보고, 나도 세심한 코드를 작성하다보면 이런 코드를 작성할 수 있는 날이 오겠지.