React에서 라우팅할 때 react-router-dom 라이브러리를 사용한다.

그래서 이 라이브러리의 동작 방식을 까보기로 했다.

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Main, Page1, Page2, NotFound } from "../pages";
import { Header } from ".";

const Router = () => {
  return (
    <BrowserRouter>
      <Header />
      <Routes>
        <Route path="/" element={<Main />} />
        <Route path="/page1/*" element={<Page1 />} />
        <Route path="/page2/*" element={<Page2 />} />
        <Route path="/*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
};

export default Router;

대충 이런식으로 구조가 되있기 때문에 처음 까볼 곳을 BrowserRouter!

// react-router-dom/index.tsx
export function BrowserRouter({
  basename,
  children,
  window,
}: BrowserRouterProps) {
  let historyRef = React.useRef<BrowserHistory>();
  if (historyRef.current == null) {
    historyRef.current = createBrowserHistory({ window, v5Compat: true });
  }

  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location,
  });

  React.useLayoutEffect(() => history.listen(setState), [history]);

  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
}
// router/history.ts
export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
  function createBrowserLocation(
    window: Window,
    globalHistory: Window["history"]
  ) {
    let { pathname, search, hash } = window.location;
    return createLocation(
      "",
      { pathname, search, hash },
      // state defaults to `null` because `window.history.state` does
      (globalHistory.state && globalHistory.state.usr) || null,
      (globalHistory.state && globalHistory.state.key) || "default"
    );
  }

  function createBrowserHref(window: Window, to: To) {
    return typeof to === "string" ? to : createPath(to);
  }

  return getUrlBasedHistory(
    createBrowserLocation,
    createBrowserHref,
    null,
    options
  );
}

history API를 사용해서 구현하는 걸 알 수 있었다.

직접 구현

import { render } from "../Kreact";

export default function createRouter(root, routes = []) {
  const history = window.history;
  const routeMap = new Map();

  routes.forEach(({ pathname, element }) => {
    routeMap.set(pathname, element);
  });

  function push(pathname, state) {
    history.pushState(state, null, pathname);

    _render(root, pathname);
  }

  window.addEventListener('popstate', () => {
    _render(root, window.location.pathname);
  });

  function _render(root, pathname) {
    const element = routeMap.get(pathname);
    if (!element) throw new Error('NOT FOUND');

    root.innerHTML = '';
    render(root, element);
  }

  return {
    push,
  }
}

createRouter를 만들어보았다. react-router-dom의 createBrowserRouter와 비슷하게 구현했다.

// react-router-dom의 createBrowserRouter
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    children: [
      {
        path: "team",
        element: <Team />,
        loader: teamLoader,
      },
    ],
  },
]);

// 내가 만든 createRouter
export const router = createRouter(document.getElementById('root'),
  [
    {
      pathname: '/',
      element: Home,
    },
    {
      pathname: '/about',
      element: About,
    },
    {
      pathname: '/contact',
      element: Contact,
    },
  ]
);