Portfolio 페이지를 GitHub Pages에 올리면서 메인페이지에 너무 많은 거를 보여주려다 보니 속도 이슈가 생겨 페이지를 분리하여 보여주고자 라우팅을 결심하게 되었습니다.
이번 포스팅에서는 라우팅을 적용한 과정을 공유해보고자 합니다.
react-router-dom
라이브러리를 이용한 페이지 분리BrowserRouter
적용을 위한 추가 설정 for GitHub Pages💡 주의
React Router dom ver.
6.22.3
최신 버전의 react-router-dom을 이용하기 때문에 설정이 조금 다를 수 있습니다.
react-router-dom
패키지를 설치합니다.
# react-router-dom
npm install react-router-dom
# Typescript를 사용할 경우
npm install -D @types/react-router-dom
react-router-dom은 4가지 라우터를 제공합니다.
BrowserRouter
: history API를 이용하여 MPA처럼 보여주는 라우터MemoryRouter
: BrowserRouter를 Test시 사용HashRouter
: URL에 해시(#)를 사용하는 RouterStaticRouter
: SSR(Server-Side Rendering)시 이용하는 Router이중에서도 특히 많이 사용되는 Router는 BrowserRouter
와 HashRouter
입니다.
이 두 Router는 react-router-dom에만 해당되는 이야기는 아닙니다. BrowserRouter는 HTML5의 history API를 사용한 방식이고, HashRouter는 URL의 hash를 사용한 방식입니다.
v6.4 Data APIs 버전부터는 createBrowerRouter, createHashRouter, createMemoryRouter, createStaticRouter 등을 이용하여 라우팅을 설정할 수 있습니다.
그 중에서도 이번포스팅에서는 BrowserRouter를 이용할 것이기 때문에 관련된 설정을 정리해보았습니다.
// 파일 위치: src/index.tsx
// Routing 설정
const router = createBrowserRouter([
{
path: "/",
element: <MainApp/>,
},
{
path: "*",
element: <ErrorApp/>,
},
], {
basename: process.env.PUBLIC_URL
});
root.render(
<React.StrictMode>
<RouterProvider
router={router}
fallbackElement={<Root/>}
/>
</React.StrictMode>
);
createBrowserRouter를 이용하여 routing 설정을 한뒤, RouterProvider를 이용하여 router를 저장합니다.
createBrowserRouter는 함수로 정의는 다음과 같습니다.
function createBrowserRouter(
routes: RouteObject[],
opts?: {
// 도메인의 루트가 아닌 하위 디렉터리에 배포할 수 없는 상황에 대한 앱의 기본 이름
basename?: string;
// 실험실 기능; v7에 추가될 기능들. 마이그레이션을 쉽게 하기 위해 정의됨
future?: FutureConfig;
// SSR(서버 사이드 렌더링)에서 사용하는 옵션
// hydration은 서버에서 내려준 HTML이 리액트 컴포넌트로 관리되기 위한 작업
hydrationData?: HydrationState;
// 브라우저 devtool 플러그인과 같은 환경이나 전역 window.
window?: Window;
}
): RemixRouter;
RouteObject
는 아래와 같이 구성되어 있습니다.
const router = createBrowserRouter([
{
// 접근할 경로
path: "/",
// 보여줄 컴포넌트
element: <Root />,
// 로더
loader: rootLoader,
// 하위 컴포넌트
children: [
{
path: "team",
element: <Team />,
loader: teamLoader,
},
],
},
]);
특이한 점은 Data API
가 제공되는 createBrowserRouter에서는 loader를 통해 각 경로에 렌더링되기 전에 경로 요소에 데이터를 제공하는 로더
기능을 정의할 수 있습니다.
createBrowserRouter([
{
element: <Teams />,
path: "teams",
loader: async () => {
return fakeDb.from("teams").select("*");
},
children: [
{
element: <Team />,
path: ":teamId",
loader: async ({ params }) => {
return fetch(`/api/teams/${params.teamId}.json`);
},
},
],
},
]);
이렇게 세팅된 React 프로젝트를 GitHub Pages에 올려도 다른 페이지로 이동할 수 없는데요, github pages
에서는 BrowserRouter
를 이용할 경우 추가 세팅이 필요합니다.
💡 왜 설정이 필요할까?
GitHub 페이지는 기본적으로 단일 페이지 앱을 지원하지 않습니다.
example.tld/foo와 같이 URL에 대한 새로운 페이지 로드가 있는 경우, GitHub 페이지 서버는 프런트엔드 경로가 어디에 있는지에 대해 아무것도 모르기 때문에 404를 반환합니다
public
디렉터리에 404.html
파일을 생성하고, 아래와 같이 내용을 복사합니다.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Single Page Apps for GitHub Pages</title>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// MIT License
// https://github.com/rafgraph/spa-github-pages
// This script takes the current url and converts the path and query
// string into just a query string, and then redirects the browser
// to the new url with only a query string and hash fragment,
// e.g. https://www.foo.tld/one/two?a=b&c=d#qwe, becomes
// https://www.foo.tld/?/one/two&a=b~and~c=d#qwe
// Note: this 404.html file must be at least 512 bytes for it to work
// with Internet Explorer (it is currently > 512 bytes)
// If you're creating a Project Pages site and NOT using a custom domain,
// then set pathSegmentsToKeep to 1 (enterprise users may need to set it to > 1).
// This way the code will only replace the route part of the path, and not
// the real directory in which the app resides, for example:
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
// https://username.github.io/repo-name/?/one/two&a=b~and~c=d#qwe
// Otherwise, leave pathSegmentsToKeep as 0.
var pathSegmentsToKeep = 0;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + '/?/' +
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>
BrowserRouter를 설정한 소스를 열어줍니다. BrowserRouter 설정에 basename
을 추가해줍니다.
// 파일 위치: src/index.tsx
import {createBrowserRouter, RouterProvider,} from "react-router-dom";
const router = createBrowserRouter([
// ...라우팅설정
], {
basename: process.env.PUBLIC_URL
});
root.render(
<React.StrictMode>
<RouterProvider router={router}/>
</React.StrictMode>
);
저는 webpack의 HtmlWebpackPlugin
을 사용하여 html 템플릿을 만들기 때문에 CopyWebpackPlugin
을 설치하여 아래와 같이 설정을 추가해주었습니다.
npm install copy-webpack-plugin --save-dev
webpack.config.js 파일을 수정합니다.
module.exports = (env, argv) => {
return {
...
plugins: [
new CopyWebpackPlugin({
patterns: [
...
{
from: 'template/404.html',
to: '404.html',
},
]
})
]
}
}
기존에 사용하고 있던 index.html
에 아래의 스크립트를 추가합니다.
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// MIT License
// https://github.com/rafgraph/spa-github-pages
// This script checks to see if a redirect is present
// in the query string,
// converts it back into the correct url and adds it to the
// browser's history using window.history.replaceState(...),
// which won't cause the browser to attempt to load the new url.
// When the single page app is loaded further down in this file,
// the correct url will be waiting in the browser's history for
// the single page app to route accordingly.
(function(l) {
if (l.search[1] === '/' ) {
var decoded = l.search.slice(1).split('&').map(function(s) {
return s.replace(/~and~/g, '&')
}).join('?');
window.history.replaceState(null, null,
l.pathname.slice(0, -1) + decoded + l.hash
);
}
}(window.location))
</script>
<!-- End Single Page Apps for GitHub Pages -->
</head>
<body>
<div id="root"></div>
</body>
</html>
이번 포스팅에서는 최신 버전인 react-router-dom
과 GitHub Pages 세팅에 대해 정리해보았습니다.
관련하여 많은 포스팅이 있지만 react-router-dom
및 GitHub 세팅(index.html
과 404.html
)이 하위 버전인 경우가 많아 정리차원에서 포스팅을 작성하였습니다.😊
읽어주셔서 감사합니다.