[강의리뷰]프레임워크 없이 SPA 만들기

김재만·2022년 5월 31일
1

프로젝트를 처음 시작하는 과정에서 SPA, MPA 중에 어떤 것이 좋을까 하는 고민을 했다. SPA가 가진 장점이 충분하다고 생각할 때, 아무 생각없이 SPA를 리액트로 구현할 수도 있지만 몇몇 기업은 프레임워크 없이 작업하는 것을 선호한다고 들었다. 물론 리액트는 라이브러리로 분류되긴 하지만, 내가 따라할 수 있는 바닥의 수준에서 SPA를 따라 만들어 보았다.

SPA

Single Page Application은 하나의 페이지를 띄워놓고, URL을 변경하는 접근을 감지하여 강제로 주소와 DOM을 조작하는 방식의 웹 어플리케이션이다. 때문에 URL 값을 받아서, 처리할 수 있는 라우터, 브라우저 대신 각 페이지를 이동한 경과를 저장하는 History와 페이지의 역할을 대신할 컴포넌트를 작성해야한다.

라우터와 History의 구현

index.html과 index.js의 코드는 아래와 같다.

//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Single Page App</title>
    <link rel="stylesheet" href="./static/css/index.css">
</head>
<body>
    <nav class="nav">
        <a href="/" class="nav__link" data-link>Dashboard</a>
        <a href="/posts" class="nav__link" data-link>Posts</a>
        <a href="/settings" class="nav__link" data-link>Settings</a>
    </nav>
    <div id="app"></div>
    <script type="module" src="/static/js/index.js"></script>
</body>
</html>
//index.js
import Dashboard from '../views/Dashboard.js';
import Posts from '../views/Posts.js';
import Settings from '../views/Settings.js';

const navigateTo = (url) => {
  history.pushState(null, null, url);
  router();
};

const router = async () => {
  const routes = [
    { path: '/', view: Dashboard },
    { path: '/posts', view: Posts },
    { path: '/settings', view: Settings },
  ];

  const potentialMatches = routes.map((route) => {
    return {
      route,
      isMatch: location.pathname === route.path,
    };
  });

  let match = potentialMatches.find((potentialMatch) => potentialMatch.isMatch);

  if (!match) {
    match = {
      route: routes[0],
      isMatch: true,
    };
  }

  const view = new match.route.view();

  document.querySelector('#app').innerHTML = await view.getHtml();
};

window.addEventListener('popstate', router);

document.addEventListener('DOMContentLoaded', () => {
  console.log('hi');
  document.body.addEventListener('click', (e) => {
    if (e.target.matches('[data-link]')) {
      e.preventDefault();
      navigateTo(e.target.href);
    }
  });

  router();
});

MPA와 SPA의 가장 큰 차이 중 하나는 화면을 다시 그릴 때, 새로고침이 일어나는지 여부이다. 이를 막기 위해서 body의 이벤트 리스너가 a태그 클릭 이벤트의 기본 동작을 방해(e.preventDefault)하도록 하였으며, 대신 naviateTo함수가 실행된다. 이 함수는 클릭된 a태그의 href값을 인자로 받아 History API의 pushState의 세 번째 인자로 전달한다.

History API는 페이지의 상태를 세션 기록 스택이라는 저장공간에 순서대로 기억하고, 필요에 따라 나중에 기록된 상태부터 호출하는 역할을 한다. 덕분에 페이지 이동이 아님에도 각 컴포넌트를 이동한 기억에 따라, 뒤로가기-앞으로 가기 버튼이 작동한다. (물론, History API가 필수적이진 않을 것이다.)

navigateTo 함수는 뒤이어 router함수를 호출한다. 라우터는 routes에 각 컴포넌트에 해당하는 path(url주소)와 view(컴포넌트)를 저장하고, 현재 클릭 동작이 어느 path값을 가리키는지 확인하는 함수이다. 해당 값이 존재하면, document.querySelector.innerHTML을 통해 화면에 렌더를 지시한다.

컴포넌트의 구현

컴포넌트는 기본 View 클래스를 구현하고 상속하는 방식으로 구현되었다.

//AbstractView.js
export default class {
  constructor() {}
  setTitle(title) {
    document.title = title;
  }

  async getHtml() {
    return '';
  }
}
//Dashboard.js
import AbstractView from './AbstractView.js';

export default class extends AbstractView {
  constructor() {
    super();
    this.setTitle('Dashboard');
  }

  async getHtml() {
    return `
        <h1>Welcome back, Dom</h1>
        <p>
            Fugiat voluptate et nisi Lorem
        </p>
        <p>
            <a href="/posts" data-link> View recent posts</a>
        </p>
      `;
  }
}
//Posts.js
import AbstractView from './AbstractView.js';

export default class extends AbstractView {
  constructor() {
    super();
    this.setTitle('Posts');
  }

  async getHtml() {
    return `
        <h1>Recent Posts</h1>
        <p>
            아무말아무말아무말아무말아무말아무말아무말아무말아무말아무말아무말아무말아무말
        </p>
        <p>
           궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁궁시렁
        </p>
      `;
  }
}
//Settings.js
import AbstractView from './AbstractView.js';

export default class extends AbstractView {
  constructor() {
    super();
    this.setTitle('Dashboard');
  }

  async getHtml() {
    return `
        <h1>Settings</h1>
        <p>
            Fugiat voluptate et nisi Lorem
        </p>
        <p>
            <a href="/posts" data-link> View recent posts</a>
        </p>
      `;
  }
}

클래스인 AbstractView를 통해 setTilte함수와 getHtml을 내장함수로 하여, 각 컴포넌트에 상속하는 방식이다. 모양만 보면 클래스형 컴포넌트로 구현된 React와 상당히 유사하다.

마무리

생각보다 방법이 한정되지 않아 선택의 어려움이 있었다. 아무 고민 없이 리액트를 사용하는 것은 곤란하지만, 빠른 개발속도와 SPA에 맞는 패키지를 고려하면 React는 꽤나 훌륭한 선택지로 보인다. 차후에 렌더링 속도를 측정해보아야 겠다.

  • 노드로 서버 돌려봄..! 신기 ㅎ

참고자료

Build a Single Page Application with JavaScript (No Frameworks)

profile
듣는 것을 좋아하는 개발자입니다

0개의 댓글