[Javascript] Vanilla JS SPA

찐새·2022년 9월 5일
0

Javascript

목록 보기
2/11
post-thumbnail

Vanilla JS SPA

MPA(Multi Page Application)

전통적인 페이지 이동 방식은 MPA(Multiple Page Application)로, 페이지마다 html 파일을 생성하고, SSR(Server Side Rendering) 방식으로 로드되었다. 서버에서 모든 데이터가 로드된 후 클라이언트에 표시되니 초기 로딩이 빠르고, 페이지 정보를 담은 각각의 파일이 있어 SEO(Search Engine Optimization)에 유리하다는 장점이 있다.

하지만 페이지가 이동할 때마다 깜빡이기 때문에 사용자 경험에 좋지 않고 서버 자원을 많이 쓴다는 단점이 있다.

SPA(Single Page Application)

SPA(Single Page Application)CSR(Client Side Rendering) 방식으로 구동하며 html을 한 번만 로드하고, 이후부터는 필요한 데이터만 화면에 보여준다. MPA와 반대로 SEO에 불리하며 초기 로딩이 느리지만, 사용자 경험에 유리하고 서버 자원을 적게 쓰며 생산성이 좋다.

Vanilla JS로 SPA 구현

base html

<!-- 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를 연결한다.

routes

// 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 요소를 가져오고 경로를 설정한다. routespath는 각각의 component 함수를 호출할 것이다.

render

const render = (path) => {
  try {
    const component =
      routes.find((route) => route.path === path)?.component || NotFound;
    root.replaceChildren(component());
  } catch (err) {
    console.error(err);
  }
};

find 함수는 배열 내에서 조건에 맞는 가장 첫 번째 요소를 반환한다. componentpath와 일치하는 routes가 있으면 그것의 component를, 없으면 NotFound 페이지를 반환한다.

replaceChildrenstring이나 Node를 받아 기존 노드를 새로운 노드로 교체한다.

pushState

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에 현재 컴포넌트 경로를 추가한다.

popstate

window.addEventListener("popstate", () => {
  render(window.location.pathname);
});

popstate는 세션 기록이 바뀔 때 반응하는 이벤트이다. navigation 링크를 클릭해 path가 변경되면 해당 경로에 맞는 컴포넌트로 렌더링한다.

components

const createElement = (string) => {
  const $temp = document.createElement("template");
  $temp.innerHTML = string;
  return $temp.content;
};

templatejavascript를 통해 그려질 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);

test - code sandbox

새 창으로 확인하면 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

profile
프론트엔드 개발자가 되고 싶다

0개의 댓글