경로에 따라 다른 view 화면을 보여주는 router는 브라우저에서 페이지를 이동할 수 있게 해주는 비행기와도 같다고 생각합니다. 이번 시간에는 라우터 동작 원리에 대해 알아보고 라이브러리를 활용과 라이브러리를 활용하지 않고 페이지 이동을 구현해보겠습니다.
브라우저는 아래와 같이 실행합니다.
이동하고자 하는 페이지가 이전 페이지와 다른 점이 없다면 위와 같은 일련의 과정을 재실행하는건 효율적이지 않습니다.
SPA는 초기에 필요한 모든 정적 리소스를 한번에 다운로드 받고 페이지 이동 시 업데이트가 필요한 데이터만 서버와 통신합니다.
따라서 spa에서는 브라우저로 페이지를 이동하고 뒤로가기 기능을 수행할 때 새로고침이 일어나지 않는 것이 중요합니다.
이 점을 고려하여 대표적인 리액트 라우터 라이브러리인 react-router-dom에 대해 자세히 알아봅시다 :)
React router dom을 클라이언트 측 라우팅이라고 표현하는 이유는 서버에서 화면 전환과 주소 값 변경을 하지 않고 클라이언트 측에서 수행하기 때문인데요, 서버에 필요한 리소스 관련 문서 전체를 재요청하지 않고 업데이트가 url 변경을 통해 필요한 부분만 ui 변경과 데이터 업데이트가 가능합니다. 앱은 일부 새 ui를 즉시 랜더링하고 데이터 요청을 통해 패칭한 데이터로 변경된 내용을 사용자에게 보여줄 수 있습니다.
설치
npm install react-router-dom@6
router.js 파일 생성하고 createBrowserRouter 사용하여 경로별로 보여줄 요소 지정
import {createBrowserRouter} from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: (
<div>
리액트 라우팅 돔 연습 :)
</div>
// 혹은 컴포넌트
)
}
]);
export default router;
import { RouterProvider } from "react-router-dom";
import router from "./Routes/router";
function App() {
return (
<div className="App">
<RouterProvider router={router}/>
</div>
);
}
export default App;
위와 같이 경로에 따라 페이지 이동
페이지 이동 시 브라우저가 서버에 문서를 재요청하고 모든 css, html을 다시 그리는 것이 아닌, 새 ui를 보여주는 react router dom은 페이지 내에서 서브 페이지 이동이 가능합니다. 예를 들어 쇼핑몰에서 배너를 클릭하면 전체 페이지가 바뀌지 않고 배너 내용이 나오는 섹션의 내용만 변경되도록 구현할 수 있습니다. 이동된 페이지에서 또다른 서브 페이지로 이동하여 새로운 컴포넌트를 보여줄 수 있습니다.
import {createBrowserRouter} from 'react-router-dom';
import FruitPage from '../Pages/fruitPage';
import MainPage from '../Pages/mainPage';
const router = createBrowserRouter([
{
path: '/',
element: (
<MainPage />
),
},
{
path: 'fruit',
element: <FruitPage />,
// 🖐️ children 추가하기
children: [
{
path: '사과',
element : (
<div>🍎</div>
)
},
{
path: '바나나',
element: (
<div>🍌</div>
)
},
{
path: '오렌지',
element: (
<div>🍊</div>
)
},
]
}
]);
export default router;
import { Outlet, useNavigate } from "react-router-dom";
import {styled} from "styled-components";
function FruitPage() {
const navigate = useNavigate();
const onClickBtn = (e) => {
navigate(`/fruit/${e.target.innerText}`)
};
return (
<>
<div>show me the 과일</div>
<S.Flex>
<button onClick={onClickBtn}>사과</button>
<button onClick={onClickBtn}>바나나</button>
<button onClick={onClickBtn}>오렌지</button>
</S.Flex>
<Outlet /> // 🖐️ Outlet 추가
</>
)
}
export default FruitPage;
시현영상
https://github.com/HyunseoKoo/React-router-dom/assets/117560047/1759a58f-bf86-45a2-9141-059b60059a86
위의 예시를 통해 react-router-dom이라는 라이브러리를 활용하여 일반적인 라우터 기능을 살펴보았습니다. 라이브러리를 활용하면 편리하기는 하지만 라우터를 직접 커스터마이징하고 메서드 추가가 필요한 경우 라이브러리에만 의존할 수 없습니다. 이런 경우 직접 라우터를 만들어야하는데요, 이에 대해 알아보겠습니다 :)
대표적으로 두가지 방법이 있습니다.
요즘은 history API를 활용하여 주로 페이지 이동 기능을 구현하지만 모두 브라우저에서 지원되는 라우터를 만들기 위해서는 Fragment 해시를 이용하는 방법도 필요합니다.
우리가 흔히 sns에서 사용하는 해시태그 #를 url 주소에서 활용하여 경로(fragment)를 지정해줍니다. fragment 별로 보여줄 콘텐츠 value를 설정하여 지정 fragment가 변하면 그에 따른 내용들도 업데이트 되는 원리입니다.
우선, 리액트를 사용하지 않고 일반 html 파일로 해시 라우터 작동에 대해 알아보겠습니다.
파일 구조
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPA 라우터 구현</title>
</head>
<body>
<header>
<a href="#/">home</a>
<a href="#/fruit">fruit</a>
</header>
<main></main>
<script type="module" src="mainPage.js"></script>
</body>
</html>
a 태그를 활용하여 태그 클릭시 url 주소를 변경되도록 했습니다. 변경되는 주소는 "#/"로 설정이 가능합니다.
const container = document.querySelector("main")
const pages = {
home: () => container.innerText = "과일 페이지가 나와야할텐데..",
fruit: () => container.innerText = "🍎🍌🍊"
};
위의 index 파일에서 설정한 main 태그에 추가할 내용에 대한 값들을 정해줍니다. pages 객체의 value들은 main 태그의 value로 활당되어 해당값들이 화면에 랜더링됩니다.
export default function createRouter() {
const routes = []; // 경로 목록을 수집하는 저장소 역할의 배열. url을 구성 요소에 매핑(저장/수집..?!)한 객체들을 저장하는 배열.
// 라우터를 구현한 메서드 객체
const router = {
// 메서드1 역할 ? 애플리케이션의 경로 목록들을 저장. routes에 url & 구성 요소들을 매핑해 저장하기 위함.
addRoute(fragment, component) {
routes.push({fragment, component});
console.log(routes);
return this; // 여기서의 this는 global object로 브라우저에서 호출하는 경우 window를 가리킴.
},
// 메서드2 역할 ? 현재 url이 변경되면 페이지 콘텐츠를 해당 url에 매핑된 구성 요소로 교체. url 변경을 청취하기 위함.
start() {
const checkRoutes = () => {
const currentRoute = routes.find(route => route.fragment === window.location.hash);
currentRoute.component();
}
window.addEventListener('hashchange', checkRoutes);
checkRoutes();
}
};
return router;
}
router를 구현할 메서드를 만들어줍니다. addRoute, start 메서드를 만들어줌으로써 변경되는 Url 주소의 해시값을 저장하고 해시가 변경되었을 때 routes 배열에 저장했던 객체들 중 저장한 해시값에 해당하는 키값을 찾아 그에 맞는 컨텐츠를 보여줍니다.
import createRouter from "./router.js";
const container = document.querySelector("main")
const pages = {
home: () => container.innerText = "과일 페이지가 나와야할텐데..",
fruit: () => container.innerText = "🍎🍌🍊"
};
const router = createRouter();
router.addRoute("#/", pages.home)
.addRoute("#/fruit", pages.fruit)
.start();
url 주소에 담기는 해시와 그에 따라 보여질 컨텐츠 내용을 저장하고, 매핑하여 url 변경시 해시를 감지하여 해당하는 값을 보여주는 createRouter를 적용해줍니다.
해시 라우터는 우리가 알고 있는 해시태그 #를 활용하여 url에 들어갈 경로(단어)를 설정하고 그에 해당하는 컨텐츠를 보여주는 방식입니다.
react-router-dom은 history api를 사용하는 라이브러리입니다(ex. useNavigate). 라이브러리에서 자동으로 설정해주기에 평소에는 모르고 사용했으나 이번 기회에 history api에 대해 깊이 알아보도록 하겠습니다.
dom의 window 객체는 history 객체를 통해 브라우저 세션 기록에 접근할 수 있도록 해줍니다. history는 유저를 방문 기록 전과 후로 이동할 수 있게 해주고 컨텐츠 내용을 저장할 수 있도록 해줍니다.
아래는 window.history.pushState(pageName, "", "/콜라")에 대한 history객체입니다.
페이지 이동에 활용되는 history api 메서드에 대해 알아봅시다.
history.pushState(state, title, url)
현재 문서와 관련된 또 다른 세션 기록 항목(객체? 브라우저에서 관리?)을 생성하고 활성화합니다. window.location( = "#foo")와 역할은 같으나 location과 달리 pushState는 http 요청을 호출하지 않습니다. 그렇기에 페이지 새로고침도 일어나지 않습니다.
cf. 만약 페이지 이동과 동시에 새로고침이 필요한 상황이라면 window.location을 사용하면 됩니다.
세션 기록 항목에서 활성 기록 항목이 바뀔때 페이지 이동 이벤트를 적용해줍니다.
pushState()를 실행했을 때 onPopState()는 발생되지 않고 페이지 이동(앞으로가기/뒤로가기) 시 해당 이벤트가 일어나게 됩니다.
아래는 두개의 메서드를 활용한 예시입니다.
폴더구조
pushState와 popState 활용하여 클릭 이벤트 추가
import { useEffect, useState } from "react";
import MainPage from "./Pages/mainPage";
import FruitPage from "./Pages/fruitPage";
import FoodPage from "./Pages/foodPage";
function App() {
const [pageName, setPageName] = useState();
useEffect(() => {
window.onpopstate = (e) => {
console.log(e);
setPageName(e.state);
}
},[pageName]);
const onChangePage1 = () => {
const pageName = 'fruit';
window.history.pushState(pageName, "", "/fruit"); // pushState(페이지 이동할때 전달할 데이터, 변경 브라우저 제목, 변경 주소)
setPageName(pageName);
}
const onChangePage2 = () => {
const pageName = 'food';
window.history.pushState(pageName, "", "/food");
setPageName(pageName);
}
return(
<>
{!pageName && <MainPage />}
<button onClick={onChangePage1}>fruit</button>
<button onClick={onChangePage2}>food</button>
{pageName === 'fruit' && <FruitPage />}
{pageName === 'food' && <FoodPage />}
</>
)
}
export default App;
이동하고자 하는 페이지를 state로 상태 관리해줍니다. window.onpopstate를 통해 popStateEvent의 값으로 pageName을 변경해줍니다.
// 메인페이지
function MainPage() {
return (
<>
<div>
💡react-router-dom 없이 라우팅 구현하기
</div>
</>
)
}
export default MainPage;
// 푸드페이지
import { useEffect, useState } from 'react';
import {styled} from 'styled-components';
function FoodPage() {
const [pageName, setPageName] = useState();
useEffect(() => {
window.onpopstate = (e) => {
console.log(e);
setPageName(e.state);
}
},[pageName]);
const onChangePage1 = () => {
const pageName = '콜라';
window.history.pushState(pageName, "", "/콜라"); // pushState(페이지 이동할때 전달할 데이터, 변경 브라우저 제목, 변경 주소)
setPageName(pageName);
}
const onChangePage2 = () => {
const pageName = '햄버거';
window.history.pushState(pageName, "", "/햄버거");
setPageName(pageName);
}
const onChangePage3 = () => {
const pageName = '감자튀김';
window.history.pushState(pageName, "", "/감자튀김");
setPageName(pageName);
}
return (
<>
<div>show me the 과일</div>
<S.Flex>
<button onClick={onChangePage1}>콜라</button>
<button onClick={onChangePage2}>햄버거</button>
<button onClick={onChangePage3}>감자튀김</button>
</S.Flex>
{pageName === '콜라' && '🥤'}
{pageName === '햄버거' && '🍔'}
{pageName === '감자튀김' && '🍟'}
</>
)
}
export default FoodPage;
const Flex = styled.div`
display: flex;
`;
const S = {
Flex
};
// 과일페이지
import { useEffect, useState } from 'react';
import {styled} from 'styled-components';
function FruitPage() {
const [pageName, setPageName] = useState();
useEffect(() => {
window.onpopstate = (e) => {
console.log(e);
setPageName(e.state);
}
},[pageName]);
const onChangePage1 = () => {
const pageName = '사과';
window.history.pushState(pageName, "", "/사과"); // pushState(페이지 이동할때 전달할 데이터, 변경 브라우저 제목, 변경 주소)
setPageName(pageName);
}
const onChangePage2 = () => {
const pageName = '바나나';
window.history.pushState(pageName, "", "/바나나");
setPageName(pageName);
}
const onChangePage3 = () => {
const pageName = '오렌지';
window.history.pushState(pageName, "", "/오렌지");
setPageName(pageName);
}
return (
<>
<div>show me the 과일</div>
<S.Flex>
<button onClick={onChangePage1}>사과</button>
<button onClick={onChangePage2}>바나나</button>
<button onClick={onChangePage3}>오렌지</button>
</S.Flex>
{pageName === '사과' && '🍎'}
{pageName === '바나나' && '🍌'}
{pageName === '오렌지' && '🍊'}
</>
)
}
export default FruitPage;
const Flex = styled.div`
display: flex;
`;
const S = {
Flex
};```

이외에도 여러 페이지 이동하는 history api 메서드가 있습니다.
세션 기록에서 한 항목(단계) 뒤로 이동하게 해줍니다.
document.getElementById("go-back").addEventListener("click", () => {
history.back();
});
back 메서드와 달리 세션 기록에서 한 항목 앞으로 이동하게 해줍니다.
document.getElementById("go-forward").addEventListener("click", (e) => {
history.forward();
});
매개변수의 값에 따라 히스토리 페이지를 전후로 이동할 수 있게 해줍니다.
현재 페이지 기준으로 이동할 페이지의 위치를 매개변수로 받습니다.
history.go(-1); // 1페이지 뒤로 이동
history.go(1); // 1페이지 앞으로 이동
history.go();
history.go(0); // 두개 모두 현재 페이지 reload
history.replace(state, title, url); // 원하는 페이지로 이동하지만 pushState와 달리 세션 기록 항목에 새로운 항목을 추가하지 않고 아예 현재 기록 항목으로 덮어쓴다는(대체) 차이점이 있습니다.
이렇게 해서 리액트에서 라이브러리 없이 브라우저 페이지 이동 방법을 알아보았습니다. 웹사이트 구현할때 페이지 이동과 함께 자주 구현하는 기능으로는 사용자가 게시글을 작성할 때 페이지를 이동하게 되면 기록중인 데이터를 저장하거나 삭제할지 물어보는 기능인데요, 어떻게 구현할 수 있는지 코드로 알아보겠습니다 :)
const preventGoBack = () => {
window.history.replaceState(null, "", location.href);
alert("페이지 이동 시 입력하신 내용이 저장되지 않을 수 있습니다.");
};
window.onpopstate = (e) => {
preventGoBack();
}
시현 영상
https://github.com/HyunseoKoo/React-router-dom/assets/117560047/2675b103-16e8-4915-80dc-23570d21edbf
replaceState는 새로고침 없이 즉시 경로 이동 가능!
const preventGoBack = () => {
history.pushState(null, '', location.href);
const result = confirm(
'페이지 이동 시 입력하신 내용이 저장되지 않을 수 있습니다.',
);
if (result === true) {
navigate('/');
}
};
useEffect(() => {
history.pushState(null, '', location.href);
window.addEventListener('popstate', preventGoBack);
return () => {
window.removeEventListener('popstate', preventGoBack);
};
}, []);
const preventClose = e => {
e.preventDefault();
e.returnValue = '';
};
useEffect(() => {
(() => {
window.addEventListener('beforeunload', preventClose);
})();
return () => {
window.removeEventListener('beforeunload', preventClose);
};
}, []);
시현 영상
https://github.com/HyunseoKoo/React-router-dom/assets/117560047/65d11ea3-d77b-4c79-a860-879743d00e38
pushState는 useNavigate()와 함께 사용하여 새로고침 없이 페이지 이동 가능!
종종 뒤로가기를 잘못 클릭해서 작성중이던 게시글이 저장되지 않고 페이지 이동이 일어나는 경우가 있습니다. 모달 알림창 없이 뒤로가기 방지가 가능합니다. (아예 뒤로가기를 못하게 해서 작성중인 내용이 날아가지 않도록 하는 극단적인 방법입니다..ㅎㅎ..)
useNavigate로 페이지 이동 경로를 설정할때, 인자를 설정해주면 이동한 페이지에서는 뒤로가기를 할 수 없도록 할 수 있습니다.
<S.GoToSignup onClick={
()=> navigate("/form/signup", {replace: true})}>회원가입</S.GoToSignup>
위와 같이 navigate의 인자로 {replace: true}를 추가하게 되면 /form/signup 경로로 이동한 회원가입 페이지에서는 뒤로가기를 방지할 수 있습니다.
https://reactrouter.com/en/main/start/overview
https://tturbo0824.tistory.com/131
https://fe-developers.kakaoent.com/2022/221124-router-without-library/
https://ljh86029926.gitbook.io/coding-apple-react/1/where-is-html
https://velog.io/@rlawogks2468/react-router-dom-%EC%97%86%EC%9D%B4-%EB%9D%BC%EC%9A%B0%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0
https://developer.mozilla.org/ko/docs/Web/API/History_API
https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
https://falsy.me/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%9D%98-%EC%9D%B4%ED%95%B4-2-%ED%9E%88%EC%8A%A4%ED%86%A0%EB%A6%AC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-history-api/