React
React에서 직접 구현해보는 react-router 원리
2025년 4월 17일
서론
이번 글에서는 History API를 활용해 간단한 라우팅 시스템을 직접 구현해보겠습니다.
본문
(1) Link 컴포넌트: 경로 이동의 시작점
- 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 라우팅을 직접 만들어보며, 실제 라이브러리의 동작 방식을 이해할 수 있었습니다.