Vanila JavaScript로 웹 컴포넌트 만들기

LOOPY·2022년 5월 10일
0

0. 서론

나는 학부 생활 중 19~20년도에는 교수님의 강의 목표에 따라 C,C++ 그리고 Python을 주로 이용하여 강의를 듣거나 과제를 수행했고, 팀 프로젝트에 프론트엔드 개발자로 참여하기로 결정한 (웹 개발을 공부하기로 결심한) 20년 하반기 즈음 부터는 JavaScript, CSS, HTML을 열심히 파고 들었으나, 최근 코딩테스트를 준비하고 단순히 개발을 위한 코딩이 아닌 기술적 개념 등에 관해 공부를 해보면서 여러 번 내 머리를 후려 친 생각이 있다.

나는 JavaScript를 잘 다루는가?

'잘 다룬다'라는 기준 자체가 굉장히 모호할 수 있다. 물론 공부를 시작할 때에는 생활코딩, 패스트캠퍼스 또는 유튜브 같은 곳에서 JavaScript만 가지고 이것저것 열심히 해보았던 기억이 있으나, 첫 프로젝트부터 React로 시작했던 터라 우테캠의 2차 과제 테스트(Vanila JavaScript 개발)와 관련된 후기들을 찾아보며 너무나 막막하다는 생각이 들었다.
웹 개발의 기초는 JavaScript이라는 것을 분명히 알고 있는데, 나는 React라는 프레임워크에 심히 의지하는? 또는 종속되어 있는? 느낌이 들었고, 이는 최종적으로 내가 되고자 하는 개발자의 모습에 걸맞지 않다는 것을 깨달았다.

그래서 이번 주에 해보고자 하는 것은, Vanila JavaScript로 웹 컴포넌트 만들기! 평생 React만 사용할 것이 아니고, 더불어 Web의 동작과 JavaScript의 기본을 이해하기에 알맞다고 생각했기 때문이다.

온전히 혼자 코딩하기에는 어디부터 어떻게 시작해야하나 싶었는데.. https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Component/ 을 기반으로 진행하였습니다🙏🏻 항상 진심으로 감사드립니다!

1. 컴포넌트와 상태관리

이전에는 흔히 jQuery 등을 사용해 DOM을 직접적으로 조작하였는데, 브라우저와 JavaScript가 발전하는 과정에서 아예 브라우저(클라이언트)단에서 렌더링을 하고, 서버에서는 REST API같이 필요한 데이터만 제공하는 형태로 기술이 변화했다. 이제는 직접적으로 DOM을 다루는 행위가 급격히 감소하고 상태(state)를 기준으로 DOM을 렌더링하는 형태로 발전한 것이다. 즉, DOM이 변하는 경우가 State에 종속되었다. 그리고 이러한 과정 속에서 'Client-Side Rendering'이라는 개념과 '상태관리'라는 개념이 생기게 되었다.

SSR(Server-Side Rendering)

약 5년 전까지 JSP, PHP, ASP 등이 웹 개발 3대장이라고 불렸다. 이들은 서버에서 HTML을 만들어 클라이언트에 넘겨주는, 즉 Server Side Rendering의 역할을 수행했고, 따라서 클라이언트단에서는 굳이 데이터를 정교하게 관리할 필요가 없었다.

CSR(Client-Side Rendering)

JS가 발전하며 아예 클라이언트단에서 모든 렌더링을 처리하려는 시도가 계속되었고, 그렇게 React, Angular, Vue 같은 프레임워크가 탄생하였다. 이를 위해서 렌더링에 필요한 상태(state)를 정교하게 관리해야 했고 Redux같은 상태관리 라이브러리가 생겨났다.

(이 중 Angular이 CSR의 시작, React는 컴포넌트 개발의 시작, Vue는 이 둘의 장점을 모두 수용한 것으로 알려져있다)

2. state - setState - render

(1)기능 구현

<div id="app"></div>
<script>
const $app = document.querySelector('#app');

let state = {
  items: ['item1', 'item2', 'item3', 'item4']
}

const render = () => {
  const { items } = state;
  $app.innerHTML = `
    <ul>
      ${items.map(item => `<li>${item}</li>`).join('')}
    </ul>
    <button id="append">추가</button>
  `;
  document.querySelector('#append').addEventListener('click', () => {
    setState({ items: [ ...items, `item${items.length + 1}` ] })
  })
}

const setState = (newState) => {
  state = { ...state, ...newState };
  render();
}

render();
</script>

핵심은 state가 변경되면 render를 실행하고, state는 setState로만 변경한다는 것이다. 그러면 브라우저에 출력되는 내용은 무조건 state에 종속되고 DOM을 직접적으로 다룰 필요가 없어진다.

(2) 추상화

<div id="app"></div>
<script>
class Component {
  $target;
  $state;
  constructor ($target) { 
    this.$target = $target;
    this.setup();
    this.render();
  }
  setup () {};
  template () { return ''; }
  render () {
    this.$target.innerHTML = this.template();
    this.setEvent();
  }
  setEvent () {}
  setState (newState) {
    this.$state = { ...this.$state, ...newState };
    this.render();
  }
}

class App extends Component {
  setup () {
    this.$state = { items: ['item1', 'item2'] };
  }
  template () {
    const { items } = this.$state;
    return `
        <ul>
          ${items.map(item => `<li>${item}</li>`).join('')}
        </ul>
        <button>추가</button>
    `
  }
  
  setEvent () {
    this.$target.querySelector('button').addEventListener('click', () => {
      const { items } = this.$state;
      this.setState({ items: [ ...items, `item${items.length + 1}` ] });
    }); 
  }
}

new App(document.querySelector('#app'));
</script>

컴포넌트 클래스를 통해 코드의 재사용성을 높일 수 있다.

(3) 모듈화

  • index.html
<!doctype html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>Simple Component 2</title>
</head>
<body>
<div id="app"></div>
<script src="./src/app.js" type="module"></script>
</body>
</html>
  • src/app.js
import Items from "./components/Items.js";

class App {
  constructor() {
    const $app = document.querySelector('#app');
    new Items($app);
  }
}

new App();
  • src/components/Items.js
import Component from "../core/Component.js";

export default class Items extends Component {
  setup () {
    this.$state = { items: ['item1', 'item2'] };
  }
  template () {
    const { items } = this.$state;
    return `
      <ul>
        ${items.map(item => `<li>${item}</li>`).join('')}
      </ul>
      <button>추가</button>
    `
  }

  setEvent () {
    this.$target.querySelector('button').addEventListener('click', () => {
      const { items } = this.$state;
      this.setState({ items: [ ...items, `item${items.length + 1}` ] });
    });
  }
}
  • src/core/Component.js
export default class Component {
  $target;
  $state;
  constructor ($target) {
    this.$target = $target;
    this.setup();
    this.render();
  }
  setup () {};
  template () { return ''; }
  render () {
    this.$target.innerHTML = this.template();
    this.setEvent();
  }
  setEvent () {}
  setState (newState) {
    this.$state = { ...this.$state, ...newState };
    this.render();
  }
}
profile
1.5년차 프론트엔드 개발자의 소소한 기록을 담습니다 :-)

0개의 댓글