Routing
Routing in App
브라우저가 화면을 전환하는 경우
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>SPA-Router - Link</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/service.html">Service</a></li>
<li><a href="/about.html">About</a></li>
</ul>
</nav>
<section>
<h1>Home</h1>
<p>This is main page</p>
</section>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>SPA-Router - ajax</title>
<link rel="stylesheet" href="css/style.css" />
<script type="module" src="js/index.js"></script>
</head>
<body>
<nav>
<ul id="navigation">
<li><a href="/">Home</a></li>
<li><a href="/service">Service</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<div id="root">Loading...</div>
</body>
</html>
// index.js
import { Home, Service, About, NotFound } from './components.js';
const $root = document.getElementById('root');
const $navigation = document.getElementById('navigation');
const routes = [
{ path: '/', component: Home },
{ path: '/service', component: Service },
{ path: '/about', component: About },
];
const render = async path => {
try {
const component = routes.find(route => route.path === path)?.component || NotFound;
$root.replaceChildren(await component());
} catch (err) {
console.error(err);
}
};
$navigation.onclick = e => {
if (!e.target.matches('#navigation > li > a')) return;
e.preventDefault();
const path = e.target.getAttribute('href');
render(path);
};
window.addEventListener('DOMContentLoaded', () => render('/'));
// components.js
const createElement = domString => {
const $temp = document.createElement('template');
$temp.innerHTML = domString;
return $temp.content;
};
const fetchData = async url => {
const res = await fetch(url);
const json = await res.json();
return json;
};
export const Home = async () => {
const { title, content } = await fetchData('/api/home');
return createElement(`<h1>${title}</h1><p>${content}</p>`);
};
export const Service = async () => {
const { title, content } = await fetchData('/api/service');
return createElement(`<h1>${title}</h1><p>${content}</p>`);
};
export const About = async () => {
const { title, content } = await fetchData('/api/about');
return createElement(`<h1>${title}</h1><p>${content}</p>`);
};
export const NotFound = () => createElement('<h1>404 NotFound</p>');
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>SPA-Router - Hash</title>
<link rel="stylesheet" href="css/style.css" />
<script type="module" src="js/index.js"></script>
</head>
<body>
<nav>
<ul>
<li><a href="#">Home</a></li>
<li><a href="#service">Service</a></li>
<li><a href="#about">About</a></li>
</ul>
</nav>
<div id="root">Loading...</div>
</body>
</html>
// index.js
// components.js는 위와 동일함
import { Home, Service, About, NotFound } from './components.js';
const $root = document.getElementById('root');
const routes = [
{ path: '', component: Home },
{ path: 'service', component: Service },
{ path: 'about', component: About },
];
const render = async () => {
try {
// url의 hash를 취득
const hash = window.location.hash.replace('#', '');
const component = routes.find(route => route.path === hash)?.component || NotFound;
$root.replaceChildren(await component());
} catch (err) {
console.error(err);
}
};
window.addEventListener('hashchange', render);
window.addEventListener('DOMContentLoaded', render);
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>SPA-Router - pjax</title>
<link rel="stylesheet" href="css/style.css" />
<script defer src="js/index.js"></script>
</head>
<body>
<nav>
<ul id="navigation">
<li><a href="/">Home</a></li>
<li><a href="/service">Service</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<div id="root">Loading...</div>
</body>
</html>
// index.js
// components.js는 위와 동일함
import { Home, Service, About, NotFound } from './components.js';
const $root = document.getElementById('root');
const $navigation = document.getElementById('navigation');
const routes = [
{ path: '/', component: Home },
{ path: '/service', component: Service },
{ path: '/about', component: About },
];
const render = async path => {
const _path = path ?? window.location.pathname;
try {
const component = routes.find(route => route.path === _path)?.component || NotFound;
$root.replaceChildren(await component());
} catch (err) {
console.error(err);
}
};
$navigation.addEventListener('click', e => {
if (!e.target.matches('#navigation > li > a')) return;
e.preventDefault();
// 이동할 페이지의 path
const path = e.target.getAttribute('href');
if (window.location.pathname === path) return;
window.history.pushState(null, null, path);
render(path);
});
window.addEventListener('popstate', () => {
console.log('[popstate]', window.location.pathname);
render();
});
/**
* 웹페이지가 처음 로딩되면 window.location.pathname를 확인해 페이지를 이동시킨다.
* 새로고침을 클릭하면 현 페이지(예를 들어 localhost:5004/service)가 서버에 요청된다.
* 이에 응답하는 처리는 서버에서 구현해야 한다.
*/
window.addEventListener('DOMContentLoaded', () => {
render();
});
// server
const express = require('express');
const path = require('path');
const app = express();
const port = 5004;
app.use(express.static(path.join(__dirname, 'public')));
app.get('/api/:page', (req, res) => {
const { page } = req.params;
res.sendFile(path.join(__dirname, `/data/${page}.json`));
});
// 브라우저 새로고침을 위한 처리 (다른 route가 존재하는 경우 맨 아래에 위치해야 한다)
// 브라우저 새로고침 시 서버는 index.html을 전달하고 클라이언트는 window.location.pathname를 참조해 다시 라우팅한다.
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public/index.html'));
});
app.listen(port, () => {
console.log(`Server listening on http:/localhost:${port}`);
});
구분 | History 관리 | SEO 대응 | 사용자 경험 | 서버 레더링 | 구현 난이도 | IE 대응 |
---|---|---|---|---|---|---|
전통적 링크 방식 | O | O | X | O | 간단 | |
ajax 방식 | X | X | O | X | 보통 | 7 이상 |
hash 방식 | O | X | O | X | 보통 | 8 이상 |
pjax 방식 | O | O | O | △ | 복잡 | 10 이상 |