바닐라 JS로 SPA 만들기 - 컴포넌트

October 26, 2023

JavaScript바닐라JS

자바스크립트 라이브러리인 React를 사용하면서 자바스크립트로 어떻게 SPA(Single-Page Application)을 구현했을까 궁금했었습니다. 그리고 항상 마음 속에는 바닐라 자바스크립트를 잘하는 개발자가 되야지! 라는 생각은 있었지만, 항상 라이브러리를 이용해서 무언가를 구현하기에만 급급했던 거 같습니다.

이번 기회에 영화 검색 사이트를 바닐라 자바스크립트로 구현하면서 바닐라 자바스크립트를 공부해보려고 합니다.

SPA

SPA는 Single-Page Application의 약자로 하나의 HTML 페이지에서 애플리케이션 실행에 필요한 JavaScript나 CSS를 로드하는 애플리케이션입니다. 하나의 HTML 파일에서 동작하기 때문에 새로운 HTML 파일을 불러오기 위해 재로딩되는 시간이 없습니다. 또한 상태가 변하는 부분만 리렌더링하기 때문에 앱을 사용하는 것과 같은 우수한 사용자 경험을 제공합니다.

SPA는 클라이언트 측에서 상태 관리하고, 컴포넌트 기반으로 아키텍쳐를 구성하는 특징이 있고 오늘은 그 부분에 대해서 구현해 볼 예정입니다.

폴더 구조

📦src
  📂components
   📜Counter.js
   📜Header.js
  📂core
   📜Component.js
  📜App.js
  📜main.js
📜index.html

index.html

<!DOCTYPE html>
<html lang="ko">
 
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script type="module" src="./src/main.js" defer></script>
</head>
 
<body>
  <div id="root"></div>
</body>
 
</html>

src/main.js

import App from './App';
 
const root = document.querySelector('#root');
root.append(
  new App().el
);

src/core/Component.js

export default class Component {
  constructor({ tagName = 'div', state = {}, props = {} } = {}) {
    this.el = document.createElement(tagName);
    this.state = state;
    this.props = props;
    this.render();
  }
 
  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.render();
  }
 
  render() {
 
  }
}

src/Components/Header.js

import Component from '../core/Component';
 
export default class Header extends Component {
  constructor() {
    super({
      tagName: 'header',
    })
  }
 
  render() {
    this.el.innerHTML = `
      <h1>Counter</h1>
    `;
  }
}

src/Components/Counter.js

import Component from '../core/Component';
 
export default class Counter extends Component {
  constructor() {
    super({
      state: {
        count: 0,
      }
    })
  }
 
  render() {
    this.el.innerHTML = `
      <span>${this.state.count}</span>
      <button class="btn-plus">+1</button>
      <button class="btn-minus">-1</button>
    `;
 
    const $buttonPlus = this.el.querySelector('.btn-plus');
    const $buttonMinus = this.el.querySelector('.btn-minus');
 
    $buttonPlus.addEventListener('click', () => {
      this.setState({ count: this.state.count + 1 });
    })
 
    $buttonMinus.addEventListener('click', () => {
      this.setState({ count: this.state.count - 1 });
    })
  }
}

src/components/Button.js

import Component from '../core/Component';
 
export default class Button extends Component {
  constructor({ text, onClick }) {
    super({
      tagName: 'button',
      props: {
        text: text,
        onClick: onClick,
      }
    })
  }
  render() {
    this.el.textContent = this.props.text;
    this.el.addEventListener('click', this.props.onClick);
  }
}

src/Components/Counter.js 수정

import Component from '../core/Component';
import Button from './Button';
 
export default class Counter extends Component {
  constructor() {
    super({
      state: {
        count: 0,
      }
    })
  }
 
  render() {
    this.el.innerHTML = `
      <span>${this.state.count}</span>
    `;
 
    this.el.append(
      new Button({ text: '+1', onClick: () => this.setState({ count: this.state.count + 1 }) }).el,
      new Button({ text: '-1', onClick: () => this.setState({ count: this.state.count - 1 }) }).el,
      new Button({ text: 'make count zero', onClick: () => this.setState({ count: 0 }) }).el,
    );
  }
}

결과

📦src
  📂components
   📜Button.js
   📜Counter.js
   📜Header.js
  📂core
   📜Component.js
  📜App.js
  📜main.js
📜index.html

counter 완성

⬅ 이전 포스트
HTTP (2) | Network
다음 포스트 ➡️
바닐라 JS로 SPA 만들기 - 스토어