JavaScript / SPA / Route

토끼리·2023년 5월 19일
0

JavaScript

목록 보기
2/2
post-thumbnail

1. SPA

단일 페이지 애플리케이션(Single Page Application, SPA)

기본적으로 단일 페이지로 구성되며, 웹 애플리케이션에 필요한 모든 정적 리소스를 최초 접근 시 단 한번만 다운로드한다.
즉, 서버에서 주는 html이 1개 뿐인 어플리케이션이다.

SPA가 등장하기 전 웹 애플리케이션을 구성하던 방식인 서버 사이드 렌더링(SSR)의 경우, 화면에 보여질 리소스를 서버로 요청하고, 서버로부터 받아온 리소스를 렌더링 했다.
하지만 SPA의 경우 렌더링의 역할을 서버에게 넘기지 않고, 브라우저에서 처리하는 방식이다.

새로운 페이지 요청 시, 페이지 갱신에 필요한 데이터만을 JSON으로 전달받아 페이지를 갱신하므로 전체적인 트래픽을 감소시킬 수 있고, 전체 페이지를 다시 렌더링하지 않고 변경되는 부분만을 갱신하므로 새로고침이 발생하지 않아 네이티브 앱과 유사한 사용자 경험을 제공할 수 있다.

2. Route

라우트와 라우터, 라우팅의 차이점

라우트(Route)경로이고, 서로 다른 네트워크 간 데이터를 전송하고 전송한 데이터를 받는 경로를 말한다.

라우터(Router) : 인터넷 공유기를 떠올리자, 라우터는 한개의 인터넷 회신을 여러개의 네트워트가 사용할 수 있도록 쪼개주는, 중계 역할을 해주는 장치다.

라우팅(Routing)은 엔드 포인트(URI, 경로)의 정의, 해당 엔드포인트에 대한 클라이언트 요청에 애플리케이션이 응답하는 방식이다.

라우팅이란?

출발지에서 목적지까지의 경로를 결정하는 기능이다. 애플리케이션의 라우팅은 사용자가 태스크를 수행하기 위해 어떤 화면(view)에서 다른 화면으로 화면을 전환하는 내비게이션을 관리하기 위한 기능을 의미한다. 일반적으로 라우팅은 사용자가 요청한 URL 또는 이벤트를 해석하고 새로운 페이지로 전환하기 위해 필요한 데이터를 서버에 요청하고 페이지를 전환하는 위한 일련의 행위를 말한다.

브라우저가 화면을 전환하는 경우는 다음과 같다.

  • 브라우저의 주소창에 URL을 입력하면, 해당 페이지로 이동한다.

  • 웹페이지의 링크(a 태그)를 클릭하면, 해당 페이지로 이동한다.

  • 브라우저의 뒤로가기 또는 앞으로가기 버튼을 클릭하면, 사용자 방문 기록(history)의 뒤 또는 앞으로 이동한다.
    history 관리를 위해서는 각 페이지는 브라우저의 주소창에서 구별할 수 있는 유일한 URL을 소유해야 한다.

3. SPA의 Routing

전통적인 링크 방식에서 pjax방식까지 4가지 방식의 SPA의 라우팅(Routing)이 있다.
이 중에서 마지막 pjax 방식을 공부 해보려 한다.
나머지 방식이 궁금하다면 https://poiemaweb.com/js-spa에서 확인해보면 된다.

// 전통적 링크 방식
$ npm run link

// ajax 방식
$ npm run ajax

// hash 방식
$ npm run hash

// pjax(pushState + ajax) 방식
$ npm run pjax

pjax 방식

hash 방식의 가장 큰 단점은 SEO 이슈이다. 이를 보완한 방법이 HTML5의 History API인 pushState와 popstate 이벤트를 사용한 pjax(pushState + ajax) 방식이다. pushState와 popstate은 IE 10 이상에서 동작한다.

<!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>

위 예제를 살펴보면 link tag(<a href="/service">Service</a>)의 href 어트리뷰트에 path를 사용하고 있다. 따라서 내비게이션이 클릭되면 path가 추가된 URL이 서버로 요청된다.

하지만 pjax방식은 내비게이션 클릭 이벤트를 캐치하고, preventDefault 메서드를 사용해 서버로의 요청을 방지한다.
이후, href 어트리뷰트에 path을 사용하여 ajax 요청을 하는 방식이다.

이때 ajax 요청은 브라우저 주소창의 URL을 변경시키지 않아 history 관리가 불가능하다. 이때 사용하는 것이 pushState 메서드이다. pushState 메서드는 주소창의 URL을 변경하고 URL을 history entry로 추가하지만 서버로 HTTP 요청을 하지는 않는다.

JavaScript의 구현은 다음과 같다.

// 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 },
];

// TODO: path를 상태로 관리
const render = async path => {
  // $navigation의 a 요소를 클릭하면 path(a 요소의 href)가 전달된다.
  // 하지만 웹페이지가 처음 로딩되거나 앞으로/뒤로 가기 버튼을 클릭하면 path를 전달하지 않는다.
  // 이때 window.location.pathname를 키로 routes에서 컴포넌트를 결정해 뷰를 전환한다.
  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;

  /**
   * 네비게이션을 클릭하면 주소창의 url이 변경되므로 HTTP 요청이 서버로 전송된다.
   * preventDefault를 사용하여 이를 방지하고 history 관리를 위한 처리를 실행한다.
   */
  e.preventDefault();

  // 이동할 페이지의 path
  const path = e.target.getAttribute('href');

  // 현재 페이지와 이동할 페이지가 같으면 이동하지 않는다.
  if (window.location.pathname === path) return;

  // pushState는 주소창의 url을 변경하지만 HTTP 요청을 서버로 전송하지는 않는다.
  window.history.pushState(null, null, path);
  render(path);
});

/**
 * pjax 방식은 hash를 사용하지 않으므로 hashchange 이벤트를 사용할 수 없다.
 * popstate 이벤트는 pushState에 의해 발생하지 않고 앞으로/뒤로 가기 버튼을 클릭하거나
 * history.forward/back/go(n)에 의해 history entry가 변경되면 발생한다.
 * 앞으로/뒤로 가기 버튼을 클릭하면 window.location.pathname를 참조해 뷰를 전환한다.
 */
window.addEventListener('popstate', () => {
  console.log('[popstate]', window.location.pathname);
  render();
});

/**
 * 웹페이지가 처음 로딩되면 window.location.pathname를 확인해 페이지를 이동시킨다.
 * 새로고침을 클릭하면 현 페이지(예를 들어 localhost:5004/service)가 서버에 요청된다.
 * 이에 응답하는 처리는 서버에서 구현해야 한다.
 */
window.addEventListener('DOMContentLoaded', () => {
  render();
});

pjax 방식에서 사용하는 history.pushState 메서드는 주소창의 url을 변경하지만 HTTP 요청을 서버로 전송하지는 않는다
따라서 페이지가 갱신되지 않는다.
하지만 페이지마다 고유의 URL이 존재하므로 history 관리에 아무런 문제가 없다.
또한 hash를 사용하지 않으므로 SEO에도 문제가 없다.

단, 브라우저의 새로고침 버튼을 클릭하면 브라우저 주소창의 url이 변경되지 않는 ajax 방식과 해시(fragment identifier)만 추가되는 hash 방식은 서버에 별도의 요청을 보내지 않지만,
pjax 방식은 브라우저 주소창의 url이 변경되기 때문에 요청(예를 들어 localhost:5004/service)이 서버로 전달된다.
즉, pjax 방식은 서버 렌더링 방식과 ajax 방식이 혼재되어 있는 방식으로 서버의 지원이 필요하다. 이에 대한 서버 구현은 다음과 같다.

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}`);
});

pjax 방식의 예제를 실행하려면 다음 명령을 실행한다.

$ npm run pjax
profile
안녕하세요, FE개발자 🐇🐘입니다.

0개의 댓글