[Vanilla JS 문서편집기 설명 및 개선] 4. 컴포넌트 템플릿 만들기

컴포넌트 템플릿

이전에 리액트나 뷰를 이용할 때는 정해져 있는 형식이 있었기 때문에 편리했습니다. 반면에, Vanilla JS는 제가 자유롭게 작성할 수 있는 구조입니다. 기존 코드는 규칙도 이전에 작성한 코드를 보며 ctrl+c, v 방식으로 비슷하게 만들었습니다. 

하지만 이는 비효율적인 방법으로 컴포넌트가 늘어날 수록 부담되는 방법입니다. 또, 가독성이 좋지 않기 때문에 다른 사람이 봤을 때 어떤 기능을 하는지, 어떤 방식으로 작성했는지 알 수 없는 문제가 있습니다. 

 

그렇기 때문에 편리하게 형식을 항상 동일하게 가져갈 수 있도록 작성해야 할 필요성을 느껴 컴포넌트 템플릿을 만들어 사용했습니다. 

export default class Component {
  $target;
  props;
  state;
  constructor({ $target, props }) {
    this.$target = $target;
    this.props = props;
    this.setup();
    this.render();
    this.setEvent();
  }

  setup() {} //초기화 부분
  mounted() {}
  template() { // 만들어지기 원하는 구조를 작성
    return "";
  }
  render() {
    this.$target.innerHTML = this.template();
    this.mounted();
  }
  setEvent() {}
  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.render();
  }
  addEvent(eventType, selector, callback) {
    const children = [...this.$target.querySelectorAll(selector)];
    this.$target.addEventListener(eventType, (e) => {
      if (!e.target.closest(selector)) return false;
      callback(e);
    });
  }
}

 

addEvent는 setEvent안에서 사용하면 되는데, 

기존에 이벤트를 추가해주는 방식과 비슷하게 작성해 주면 됩니다.

Before

this.$sideBarPages.addEventListener("scroll", (e) => {
      const scrollPositon = this.$sideBarPages.scrollTop;
      const eventArea = document.querySelector(".sidebar__pages");
      if (scrollPositon > 0) {
        if (!eventArea.classList.contains("scrolled")) {
          eventArea.classList.add("scrolled");
        }
      } else {
        eventArea.classList.remove("scrolled");
      }
    });
}

After

setEvent() {
    this.addEvent("scroll", ".sidebar__pages", (e) => {
      const scrollPositon = this.$target.scrollTop;
      const eventArea = document.querySelector(".sidebar__pages");
      if (scrollPositon > 0) {
        if (!eventArea.classList.contains("scrolled")) {
          eventArea.classList.add("scrolled");
        }
      } else {
        eventArea.classList.remove("scrolled");
      }
    });
}

적용 사례

Sidebar.js

Before

import { push } from "./router.js";
import Data from "./data.js";
import { SideBarHeader, SideBarPages } from "@components";
/**
 * SideBar를 만들어주는 컴포넌트
 */
export default class SideBar {
  constructor({ $target, initialState, editorsetState }) {
    this.$target = $target;
    this.$page = document.createElement("aside");
    this.$page.className = "sidebar__aside--flex";
    this.sideBarHeader = new SideBarHeader({ $target: this.$page });
    new SideBarPages({ $target: this.$page, initialState, editorsetState });
    this.$target.appendChild(this.$page);
  }
}

After

import { Component } from "@core";
import { push } from "./router.js";
import { SideBarHeader, SideBarPages } from "@components";

export default class SideBar extends Component {
  setup() {}
  template() {
    return `
      <aside class="sidebar__aside--flex">
        <section class="sidebar__header"></section>
        <section class="sidebar__pages"></section>
      </aside>
    `;
  }

  mounted() {
    const $sidebarHeader = this.$target.querySelector(".sidebar__header");
    const $sidebarPages = this.$target.querySelector(".sidebar__pages");
    new SideBarHeader({ $target: $sidebarHeader });
    new SideBarPages({ $target: $sidebarPages, props: this.props });
  }
}

SideBarHeader.js

Before

import { push } from "@/router";
import Data from "@/data";
import { setItem, getItem } from "@stores";

export default class SideBarHeader {
  constructor({ $target }) {
    this.$target = $target;
    this.$sideBarHeader = document.createElement("section");
    this.$sideBarHeader.className = "sidebar__header";
    this.data = new Data();
    this.$beforeSelected = 0;

    this.initialize();
    this.eventAdd();
  }

  initialize = () => {
    this.$sideBarHeader.innerHTML = `
                <span class = 'sidebar__header--main sidebar__header--action' data-action = 'main'>H의 Notion</span>
                <div class = 'sidebar__header--container'>         
                  <div class = 'sidebar__header--action' data-action = 'quick_start'>  
                    <span class = "material-symbols-rounded">bolt</span>
                    <span>Quick start</span>
                  </div>
                  <div class = 'sidebar__header--action' data-action = 'guestbook'>  
                    <span class = "material-symbols-rounded">menu_book</span>
                    <span>Guestbook</span>
                  </div>
                  <div class = 'sidebar__header--action' data-action = 'add'>  
                    <span class = "material-symbols-rounded">edit_square</span>
                    <span>Add a page</span>
                  </div>
                </div>`;
    this.$target.appendChild(this.$sideBarHeader);
  };

  render = () => {
    const selected = getItem("selected");

    document
      .querySelector(
        `.sidebar__pages--detail[data-id="${this.$beforeSelected}"]`
      )
      ?.classList.remove("highlight");

    document
      .querySelector(`.sidebar__pages--detail[data-id="${selected}"]`)
      ?.classList.add("highlight");
  };

  eventAdd = () => {
    this.$sideBarHeader.addEventListener("click", this.clickEventAdd);
  };

  clickEventAdd = async (e) => {
    if (e.target.closest(".sidebar__header--action")) {
      const action = e.target.closest(".sidebar__header--action").dataset
        .action;
      switch (action) {
        case "main":
          push(`/`);
          break;
        case "add":
          await this.data.addDocumentStructure().then((x) => {
            push(`/${x.id}`);
            this.$beforeSelected = getItem("selected");
            setItem("selected", x.id);
            this.render();
          });
          break;
        case "quick_start":
          push(`/quick_start`);
          break;
        case "guestbook":
          push(`/guestbook`);
          break;
      }
    }
  };
}

After

import { push } from "@/router";
import Data from "@/data";
import { setItem, getItem } from "@stores";
import { Component } from "@core";

export default class SideBarHeader extends Component {
  setup() {
    this.data = new Data();
    this.$beforeSelected = 0;
  }
  template() {
    return `
      <span class = 'sidebar__header--main sidebar__header--action' data-action = 'main'>H의 Notion</span>
      <div class = 'sidebar__header--container'>         
        <div class = 'sidebar__header--action' data-action = 'quick_start'>  
          <span class = "material-symbols-rounded">bolt</span>
          <span>Quick start</span>
        </div>
        <div class = 'sidebar__header--action' data-action = 'guestbook'>  
          <span class = "material-symbols-rounded">menu_book</span>
          <span>Guestbook</span>
        </div>
        <div class = 'sidebar__header--action' data-action = 'add'>  
          <span class = "material-symbols-rounded">edit_square</span>
          <span>Add a page</span>
        </div>
      </div>
    `;
  }
  setEvent() {
    this.addEvent("click", ".sidebar__header", async (e) => {
      if (e.target.closest(".sidebar__header--action")) {
        const action = e.target.closest(".sidebar__header--action").dataset
          .action;
        switch (action) {
          case "main":
            push(`/`);
            break;
          case "add":
            await this.data.addDocumentStructure().then((x) => {
              push(`/${x.id}`);
              this.$beforeSelected = getItem("selected");
              setItem("selected", x.id);
            });
            break;
          case "quick_start":
            push(`/quick_start`);
            break;
          case "guestbook":
            push(`/guestbook`);
            break;
        }
      }
    });
  }
}

 

두 사례 모두 가독성이 훨씬 향상된 것을 알 수 있습니다. 

이 정도면 이전과 비교했을 때 처음 보는 사람도 이해하기 편하지 않을까 생각합니다.