본문 바로가기
TIL

11. 11. 25일차 TIL 라이브러리 없이 라우터(Router) 만들기

by 눈 새 2024. 11. 11.

오늘은 챌린지 반 수업을 통해 react 라이브러리 없이 라우터(Rounter)를 만드는 방법에 대해 학습하였다.


1. 라우팅 (복습)

라우팅(Routing)은 애플리케이션에서 클라이언트의 요청 (URL)에 따라 적절한 콘텐츠나 페이지를 제공하는 과정을 의미한다. 주로 SPA(Single Page Application)에서 사용되며, 사용자가 URL을 변경할 때 페이지를 새로 고침하지 않고도 콘텐츠를 동적으로 업데이를 할 수 있게 한다.

 

1) 주요 요소

  • URL 매핑 : 특정 URL 경로를 정의하고, 해당 경로에 맞는 컴포넌트나 페이지를 연결한다
  • 해시 라우팅 : URL의 해시(#)를 사용하여 페이지 전환을 관리한다.
  • 히스토리 관리 : 사용자가 뒤로 가기, 앞으로 가기와 같은 내비게이션을 할 수 있도록 브라우저의 히스토리 API를 사용하여 상태를 관리한다.
  • 동적 파라미터 : URL의 일부를 동적으로 받아와서 해당 값에 따라 콘텐츠를 조정할 수 있다.
  • 404 처리 : 유효하지 않은 URL에 대한 요청이 들어올 때 적절한 메시지나 페이지를 표시한다.

✔ 라우팅은 웹 애플리케이션에서 사용자 요청을 처리하고 적절한 콘텐츠를 제공하는 중요한 과정으로 사용자 경험을 개선하고 애플리케이션의 구조를 관리하는 데 필수적인 기능이다.

 

2. 데이터 로딩과 loader, defer 활용

loader와 defer은 React Router v6.4부터 도입된 기능으로 데이터 패칭을 더욱 간편하고 효율적으로 할 수 있게 도와준다.

 

1) Loader 

loader는 특정 라우트에 필요한 데이터를 서버에서 미리 로드하는 함수이다. 이 데이터를 로드하는 동안 사용자에게 로딩 상태를 표시할 수 있다. 이를 통해 페이지가 렌더링되기 전에 필요한 데이터를 준비할 수 있다.

더보기
import { createBrowserRouter, RouterProvider, Route } from 'react-router-dom';

// 데이터 로딩 함수
const loadData = async () => {
    const response = await fetch('/api/data');
    if (!response.ok) {
        throw new Error('데이터 로딩 실패');
    }
    return response.json();
};

// 컴포넌트 정의
const DataDisplay = ({ data }) => (
    <div>
        <h1>데이터</h1>
        <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
);

// 라우터 설정
const router = createBrowserRouter([
    {
        path: '/',
        element: <DataDisplay />,
        loader: loadData, // loader 설정
    },
]);

const App = () => {
    return (
        <RouterProvider router={router} />
    );
};

export default App;

 

2) defer 

defer는 데이터 로딩을 더 세분화하여 필요한 데이터를 비동기적으로 로드할 수 있는 함수이다. 이는 페이지의 일부가 로드되는 동안 다른 데이터도 동시에 로드할 수 있다.

더보기
import { createBrowserRouter, RouterProvider, Route } from 'react-router-dom';
import { Suspense } from 'react';

// 데이터 로딩 함수
const loadData = async () => {
    const response = await fetch('/api/data');
    if (!response.ok) {
        throw new Error('데이터 로딩 실패');
    }
    return response.json();
};

// 비동기 컴포넌트
const AsyncComponent = React.lazy(() => import('./AsyncComponent'));

// 라우터 설정
const router = createBrowserRouter([
    {
        path: '/',
        element: (
            <Suspense fallback={<div>로딩 중...</div>}>
                <AsyncComponent />
            </Suspense>
        ),
        loader: loadData, // loader 설정
    },
]);

const App = () => {
    return (
        <RouterProvider router={router} />
    );
};

export default App;

 

3) 데이터 로딩과 상태 관리

라우터에 설정한 loader는 해당 경로가 활성화될 때마다 실행되며 데이터를 로딩하고 그 결과를 라우트의 props로 전달한다. 

 

Loader 특정 라우트에 필요한 데이터를 로드하고, 해당 데이터가 준비될 때까지 기다린 후 페이지를 렌더링한다.

Defer : 데이터를 비동기적으로 로드하여 페이지의 다른 부분이 로드되는 동안 추가 데이터를 가져온다.


3. Router 만들기 !

1) SPA 구축하기

// index.html

<body>
  <header>
    <button data-navigate="/">home</button>
    <button data-navigate="/melon">melon</button>
    <button data-navigate="/melon/IU/raindrop">detail</button>
  </header>
  <main></main>
  <script type="module" src="index.js"></script>
</body>
// index.js

// router를 SPA에 연결

import createRouter from "./router/router.js";

// SPA 구축
const container = document.querySelector("main");
const pages = {
  home: () => (container.innerText = "home page"),
  melon: () => (container.innerText = "melon page"),
  board: (params) => (container.innerText = `${params.name} ${params.song}`),
};

const router = createRouter();

router
  .addRoute("#/", pages.home)
  .addRoute("#/melon", pages.melon)
  .addRoute("#/melon/:name/:song", pages.board)
  .start();

window.addEventListener("click", (e) => {
  if (e.target.matches("[data-navigate]")) {
    router.navigate(e.target.dataset.navigate);
  }
});
// router.js

// :name, "song", 등 path parameters를 매칭하기 위한 정규표현식
const ROUTE_PARAMETER_REGEXP = /:(\w+)/g;
const URL_REGEXP = "([^\\/]+)";

// Router
export default function createRouter() {
  // App의 경로 목록들을 담을 배열
  // routes : App의 경로 목록을 수집하는 레지스트리
  const routes = [];

  // router : 라우터를 구현한 객체로 기능들을 메서드로 추상화
  const router = {
  
    // 기능 1. App의 경로 목록을 저장
    // addRoute : routes 배열에 URL과 구성 요소들을 매핑하여 "저장"하기 위한 메서드
    addRoute(fragment, component) {
      const params = [];
      const parsedFragment = fragment
        .replace(ROUTE_PARAMETER_REGEXP, (_, paramName) => {
          // path parameter 이름을 추출 후 배열에 추가 ["name, song"]
          params.push(paramName);

          // path parameter에 매치되는 문자를 URL_REGEXP로 치환
          return URL_REGEXP;

          // "/"의 텍스트로써 사용을 위해 모든 "/"앞에 이스케이프 문자 ("\")를 추가함.
        }).replace(/\//g, "\\/");

      routes.push({
        fragmentRegExp: new RegExp(`^${parsedFragment}$`),
        component,
        params,
      });

      return this;
    },

    // 기능 2. 현재 URL이 변경되면 페이지 콘텐츠를 해당 URL에 매핑된 구성 요소로 교체
    // URL 변경을 "청취"하는 메서드
    start() {
      const getUrlParams = (route, hash) => {
        const params = {};
        const matches = hash.match(route.fragmentRegExp);

        if (matches) {
          // 배열의 첫번째 값에는 url 전체가 담겨있으므로 먼저 제거해준다.
          matches.shift(); 
          matches.forEach((paramValue, index) => {
            const paramName = route.params[index];
            params[paramName] = paramValue;
          });
        }
        // params = {name: 'IU', song: 'raindrop'} 가 맞는지 확인
        console.log([params]);
        return params;        
      };

      const checkRoutes = () => {
        const currentRoute = routes.find((route) =>
          route.fragmentRegExp.test(window.location.hash)
        );

        if (currentRoute.params.length) {
          // path parameters가 있는 url인 경우
          const urlParams = getUrlParams(currentRoute, window.location.hash);
          currentRoute.component(urlParams);
        } else {
          currentRoute.component();
        }
      };

      // 브라우저에서 hash값이 바뀔 때 발생하는 이벤트
      window.addEventListener("hashchange", checkRoutes);
      checkRoutes();
    },

    // 버튼이 클릭되면 브라우저의 URL의 #을 포함하는 뒷부분을 변경 및 전환
    navigate(fragment, replace = false) {
      if (replace) {
        const href = window.location.href.replace(
          window.location.hash,
          "#" + fragment
        );
        window.location.replace(href);
      } else {
        window.location.hash = fragment;
      }
    },
  };
  return router;
}

 

하지만 위와 같이 Router를 구현하는 것에는 가장 큰 문제가 있다. router의 주요 요소를 모두 충족할 수 없다는 것이다. 버튼을 클릭하면 URL도 변하고 그에 맞는 페이지로 전환된다고 생각할 수도 있다. 하지만 histroy API를 사용하고 있지 않기 때문에 페이지가 전환되는 것처럼 보이지만 전 페이지로 돌아간다거나, 다음 페이지로 이동한다거나 하는 기능을 사용할 수 없다. 그렇게 때문에 튜터님께서도 시간이 된다면 다른 방법으로도 기능을 구현해보라고 말씀해주셨다. 이번 주말에는 오늘 챌린지반 강의를 복습하며 다른 방법으로 구현해봐야겠다.


★ 24일차 소감

 

오늘은 Router에 대해 조금 더 깊이있게 알게 되었다. 내일은 예비군훈련이 있는 날이다. 갔다오면 React 개인과제에 몰두해서 필수 기능은 모두 구현해야겠다.. 지난 주말을 활용하여 react 과제를  빨리 끝내고 챌린지반 개인프로젝트를 구상하고 싶었는데.. 김장철이라 주말 내내  무도 뽑고.. 씻고.. 옮기고.. 복도도 치우고.. 쉽지 않다..  계획했던 것보다 시간이 너무 쪽박해졌다😥  이번주 주말에는 반드시 이번주 학습내용을 복습하고 다른 방식으로 router를 구현해봐야겠다. 그러고보니 이제 슬슬 다음에 진행할 팀프로젝트에 대해서도 고민해야겠다.. 이번 개인과제가 아쉬운 만큼 팀프로젝트만은 정말 이쁘게 만들고 싶다..