TIL13. Vanilla JS로 컴포넌트 다루기(2)

imloopy·2022년 4월 2일
0

Today I Learned

목록 보기
13/56

Today I Learned

오늘도 어제에 이어서 바닐라 자바스크립트로(타입스크립트로) 컴포넌트를 만드는 연습을 했다. 그래도 어제에 비해서 조금 더 유연하게 설계한 것 같아서 기쁘다. 아직 클래스 상속에 대하여 개념을 잘 몰라서 체계적인 컴포넌트를 만드는 것이 어렵다. 일단 상속에 대한 개념을 조금 익혀둔 다음, 이를 이용하여 구현하는 연습을 꾸준히 해나가야겠다.

클래스의 상속

객체 지향 프로그래밍의 가장 큰 특징은 다형성, 캡슐화, 상속이다. 이 중 상속은 부모 클래스로부터 특징을 내려받아 자식클래스에서 사용할 수 있다는 특징을 만족하며, 메서드를 오버라이딩 할 수 있으며, 자식은 부모 클래스의 메서드 + 자식 클래스에서 고유 메서드를 구현하여 확장이 가능하다. 부모 클래스가 사용되던 곳에 자식 클래스로 대체하더라도, 완벽하게 동작할 수 있어야 한다.

class Parent {
  constructor(value) {
    this.value = value;
    this.setup();
  }
  
  setup() {
  }
}

이렇게 부모 클래스가 있을 때, 자식클래스는 extends 예약어를 통하여 부모 클래스를 상속한다.

class Child extends Parent {
  constructor(value) {
    super(value);
    this.childrenValue = value;
  }
}

Child 클래스는 Parent 클래스를 상속받는다. 자식클래스는 항상 super() 메서드를 통하여 부모의 constructor(생성자 함수)를 먼저 실행한다.
Parent 클래스의 constructor를 보면, setup() 메서드가 초기 생성 시 실행되도록 구현이 되어 있다. 만약 Child 클래스에서 setup() 메서드가 따로 구현되어 있지 않다면, 자동으로 Parent 클래스의 setup() 메서드를 실행하고, 만약 Child 클래스에 setup() 메서드를 명시적으로 구현하였다면, Child 클래스에서는 Child 클래스에서 구현한 setup() 메서드를 이용하여 constructor을 실행한다. 이를 메서드 오버라이딩이라고 한다.

생성자 오버라이딩도 있다. Child 클래스에서 constructor()를 이용하여 새로운 value값을 지정해주었다. 만약 어떠한 새로운 인자들을 추가할 필요가 없다면 constructor()를 작성하지 않아도 된다. 만약 자식 클래스에서 새 생성자들을 추가하고 싶다면, super()를 이용하여 부모 클래스의 constructor()에 인자를 넘겨 주어야 한다.

컴포넌트 만들기

어제는 버튼 클래스를 상속하는 토글 버튼을 만들었는데, 오늘은 좀 더 유연한 컴포넌트를 만들기 위해 버튼 클래스의 부모 클래스인 Component 클래스를 만들고 이를 상속하는 것을 만들어 보기로 한다.

먼저 모든 컴포넌트에 대해 공통적으로 구현해야 하는 것을 생각해본다.
1. $target에 컴포넌트 붙이기
2. setStatestate 바꾸기
3. render로 브라우저에 렌더하기

class Component<S> {
  $target: Element | null;
  state?: S;
  constructor({ $target }: { $target: Element | null }) {
    this.$target = $target;
  }

  setState(nextState: S) {
    this.state = { ...nextState };
    this.render();
  }

  render() {}
}

export { Component };

다음과 같이 컴포넌트 클래스를 구현하였다.
1. state는 필요한 컴포넌트도 있고, 필요 없는 컴포넌트도 있기 때문에 제네릭으로 구성하였고, 처음에는 비어있는 상태로 정의하여 하위 클래스에서 필요할 경우 state를 구현하도록 하였다. constructor에서부터 $target을 정의하여 하위 클래스에서 필요할 경우 appendChild 할 수 있도록 했다.
2. render 메서드는 constructor 내부에 넣어도 가능하지만, 현재 appendChild 방식으로 컴포넌트를 붙일지, template 방식(innerHTML) 방식으로 붙일지 확실히 결정하지 않았기 때문에 빼놓았다.
3. setState 메서드는 state를 명시했든, 명시하지 않았든 동일하게 동작하므로 가장 상위 컴포넌트에 작성 후, 하위 클래스에서는 메서드를 따로 구현하지 않도록 만들었다.

잘 작동하는지 일단 Component를 상속받는 Button 클래스를 만들어보자.

class Button<T> extends Component<T> {
  state: T;
  button: HTMLElement;
  constructor({ $target, initialState }: ButtonProps<T>) {
    super({ $target });
    this.state = initialState;
    this.button = document.createElement("button");
    this.$target?.appendChild(this.button);
    this.render();
    this.setEvent();
  }

  setEvent(): void {}
}
  1. Button클래스의 constructorinitialState를 추가로 입력받아 state를 정의하한다.
  2. button 요소를 만들어 타겟에 붙인다.
  3. render함수를 실행하여 버튼을 렌더링한다.
  4. 버튼에 필요한 이벤트를 등록한다.

보면 target을 등록하지 않았는데도 this.target을 다룰 수 있는데, 부모 클래스에서 이를 미리 정의하였기 때문이다.
render 메서드, setEvent역시 Button에 따로 등록하지 않았지만, 부모 클래스에서 메서드를 정의하였으므로 사용에 문제가 없다. 아무 내용도 없지만, Button클래스를 상속받는 ToggleButton에서 이벤트와 렌더 함수를 정의하도록 하겠다.

class ToggleButton extends Button<ToggleButtonState> {
  setEvent() {
    this.button.addEventListener("click", (e) => {
      console.log(e.target);
      this.setState({
        ...this.state,
        toggled: !this.state.toggled,
        clickCount: this.state.clickCount + 1,
      });
    });
  }

  render() {
    this.button.textContent = `${this.state.clickCount}번 클릭했습니다.`;
    this.button.className = this.state.className;
  }
}
  1. 추가해야할 생성자가 없으므로 constructor를 정의하지 않았다. 정의하지 않았더라도 ToggleButton에는 두 가지 인자가 필요하다.
  2. 실질적인 렌더링 역할을 하는 render 함수를 정의하였다.
  3. 이벤트를 등록하였다. 클릭시 state를 업데이트 하는 함수를 간단하게 만들었다. setState를 현재 클래스에서 따로 정의하지 않았지만, Component 클래스에서 이미 정의하였으므로 사용에 문제가 없다.

index파일을 만들고 한번 테스트 해보았다.

import { ToggleButton } from "./components/Button.js";

const $app = document.querySelector("#app");

new ToggleButton({
  $target: $app,
  initialState: {
    clickCount: 0,
    toggled: false,
    innerText: "0번 클릭했습니다.",
    className: "1",
  },
});

new ToggleButton({
  $target: $app,
  initialState: {
    clickCount: 0,
    toggled: false,
    innerText: "0번 클릭했습니다.",
    className: "2",
  },
});

new ToggleButton({
  $target: $app,
  initialState: {
    clickCount: 0,
    toggled: false,
    innerText: "0번 클릭했습니다.",
    className: "3",
  },
});

new ToggleButton({
  $target: $app,
  initialState: {
    clickCount: 0,
    toggled: false,
    innerText: "0번 클릭했습니다.",
    className: "4",
  },
});

new ToggleButton({
  $target: $app,
  initialState: {
    clickCount: 0,
    toggled: false,
    innerText: "0번 클릭했습니다.",
    className: "5",
  },
});


야호! 의도한대로 잘 동작한다.

마치며

아직 다듬어야 할 부분이 많지만, 그래도 의도한 대로는 동작하는 것 같아 기쁘다. 클래스의 상속과 유연하게 만드는 과정은 이론으로만 알고 있었기 때문에 실제로 코드를 쳐보면서 작성하는 것은 또 다른 느낌이었고, 매우 어려웠다... 연습할 때는 항상 코드를 치면서 연습을 하는 습관을 갖도록 하자.

0개의 댓글