sehyun.dev
React

React에서 직접 구현해보는 react-router 원리

2025년 4월 17일

서론

이번 글에서는 History API를 활용해 간단한 라우팅 시스템을 직접 구현해보겠습니다.

본문

  • Link는 사용자가 클릭하면 URL을 변경하는 역할을 합니다. History API의 pushState를 사용해 페이지 새로고침 없이 경로를 업데이트하고, 커스텀 이벤트 pushstate를 발생시켜 경로 변경을 알립니다.
export const Link = ({ to, children }) => {
  const handleClick = (event) => {
    event.preventDefault();
    window.history.pushState({}, '', to);
    window.dispatchEvent(new Event('pushstate'));
  };
  return <a href={to} onClick={handleClick}>{children}</a>;
};

(2) Router 컴포넌트: 경로 추적과 렌더링

  • Router는 현재 경로를 추적하고, 그에 맞는 컴포넌트를 렌더링합니다. useEffect로 pushstate와 popstate 이벤트를 감지해 경로가 바뀔 때마다 상태를 업데이트합니다.
export const Router = ({ children }) => {
  const [path, setPath] = useState(window.location.pathname);
  
  useEffect(() => {
    const handleLocationChange = () => setPath(window.location.pathname);
    window.addEventListener('pushstate', handleLocationChange);
    window.addEventListener('popstate', handleLocationChange);
    return () => {
      window.removeEventListener('pushstate', handleLocationChange);
      window.removeEventListener('popstate', handleLocationChange);
    };
  }, []);
  
  const routes = Children.toArray(children);
  const activeRoute = routes.find((child) => child.props.path === path);
  return activeRoute ? cloneElement(activeRoute) : null;

(3) Route 컴포넌트: 경로별 컴포넌트 매핑

  • Route는 단순히 전달받은 컴포넌트를 렌더링합니다. 경로 매핑 로직은 Router가 처리합니다.
const Route = ({ component: Component }) => <Component />;

(4) 전체 코드

import { Children, cloneElement, useEffect, useState } from 'react';
import './App.css';
 
/**
 * Link 컴포넌트 클릭시 history API의 pushState를 사용하여 현재 이동하고자 하는 경로를 to로 전달한다.
 * 이때 pushstate 이벤트를 발생
 */
export const Link = ({ to, children }) => {
  const handleClick = (event) => {
    event.preventDefault();
    // pushState 스택에 누적
    window.history.pushState({}, '', to);
 
    // pushstate 이벤트 발생
    window.dispatchEvent(new Event('pushstate'));
  };
 
  return <a style={{ color: 'red', width: '100px', display: 'inline-block' }} href={to} onClick={handleClick}>{children}</a>;
};
 
/**
 * Router 컴포넌트는 현재 경로를 추적하고 이를 자식 컴포넌트에 전달한다.
 * 이때 pushstate 이벤트를 발생시키면 현재 경로를 추적하고 이를 자식 컴포넌트에 전달한다.
 */
export const Router = ({ children }) => {
  const [path, setPath] = useState(window.location.pathname);
 
  useEffect(() => {
    const handleLocationChange = () => {
      console.log(window.history)
      setPath(window.location.pathname);
    };
 
 
    window.addEventListener('pushstate', handleLocationChange);
    window.addEventListener('popstate', handleLocationChange);
    return () => {
      window.removeEventListener('pushstate', handleLocationChange);
      window.removeEventListener('popstate', handleLocationChange);
    };
  }, []);
 
  const routes = Children.toArray(children);
  const activeRoute = routes.find((child) => child.props.path === path);
 
  return activeRoute ? cloneElement(activeRoute) : null;
};
 
const Route = ({ component: Component }) => {
  return <Component />;
};
 
export const Home = () => {
  return <div>Home</div>;
};
 
export const About = () => {
  return <div>About</div>;
};
 
function App() {
  return (
    <>
      <nav>
        <Link to="/home">Home</Link> | <Link to="/about">About</Link>
      </nav>
 
      <Router>
        <Route path="/home" component={Home} />
        <Route path="/about" component={About} />
      </Router>
    </>
  );
}
 
export default App;

마무리

이 코드는 react-router의 핵심 원리를 간단히 구현한 예제입니다. History API와 React의 상태 관리를 활용해 SPA 라우팅을 직접 만들어보며, 실제 라이브러리의 동작 방식을 이해할 수 있었습니다.