Vanilla React로 Router 구현하기 - useRouter 훅 분리하기

지은·2023년 7월 7일
0

🚂 토이 프로젝트

목록 보기
9/10

이전 글) [원티드 프리온보딩 챌린지] React에서 History API 사용하여 SPA Router 기능 구현하기

useRouter 훅으로 분리하기

App.js 안에 모든 코드를 작성했는데, 이를 Router, Route 컴포넌트, useRouter() 훅으로 분리하려고 한다.

📁 src
⎿ 📄 App.js
⎿ 📄 index.js
⎿ 📁 component
   ⎿ 📄 Root.js
   ⎿ 📄 About.js
   ⎿ 📄 Router.js
   ⎿ 📄 Route.js
⎿ 📁 hooks
   ⎿ 📄 useRouter.js

index.js

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <Router>
    <Route path="/" component={<Root />} />
    <Route path="/about" component={<About />} />
  </Router>
);

Router.js

function Router({ children }) {
  return children;
}
export default Router;

Route.js

function Route({ path, component }) {
  const { currentPath } = useRouter();
  return currentPath === path ? component : null;
  // pathname이 /이라면 <Home /> 컴포넌트를 렌더링한다.
  // pathname이 /about이라면 <About /> 컴포넌트를 렌더링한다.
}

export default Route;

useRouter.js

import { useState, useEffect } from "react";

export const useRouter = () => {
  const [currentPath, setCurrentPath] = useState(window.location.pathname);

  useEffect(() => {
    const handlePopState = () => {
    setCurrentPath(window.location.pathname);
  	}
    
    window.addEventListener("popstate", handlePopState);

    return () => {
      window.removeEventListener("popstate", handlePopState);
    };
  }, []);

  const updateURL = (path) => {
    window.history.pushState(null, "", path);
    setCurrentPath(window.location.pathname);
  };

  return { updateURL, currentPath };
};

About.js

import { useRouter } from "../hooks/useRouter";

const About = () => {
  const { updateURL } = useRouter();

  return (
    <div>
      <h1>About</h1>
      <button onClick={() => updateURL("/")}>Home으로 가기</button>
    </div>
  );
};

export default About;

이렇게 useRouter 훅으로 분리하고 나니
"About으로 가기" 버튼을 눌렀을 때 updateURL() 함수가 실행되고, currentPath 상태가 '/about'으로 업데이트되지만,
<Root> 컴포넌트는 currentPath 상태를 사용하고 있지 않기 때문에 리렌더링을 수행하지 않는다.

useRouter 훅의 코드를 다시 살펴보자.

export const useRouter = () => {
  const [currentPath, setCurrentPath] = useState(window.location.pathname);

  useEffect(() => {
    const handlePopState = () => {
    setCurrentPath(window.location.pathname);
  	}
    
    window.addEventListener("popstate", handlePopState);

    return () => {
      window.removeEventListener("popstate", handlePopState);
    };
  }, []);

  const updateURL = (path) => {
    window.history.pushState(null, "", path);
    setCurrentPath(window.location.pathname); // ❌
  };

  return { updateURL, currentPath };
};

경로에 따른 알맞은 컴포넌트가 렌더링되게 하려면 popState 이벤트가 발생해야 한다.
하지만 updateURL() 함수 내에서 setCurrentPath()로 상태를 업데이트해주면, popState 이벤트가 발생하지 않고, currentPath 상태값만 업데이트해주는 것이다.

그렇다면 updateURL() 함수를 어떻게 수정해야할까?
X 표시한 줄에서 직접 상태를 업데이트시켜주는 것이 아닌 popState 이벤트가 발생하도록 해줘야 한다.

커스텀 이벤트 생성하기

new Event() 생성자를 이용해 이벤트를 만들고, dispatchEvent(event) 메소드를 사용해 이벤트를 발생시킬 수 있다.
Creating and triggering events
EventTarget.dispatchEvent()

const updateURL = (path) => {
    window.history.pushState(null, "", path);
    const event = new Event("popstate"); // 이벤트 생성
    dispatchEvent(event); // 이벤트 트리거
  };

이제 updateURL() 함수가 실행될 때마다 url 변경과 popState 이벤트가 발생하고, currentPath가 업데이트되며 화면이 제대로 리렌더링되는 것을 확인할 수 있다.

profile
블로그 이전 -> https://janechun.tistory.com

4개의 댓글

comment-user-thumbnail
2023년 7월 7일

react 뜯어보는 게 참 재밌는거 같아요. 로직 계속 봤는데도, 어렵네요 ㅠㅠ
훅으로 쓰면 내보낼 때 state 값이 아니라 순수하게 string으로 내보내는거같아요 신기하네요.

답글 달기
comment-user-thumbnail
2023년 7월 8일

이렇게 한번 뜯어보면 확실히 이해가 잘 될것 같아요 !! 고생하셨습니당

답글 달기
comment-user-thumbnail
2023년 7월 9일

우오... 어렵네요.. 고생하셨습니당

답글 달기
comment-user-thumbnail
2023년 7월 9일

오 ,, 이해하기 쉽지많은 않군요 ,,,

답글 달기