Create React App Docs를 보면 client-side routing에 대한 노트가 적혀 있는데, GitHub Pages는 내부적으로 HTML5 pushState history API를 사용하는 라우터를 지원하지 않는다고 나와있다.
HTML5 history API는 사용자가 방문한 페이지의 기록을 관리해주는 기능을 제공하는 웹 브라우저 API로 window.history.back(), forward(), go() 등의 메서드를 이용해 브라우저 뒤로 가기, 앞으로 가기 등의 기능을 구현할 수 있게 해준다.
history API는 pushState(), replaceState()라는 메서드를 제공하는데 pushState는 히스토리 엔트리에 새로운 페이지 엔트리를 추가, replaceState는 페이지 새로 고침 없이 현재의 히스토리 엔트리를 변경하여 URL만 이동할 수 있도록 해준다.
내가 사용하려던 Browser Router가 history API를 사용하여 UI 업데이트를 하는데, GitHub Pages에서 history API 기능을 제공하지 않기 때문에 새로운 페이지 렌더링 없이 URL만 이동하는 것이 불가능하다. 즉, http://user.github.io/reps/todos/1 주소 입력을 했을 때, URL 변경만 이루어지는 것이 아닌 Github Pages server로 해당 서브 디렉토리의 라우팅을 요청하게 되고, server는 히스토리 정보를 알지 못하기 때문에 404 에러를 반환하게 된다.
즉, 해결을 위해서는 서브 디렉토리 라우팅 요청에 대해 server side routing이 이루어지지 않도록 해야 하며, CRA Docs에서 제시하는 두 가지 솔루션이 있다.
첫 번째는 history API 대신 hash routing을 사용하는 HashRouter이다.
Hash Router는 루트 디렉토리와 서브 디렉토리를 hash로 구분해주는 데(http://user.github.io/reps/#/todos/42?_k=yknaj) hash 이후의 URL 변경에는 리랜더링이 이루어지지 않아 서버에 요청이 들어가지 않고, 404 에러도 반환되지 않게 된다.
다만 Hash Router는 Client에서만 페이지 정보를 알고 있고, Server에는 그 정보가 없기 때문에 검색 엔진에 잡히지 않아 SEO에 불리하다는 단점이 있고, URL이 Browser Router 사용에 비해 길고 복잡하다.
Browser Router를 사용하되 404 에러 발생 시 redirect를 통해 올바른 디렉토리로 이동할 수 있도록 처리해주는 방법도 있다.
CRA Docs에서 이 방법을 위한 MIT 오픈 소스를 제공하고 있으며, 아래와 같이 적용할 수 있다.
사용 시 MIT license를 꼭 명시해야 한다.
1. 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
// https://github.com/rafrex/spa-github-pages
// Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
// ---------------------------------------------- ------------------------
// 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. http://www.foo.tld/one/two?a=b& c=d#qwe, becomes
// http://www.foo.tld/?p=/one/two& q=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 segmentCount 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/? p=/one/two&q=a=b~and~c=d#qwe
// Otherwise, leave segmentCount as 0.
var segmentCount = 1;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + segmentCount).join('/') + '/?p=/' +
l.pathname.slice(1).split('/').slice (segmentCount).join('/').replace(/&/g, '~and~') +
(l.search ? '&q=' + l.search.slice(1) .replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>
2. index.html에 script 추가
404.html을 통해 쿼리로 변경된 URL을 index.html에서 받고, index.html에서 해당 쿼리를 해석하여 올바른 URL로 변경할 수 있도록 script를 추가해야 한다.
<head>
//...
<!-- Start Single Page Apps for GitHub Pages -->
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// https://github.com/rafrex/spa-github-pages
// Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License
// ----------------------------------------------------------------------
// This script checks to see if a redirect is present in the query string
// and 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) {
var q = {};
l.search.slice(1).split('&').forEach(function (v) {
var a = v.split('=');
q[a[0]] = a.slice(1).join('=').replace(/~and~/g, '&');
});
if (q.p !== undefined) {
window.history.replaceState(null, null,
l.pathname.slice(0, -1) + (q.p || '') +
(q.q ? ('?' + q.q) : '') +
l.hash
);
}
}
}(window.location))
</script>
<!-- End Single Page Apps for GitHub Pages -->
</head>
3. BrowserRouter 속성에 basename 설정
pakage.json에 homepage 속성을 추가한다.
{
"homepage": "https://user.github.io/resp"
...
}
4. Router 생성 시 basename 속성을 추가
const basename = process.env.PUBLIC_URL;
const root = ReactDOM.createRoot(document.getElementById('root'));
const routes = [
{
path: '/',
element: <App />,
},
{
path: '/todo',
element: <Todo />,
},
];
const router = createBrowserRouter(routes, {basename: basename});
root.render(
// <React.StrictMode>
<RouterProvider router={router} />
// </React.StrictMode>
);
CRA Docs: notes-on-client-side-routing
gh-pages에서 CRA로 빌드한 React App(SPA) 호스팅 하기: BrowserRouter 404 에러
브라우저의 이해 #2 히스토리 그리고 history API
github pages에 CRA+react-router-dom 배포시 404에러 발생 에러 잡기
덕분에 해결했습니다! 감사합니다