OOJS with Class Component

김영현·2023년 10월 4일
0

서론

최근엔 함수형 프로그래밍 패러다임이 떠올랐다. 클래스형 컴포넌트를 사용하던 리액트도 함수형으로 바뀐지 좀 됐음.
그렇다고 객체지향 프로그래밍이 올드한건 절대 아니다. 아직도 수많은 프로그램은 객체지향으로 돌아가고 있다.
함수형과 객체지향은 빛과 어둠이 아닌 왼쪽, 오른쪽이다. 방향의 차이다.
결국 개발자를 자칭할 수 있는 사람이 되려면 객체지향정도는 알고 넘어가야한다.
이론은 OOJS시간에 다뤘으니, 예제로 연습해보자.
나는 클래스형 컴포넌트바닐라 JS로 구현해보기로 했다.


컴포넌트

그 전에 컴포넌트에 대해 짚고 넘어가야한다.

출처 : 위키백과

아하 그러니까, 어떠한 기계를 구성하는 요소구나!
=> 프로그래밍에서는 재사용 가능한 구성요소라고 정의한다.

컴포넌트를 사용하는 소프트웨어 목록을 보면 알겠지만, 생각보다 유명한 곳에서 많이 쓰이고있다.
또한, 1990년대부터 시작되었다고하니 생각했던 것 보다 더 오래된 근본있는 방식이다.

고리타분한 단어 정의는 여기까지만 설명하고, 리액트의 컴포넌트에 대해 살펴보자.

React applications are built from isolated pieces of UI called components. A React component is a JavaScript function that you can sprinkle with markup. Components can be as small as a button, or as large as an entire page. Here is a Gallery component rendering three Profile components
출처

독립적인 UI 조각이자 마크업을 뿌리는(렌더링) 함수다..
컴포넌트는 무엇이든 될 수 있음. 버튼, 갤러리, 프로필....등 크고 작은 다양한 페이지로!
내부적으로는 html태그를 사용하지만, 리액트가 이를 받아 가공해서 사용한다(JSX).
서론에 설명했듯 최근엔 함수형 컴포넌트를 사용하지만, 나에겐 클래스형 컴포넌트가 필요하다

클래스형 컴포넌트

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

클래스형 컴포넌트는 이렇게 사용한다. 상속받아서 재정의(overriding)하는 메소드는 단 하나.
render()함수다. 누가 봐도 화면을 보여주는 함수.
이걸 먼저 구현해보자. 복잡하게 생각하지 말고!

class Component {
  //컴포넌트가 가지고있어야 할 프로퍼티들....내용, 상태. 부모자식도 필요할것 같긴한데, 더 생각해보자
  constructor(props) {
    this.props = props;
    this.state = {};
  }
  render() {
    //...반환값을 렌더링 한다
  }
  createElement() {
    const html = this.render();
    const template = document.createElement("template");
    template.innerHTML = html.trim();
    return template.content.firstChild;
  }
}
class Welcome extends Component {
  render() {
    return `<h1>Hello!</h1>`;
  }
}

const welcome = new Welcome();
const $root = document.getElementById("root");
$root.appendChild(welcome.createElement());

이게 맞는걸까? 싶기도 하고. 일단 간단하게 구현해봤다.

잘 나오는구먼?
이제 리액트의 핵심인 상태(state)를 추가해보자. 상태에 따라 렌더링이 바뀌어야한다.

//컴포넌트의 setState부분
 setState(state) {
    const prevState = this.state;
    this.state = { ...this.state, state };
    if (prevState !== this.state) this.render();
  }
...
class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  onClick = () => {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count);
  };
  render() {
    const { count } = this.state;
    return `<button onclick='${this.onClick}'>클릭 횟수 : ${count}</button>`;
  }
}

에러1(innerHTML에 인라인 함수 전달)

얼랍쇼? 원하는대로 되지 않는다. 버튼을 클릭해도 반응이 없다.
개발자도구를 열어 요소를 열어보니 이렇게 되어있었다.

<button onclick="() => {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count);
  }">클릭 횟수 : 0</button>

html에서는 이벤트 리스너에 바로 함수를 작성하는건 적용되지 않는다.
JS에서나 가능한 일이다.
그리고 innerHTML()에 html요소에 함수까지 포함해서 반환하는건 보안상 문제가 있다고 한다.
크로스 사이트 스크립팅 공격(XSS. 악의적인 사용자가 스크립트를 끼워넣는거다).
다른 방식을 찾아보자.

  • addEventListner()를 이용한다.
    먼저 생각난 방법이지만, 이 함수를 사용하려면 DOM을 지정할 식별자가 필요하다.
    id, class할당을 어떻게 할 것인지가 관건이다.

  • DOMParser를 이용해 파싱 후 적절히 addEventListner를 붙여준다.
    굉장히 수고로움이 많이 들것 같다. 그냥 리스너를 붙이는 선에서 합의 보겠음.

좋아. 그냥 이벤트리스너를 붙이는걸 택했다.
일단 여기서 킵해두고, 렌더함수를 잠깐 만져봐야겠다.
컴포넌트는 결국 html태그 어딘가에 붙일것이다. 따라서 구분할수 있는 id가 필요하다

class Component {
  constructor(parent, props) {
    this.props = props;
    this.state = {};
    this.$parent = parent;
  }
  render() {
    //...반환값을 렌더링 한다
  }
  createElement() {
    const html = this.render();
    const template = document.createElement("template");
    template.innerHTML = html.trim();
    this.$parent.appendChild(template.content.firstChild);
  }

이렇게 부모를 명명한다.
그리고 부모에 달아줘야한다. id로 구분하기보다는 data속성을 이용하여 구분짓는 게 나아보인다.
잘 되가고있는데 또 문제가 생겼따.


프로퍼티 오버라이딩(부모constructor 내부에서 함수 호출 후 상속받은 클래스에서 super()호출 후 오버라이딩)

class test {
  constructor(parent) {
    this.parent = parent;
    this.val = {};
    this.render();
  }
  render() {}
}

class test2 extends test {
  constructor(parent) {
    super(parent);
    this.val = { count: 1 };
  }
  render() {
    console.log(this.val);
  }
}

new test2("g");

예상한 바로는 testthis.val을 오버라이딩하여 빈 객체가 아니어야하는데, 빈 객체로 나온다.
아마 constructor내부에서 생성하며 바로 render()를 실행하기 때문인것 같은데...
어떻게 해결해볼까?

  1. constructor 내부에 render를 사용하지 않는다. 간단한 방법같다.
class Component {
  constructor(parent, props) {
    this.props = props;
    this.state = {};
    this.$parent = parent;
  }
  render() {
    //...반환값을 렌더링 한다
  }
  renderChild() {
    // 자식 있으면 렌더링...
  }
  createElement() {
    const html = this.render();
    const template = document.createElement("template");
    template.innerHTML = html.trim();
    this.$parent.appendChild(template.content.firstChild);
    this.renderChild();
  }

이후 부를땐 new Component.createElemnt()로 부른다.
뭔가 이름이 너무 길어진다...

  1. 하나 더 생각해보자.

아무튼 해결완. 그다음 setState로 상태 조작을 해야한다.
addEventListener로 더해주기로했는데, 맨 위 클래스에서 추상화를 하는게 좋아보인다.

https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Component/#_2-%E1%84%8B%E1%85%B5%E1%84%87%E1%85%A6%E1%86%AB%E1%84%90%E1%85%B3-%E1%84%87%E1%85%A5%E1%84%87%E1%85%B3%E1%86%AF%E1%84%85%E1%85%B5%E1%86%BC
이분의 블로그를 많이 참고했는데, 그중 closest이라는 메소드를 처음 알게되었다.

  • closest : 주어진 셀렉터를 찾을때까지 부모방향으로 계속 진행. false/true 반환

아주좋은 메소드군.

setEvent(selector, eventType, handler) {
    //컴포넌트에 이벤트를 달고, 버블링 활용. selector는 '[data-component="이름"]'
    const eventTarget = [...this.parent.querySelectorAll(selector)];
    this.parent.addEventListener(eventType, (e) => {
      if (e.target.closest(selector)) return false;
      handler(e);
    });
  }

요래 달았다.
근데 또 문제가 생겼다.


이벤트 추가 중복, this

  onClick = () => {
    this.setState({ count: this.state.count + 1 });
  };
  setEvent() {
    this.addEvent('[data-component="Counter"]', "click", this.onClick);
  }

addEvent는 상속받아 사용한다. onClick이 화살표함수니까, 바인딩이 호출한 ...위로가나보다
저걸 축약메소드로 표현해도 undeifned가 나온다.

  setEvent() {
    this.addEvent('[data-component="Counter"]', "click", () => {
      this.setState({ count: this.state.count + 1 });
    });
  }

이렇게 직접 전달하면 오류가 없다.
또한 렌더링할때마다 이벤트를 등록했더니, count가 미쳐 날뛰었다.
이벤트는 무조건 한 번만 등록하자...


profile
모르는 것을 모른다고 하기

0개의 댓글