전통적인 페이지 이동 방식은 MPA(Multiple Page Application)
로, 페이지마다 html
파일을 생성하고, SSR(Server Side Rendering)
방식으로 로드되었다. 서버에서 모든 데이터가 로드된 후 클라이언트에 표시되니 초기 로딩이 빠르고, 페이지 정보를 담은 각각의 파일이 있어 SEO(Search Engine Optimization)
에 유리하다는 장점이 있다.
하지만 페이지가 이동할 때마다 깜빡이기 때문에 사용자 경험에 좋지 않고 서버 자원을 많이 쓴다는 단점이 있다.
SPA(Single Page Application)
는 CSR(Client Side Rendering)
방식으로 구동하며 html
을 한 번만 로드하고, 이후부터는 필요한 데이터만 화면에 보여준다. MPA
와 반대로 SEO
에 불리하며 초기 로딩이 느리지만, 사용자 경험에 유리하고 서버 자원을 적게 쓰며 생산성이 좋다.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>SPA-Router</title>
</head>
<body>
<nav>
<ul id="navigation">
<li><a href="/">Home</a></li>
<li><a href="/posts">Posts</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<div id="root">Loading...</div>
</body>
<script defer src="index.js"></script>
</html>
베이스가 되는 html
파일을 하나 만들고 js
를 연결한다.
// index.js
const root = document.getElementById("root");
const navigation = document.getElementById("navigation");
const routes = [
{ path: "/", component: Home },
{ path: "/posts", component: Posts },
{ path: "/about", component: About },
];
root
요소와 navigation
요소를 가져오고 경로를 설정한다. routes
의 path
는 각각의 component
함수를 호출할 것이다.
const render = (path) => {
try {
const component =
routes.find((route) => route.path === path)?.component || NotFound;
root.replaceChildren(component());
} catch (err) {
console.error(err);
}
};
find
함수는 배열 내에서 조건에 맞는 가장 첫 번째 요소를 반환한다. component
는 path
와 일치하는 routes
가 있으면 그것의 component
를, 없으면 NotFound
페이지를 반환한다.
replaceChildren
은 string
이나 Node
를 받아 기존 노드를 새로운 노드로 교체한다.
navigation.addEventListener("click", (e) => {
if (!e.target.matches("#navigation > li > a")) return;
e.preventDefault();
const path = e.target.getAttribute("href");
window.history.pushState({}, null, path);
render(path);
});
navigation
의 링크를 클릭하면 해당 주소를 얻으면서 history
에 이동 경로를 누적한다. pushState
가 그 역할을 하는데, 세션 기록을 스택에 추가하여 이전 페이지, 다음 페이지 기록을 돕는다. url
에 현재 컴포넌트 경로를 추가한다.
window.addEventListener("popstate", () => {
render(window.location.pathname);
});
popstate
는 세션 기록이 바뀔 때 반응하는 이벤트이다. navigation
링크를 클릭해 path
가 변경되면 해당 경로에 맞는 컴포넌트로 렌더링한다.
const createElement = (string) => {
const $temp = document.createElement("template");
$temp.innerHTML = string;
return $temp.content;
};
template
은 javascript
를 통해 그려질 html
구조를 담는 일종의 그릇이다. 해당 함수를 통과한 string
형태의 html
요소는 우리가 기대한 모양으로 렌더링된다.
const Home = () => {
return createElement("<h1>hi</h1><p>SPA HOME</p>");
};
const Posts = () => {
return createElement("<h1>bye</h1><p>SPA POSTS</p>");
};
const About = () => {
return createElement("<h1>hello</h1><p>SPA ABOUT</p>");
};
const NotFound = () => createElement("<h1>404 NotFound</p>");
각각 렌더링 될 형태의 html
요소를 string
으로 적어 createElement
함수에 넣는다.
render(window.location.pathname);
마지막으로 최초 render
함수를 실행한다.
const createElement = (string) => {
const $temp = document.createElement("template");
$temp.innerHTML = string;
return $temp.content;
};
const Home = () => {
return createElement(`<h1>hi</h1><p>SPA HOME</p>`);
};
const Posts = () => {
return createElement(`<h1>bye</h1><p>SPA POSTS</p>`);
};
const About = () => {
return createElement(`<h1>hello</h1><p>SPA ABOUT</p>`);
};
const NotFound = () => createElement("<h1>404 NotFound</p>");
const root = document.getElementById("root");
const navigation = document.getElementById("navigation");
const routes = [
{ path: "/", component: Home },
{ path: "/posts", component: Posts },
{ path: "/about", component: About },
];
const render = (path) => {
try {
const component =
routes.find((route) => route.path === path)?.component || NotFound;
root.replaceChildren(component());
} catch (err) {
console.error(err);
}
};
navigation.addEventListener("click", (e) => {
if (!e.target.matches("#navigation > li > a")) return;
e.preventDefault();
const path = e.target.getAttribute("href");
window.history.pushState({}, null, path);
render(path);
});
window.addEventListener("popstate", () => {
render(window.location.pathname);
});
render(window.location.pathname);
새 창으로 확인하면 url
은 바뀌되 깜빡임 없이 내용만 변하는 것을 확인할 수 있다.
참고
poiemaweb - 5.37 SPA & Routing
hanamon - SPA vs MPA와 SSR vs CSR 장단점 뜻정리
miracleground - SSR(서버사이드 렌더링)과 CSR(클라이언트 사이드 렌더링)
MDN Docs - SEO
MDN Docs - replaceChildren
MDN Docs - pushState
MDN Docs - popstate
MDN Docs - template