티스토리 뷰

728x90
SMALL

새롭게 글을 썼습니다. diff알고리즘을 적용해서

https://ms3864.tistory.com/409

 

바닐라자바스크립트(ts)에서 컴포넌트 만들기1(diff 알고리즘)

배경 사실 이전에도 글을 올렸었다. 그런데 많이 부족한 내용이였고 버그도 많이 있었다. 이번에 기회가 있어서 새롭게 간단한 프로젝트를 만들었는데 저번보다 훨씬 괜찮게 만들었다고 생각해

ms3864.tistory.com

 

 

배경

우아한테크캠프에서 바닐라 자바스크립트로 프로젝트를 했었다. 그때 아쉬웠던 부분들을 생각하며 조금 더 보완해보려고 한다. 그리고 황준일님의 블로그를 참고했다. 

 

개요

나는 리액트, 리덕스를 사용한 경험이 있다. 이를 바탕으로 생각해보자. 리액트부터 생각을 해보면 리액트는 컴포넌트를 기반으로 동작하며 jsx를 바벨을 통해 트랜스파일해서 변환한다. 그리고 이 컴포넌트는 재사용이 가능하고 props를 넘겨줄 수 있다. 또한 상태를 가지며 이  상태가 바뀔때 렌더링을 한다. 렌더링과정에서 바뀌는 부분을 찾아 그 부분만 렌더링해준다. 이렇게 할 수 있는 이유는 트리구조로 되어 있으며 한 부분이 바뀐다면 그 아래부분까지 모두 바꾸는 것이다. 이때 React.memo를 이용해서 최적화를 할 수 있다. 또한 map을 사용할 때 key라는 것을 바탕으로 바뀌는 부분을 최소화한다. 참고로 map의 두번째인자인 index를 사용하지 말라고 하는 것은 index만 바뀌어도 새롭게 모두 렌더링하기 때문이다. 바뀌는 부분만 렌더링하는 것을 virtual dom을 이용해서 이전과 다음 상태를 트리구조로 비교해서 업데이트 하는 것이다. 가볍게 컴포넌트만 생각해봤는데 이정도가 나온다. 그리고 가장 어려운 것은 virtaul dom을 구현하는 것 같다. 리액트는 diffing알고리즘이 잘되어있다. 그래서 조금 무식하게 비교를 해도 빠른 것이다. 이걸 할 수 있을까?? 이건 컴포넌트만 얘기한 것이고 라우터나 리덕스는 다음 글에서 얘기하겠다.

 

구현할 것

먼저 구현할 것을 가볍게 생각해보자. 먼저 리액트는 요즈음 클래스보다는 함수형으로 만드는 추세다. 클래스형이 잘못됬다는 것이 절대 아니다. 여러가지 장단점들이 있는데 글이 너무 길어지므로 생략하겠다. 함수형은 클래스보다 구현하기 훨씬 어렵다... 그래서 클래스 형으로 만들자. 리액트처럼 상속하는 방식으로 하면 될 것 같다. constructor에 파라미터는 무엇이 들어가야 할까?? 리액트라면 props만 들어가면 된다. 그런데 여기서 문제가 하나 있다. html을 그냥 리턴할 수 있을까?? 리액트가 가능한 이유는 jsx를 바벨을 통해 컴파일링하기 때문이다. 그러면 jsx를 구현해야 한다. 그런데 꼭 구현해야 할까?? 물론 하면 좋다. 하지만 이건 내생각에 필수사항이 아니다. 추후에 필요하면 jsx를 구현하고 일단은 그냥 진행하자. 그러려면 내용을 담을 태그가 필요하다. 그래서 target을 constructor에 파라미터로 넣어줘야 한다. 그리고 리액트를 보면 constructor에서 state를 정의하고 render가되고 componentdidmount가 실행된다. 대충 이런식으로 만들면 되겠네. 조금 문제가 있다면 상태가 바뀔때 업데이트 하는 부분이다. virtual dom을 구현하려면 트리순회를 해가면서 해야되는데 조금 나에게는 벅차다. 나뿐만 아니라 우아한테크캠프를 통해 우아한형제들을 들어간 작년 기수사람들이나 이번기수 사람들에게도 그랬다. 그래서 그냥 최대한 해보고 안되면 그냥 전체 랜더링을 해버리자.

3줄요약

1. 컴포넌트를 클래스로 구현하고 상속하자

2. constructor에는 target, props를 넣자

3. 생성자 및 render 순서를 고려하자

 

기본 틀 잡기

class Component {
  constructor(target, props) {
    this.target = target;
    this.props = props;
    this.setup();
    this.render();
    this.componentDidMount();
  }

  setup() {}

  render() {
    this.target.innerHTML = this.markup();
  }

  componentDidMount() {}

  markup() {
    return '';
  }

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

export default Component;

가볍게 틀을 잡아봤다. setsup에서는 this.state={~~}를 넣어준다. constructor에서 하지 않고 이렇게 한 이유는 실행 순서를 지키기 위해서다.

1. state정의 2. 렌더링 3. 상태를 바꾸거나 렌더링이 된 후에 실행할 것들을 실행

markup에서는 string으로 html태그를 넣어주고 render에서 target에 innerhtm으로 넣어주면 된다. 그리고 componentDidMount에서 보통 비동기로 데이터들을 가져와서 state를 바꾼다. state를 바꿀때는 setState를 이용해 상태를 바꾸고 render 메서드를 호출한다.

 

문제점 찾기

여기서 문제점을 찾아보자.

첫번째는 state가 바뀔때마다 전부다 새로 그려지는 것이다. virtual dom을 구현하지는 못하더라도 조금이나마 괜찮은 방법이 없을까?? 시도해보자!!!

또 하나의 컴포넌트가 너무 커지면 안된다. 그러면 재사용도 힘들고 유지보수도 힘들다. 컴포넌트를 나누는 기준은 개발자의 취향이나 디자인 패턴에 따라 다르지만 무조건 나눠야 한다. 그럴려면 하위 컴포넌트들을 넣어야한다. 어디서 넣어야할까?? constructor에서 메서드 호출을 추가하면 되겠다.

세번째로 타겟을 주게되면 불필요한 태그가 들어가게 된다. 필요한 경우도 있겠지만 불필요한 경우가 있다. 이럴때는 부모 태그를 제거해주면 된다.

네번째로 비슷한 컴포넌트가 필요할 때가 있다. styled-components를 사용하면 props로 처리하면 되지만 지금은 그런 라이브러리를 사용하지 않기 때문에 props로 받고 if문으로 클래스이름을 할당해서 넣어줘야 한다. 이렇게 해도 되지만 한번에 클래스를 넘겨주고 싶다.(개인의 취향..) 

다섯번째로 이벤트를 생각해보자. 리액트에서는 생각없이편하게 이벤트를 넣어주면 된다. 사실 리액트에서는 이벤트 위임을 해준다. 그래서 직접 구현할때는 이벤트 위임을 생각해야 한다.

 

리렌더링

  setState(nextState) {
    this.state = { ...this.state, ...nextState };
    this.update();
  }

  update() {
    const newMarkup = this.markup();

    const newDom = document.createRange().createContextualFragment(newMarkup);

    const newElements = [...newDom.querySelectorAll('*')];
    const currentElements = [...this.target.querySelectorAll('*')];

    if (newElements.length !== currentElements.length) {
      this.target.innerHTML = newMarkup;
      return;
    }

    for (let i = 0; i < newElements.length; i++) {
      const newEl = newElements[i];
      const curEl = currentElements[i];

      if (newEl.childElementCount !== curEl.childElementCount) {
        this.target.innerHTML = newMarkup;
        return;
      }

      if (!newEl.isEqualNode(curEl)) {
        if (newEl.tagName !== curEl.tagName) {
          curEl.replaceWith(newEl);
        } else {
          if (
            curEl.firstChild?.nodeName === '#text' &&
            newEl.firstChild?.nodeName === '#text' &&
            curEl.firstChild.nodeValue !== newEl.firstChild.nodeValue
          ) {
            curEl.firstChild.nodeValue = newEl.firstChild.nodeValue;
          }

          const curAttributes = curEl.attributes;
          const newAttributes = newEl.attributes;

          [...curAttributes].forEach((curAttr) => {
            if (!newAttributes.getNamedItem(curAttr.name))
              curEl.removeAttribute(curAttr.name);
          });

          [...newAttributes].forEach((newAttr) => {
            const currentAttribute = curAttributes.getNamedItem(newAttr.name);
            if (!currentAttribute || currentAttribute.value !== newAttr.value)
              curEl.setAttribute(newAttr.name, newAttr.value);
          });
        }
      }
    }
  }

위의 업데이트 방식은 사실 미완성이다. 하지만 input text를 바꾸거나 클래스명이 바뀌거나 이런 경우에는 업데이트가 동작한다. 

document.createRange().createContextualFragment(newMarkup)를 통해서 바뀐 마크업의 태그들을 가져온다. range객체를 이용했는데 조금 생소한 개념이다. range, selection은 보통 웹 에디터를 만들때 사용한다.

쉽게 말해서 그냥 fragment에 string을 html태그로 바꿔서 만든다고 생각하면 된다. 그래서 이전과 다음 html을 비교하는 것이다.

먼저 개수가 다르다면 그냥 render 메서드를 호출해버린다.(미완성...)

또 childElementCount가 다르다면 render 메서드를 호출한다.(미..완성)

그리고 tagName이 다르면 변경하고 텍스트가 다르면 변경하고 attribute가 이전 마크업에만 있다면 제거하고 바뀌거나 생겼다면 setAttribute를 하는 식이다.

사실 코드 자체는 엄청 간단한다. 그냥 전부 가져와서 태그, 텍스트, 속성만 바꿔주는 것이다. jsx처럼 객체로 바꾸지 않아도 이 방법으로 조금 개선한다면 괜찮게 만들수있다.??


혹시 range, selection에 대해 궁금하다면....

https://ko.javascript.info/selection-range

 

Selection and Range

 

ko.javascript.info

https://www.youtube.com/watch?v=smV4W4OJRD0&ab_channel=%EC%9C%A4%EC%9C%A4 


자식 컴포넌트 만들기

  constructor(target, props) {
    this.target = target;
    this.props = props;
    this.setup();
    this.render();
    this.appendComponent();
    this.componentDidMount();
  }
  
  appendComponent() {}

사실 이건 별거없다. 그냥 하위 컴포넌트를 호출하면 된다. render가 되고 하위 컴포넌트를 render하고 componentDidMount를 호출하자.

조금 더 구체적으로 말하면 상위 컴포넌트에서는 타겟이 될 태그를 만들고 render가 되고 그 타겟에 마크업을 넣어주는 것이다.

 

예시

class MainPage extends Component {
  markup() {
    return /* html */ `
      <div class="button-div"></div>
      <div class="button-div2"></div>
    `;
  }

  appendComponent() {
    const $buttonDiv = document.querySelector('.button-div');
    const $buttonDiv2 = document.querySelector('.button-div2');
    new Button($buttonDiv);
    new Button($buttonDiv2);
  }
}

 

부모 태그 제거

  render() {
    this.target.innerHTML = this.markup();
    if (this.props?.inside) {
      this.target.replaceWith(...this.target.childNodes);
    }
  }

props로 inside를 넣어주면 타겟의 모든 노드들을 가져오고 replace해주는 것이다. 이렇게하면 간단하게 해결할 수 있다.

 

컴포넌트에 클래스 추가하기

  render() {
    this.target.innerHTML = this.markup();
    if (this.props?.class) {
      const el = this.target.firstElementChild;
      const classArr = this.props.class.split(' ');
      classArr.forEach((className) => {
        el.classList.add(className);
      });
    }
    if (this.props?.inside) {
      this.target.replaceWith(...this.target.childNodes);
    }
  }

간단한 로직으로 클래스를 추가하자

 

이벤트 위임

  constructor(target, props) {
    this.target = target;
    this.props = props;
    this.setup();
    this.render();
    this.appendComponent();
    this.componentDidMount();
    this.setEvent();
  }
  
  setEvent() {}

이렇게 만들고 상위 컴포넌트에서 setEvent에다가 이벤트를 걸어주면 된다. 이러면 이벤트위임 끝이다. 그런데 그렇게하면 

이렇게 else if가 지저분하게 나오게되고 코드가 지저분해진다. 이를 해결하기 위해서 이벤트를 넣어주는 메서드가 필요하다.

 

이벤트 메서드 만들기

  addEvent(eventType, selector, callback) {
    const children = [...this.target.querySelectorAll(selector)];
    const isTarget = (target) =>
      children.includes(target) || target.closest(selector);
    this.target.addEventListener(eventType, (event) => {
      if (!isTarget(event.target)) return false;
      callback(event);
    });
  }

이부분 황준일님 블로그 그냥 그대로 보고 배꼈다.... 더 좋은 방법이 있을까 생각했는데 찾지 못했다.

코드는 딱히 어려운 부분이 없어서 설명은 생략하겠다.

 

앞으로 필요한 것들

일단 컴포넌트를 만들어봤다. 다음에는 전역상태관리에 대해서 얘기해보겠다...

 

 

 

오류...

사용하다보니 오류 및 수정하고 싶은 부분이 있었다. 먼저 props에 inside를 써주는게 너무 귀찮았다. 그래서 그냥 <inside>태그에 넣고 nodeName으로 replace해줬다.(순서를 주의하자.. appendComponent를 먼저 호출해야 한다. 태그를 replace해버리면 렌더링 과정에서 문제가 생긴다)

  render() {
    this.target.innerHTML = this.markup();
    if (this.props?.class) {
      const el = this.target.firstElementChild;
      const classArr = this.props.class.split(' ');
      classArr.forEach((className) => {
        el.classList.add(className);
      });
    }
    this.appendComponent(this.target);
    if (this.target.nodeName === 'INSIDE') {
      this.target.replaceWith(...this.target.childNodes);
    }
  }

 

또 update를 할 때 appendComponent가 있으면 제대로 동작하지 않았다. 내부 코드를 찾아보니 새로운 dom을 가져올 때 markup을 기준으로 가져와서 appendComponent의 코드가 동작하지 않아서 그랬다. 그래서 전체적인 코드 수정이 필요했다. appendComponent을 호출할 때 target 인자값을 넣어줘서 경우를 나눠야 한다. 먼저 constructor에서 appendComponent를 없애고 render 메서드의 마지막줄에 this.appendComponent(this.target); 를 넣어줘야 한다. 또 update 메서드를 다음과 같이 수정해야 한다. 

  update() {
    const newMarkup = this.markup();

    const newDom = document.createRange().createContextualFragment(newMarkup);
    this.appendComponent(newDom);

    const newElements = [...newDom.querySelectorAll('*')];
    const currentElements = [...this.target.querySelectorAll('*')];

    if (newElements.length !== currentElements.length) {
      this.target.innerHTML = newMarkup;
      this.appendComponent(this.target);
      return;
    }

    for (let i = 0; i < newElements.length; i++) {
      const newEl = newElements[i];
      const curEl = currentElements[i];

      if (newEl.childElementCount !== curEl.childElementCount) {
        this.target.innerHTML = newMarkup;
        this.appendComponent(this.target);
        return;
      }

 

update에서 또 문제가 발생했다. 에러 메세지를 axios로 불러오는 과정에서 제대로 동작하지 않아서 차근차근 다시 살펴봤다. 왜 그런지 봤더니 <div></div>의 firstchild는 null이고 <div>error</div>의 firstchild의 nodevalue는 text라서 그렇다. 텍스트가 사라지는 경우도 마찬가지다. 그래서 로직을 좀 추가해줬다.

          if (curEl.firstChild?.nodeName === '#text' && newEl.firstChild?.nodeName === '#text') {
            if (curEl.firstChild.nodeValue !== newEl.firstChild.nodeValue) {
              curEl.firstChild.nodeValue = newEl.firstChild.nodeValue;
            }
          } else if (curEl.firstChild?.nodeName === '#text') {
            curEl.removeChild(curEl.firstChild);
          } else if (newEl.firstChild?.nodeName === '#text') {
            const text = document.createTextNode(newEl.firstChild.nodeValue);
            curEl.appendChild(text);
          }

 

또 오류가 나왔다. inside로 만든 태그가 update가 될때 문제가 생겨서 차근차근 디버깅해봤다. target에 replace를 해주면 타겟은 그대로 있고 아래 태그들만 바뀌었다. 그래서 this.target이 이상해졌던 것이다. 그리고 inside를 true로 한 이유는 업데이트를 할 때 markup은 inside태그를 포함하기 대문에 경우를 나누기 위해서 변수를 만들었다.

  render() {
    this.target.innerHTML = this.markup();
    if (this.props?.class) {
      const el = this.target.firstElementChild;
      const classArr = this.props.class.split(' ');
      classArr.forEach((className) => {
        el.classList.add(className);
      });
    }
    this.appendComponent(this.target);
    if (this.target.nodeName === 'INSIDE') {
      this.inside = true;
      const temp = this.target.firstElementChild;
      this.target.replaceWith(...this.target.childNodes);
      this.target = temp;
    }
  }
  
  update() {
    const newMarkup = this.markup();

    const newDom = document.createRange().createContextualFragment(newMarkup);

    this.appendComponent(newDom);

    const newElements = this.inside ? [...newDom.querySelectorAll('*')].slice(1) : [...newDom.querySelectorAll('*')];
    // 생략
    }

 

그리고 함수가 커져서 리팩토링을 했다.

  render() {
    this.target.innerHTML = this.markup();
    if (this.props?.class) {
      this.addClass(this.target);
    }
    this.appendComponent(this.target);
    if (this.target.nodeName === 'INSIDE') {
      this.inside = true;
      this.target = this.changeInside(this.target);
    }
  }

  changeInside(target) {
    const temp = target.firstElementChild;
    target.replaceWith(...target.childNodes);
    return temp;
  }

 

또 오류가 나왔다.... 디버깅해보면서 고생했는데 어렵지 않은 부분이였다. 프로젝트를 하는 도중에 태그가 중복되는 경우가 나타났다. 렌더할때처럼 update할때 inside처리를 안해줘서 그랬다. outerhtml이라던가 map이라던가 다른 것들로 삽질을 했다. 이것과는 별개로 조금 dom에대한 깊은 공부를 해야될 필요성을 느꼈다.

    if (newElements.length !== currentElements.length) {
      this.target.innerHTML = newMarkup;
      if (this.inside) {
        this.target = this.changeInside(this.target);
      }
      this.appendComponent(this.target);
      return;
    }

    for (let i = 0; i < newElements.length; i++) {
      const newEl = newElements[i];
      const curEl = currentElements[i];
      if (newEl.childElementCount !== curEl.childElementCount) {
        this.target.innerHTML = newMarkup;
        if (this.inside) {
          this.target = this.changeInside(this.target);
        }
        this.appendComponent(this.target);
        return;
      }

 

또 오류를 찾았다. 이벤트가 사라져서 생각해보니 target을 update될때마다 바꿔버리면 이벤트위임이 의미가 없어진다. 다시 디버깅 시작...

일단 currentElements가 inside일때도 나눠서 생각해야되는데 그러지 않았었다. 그리고 update를 새로 새로 랜더링할때 inside가 true라면 firstElementChild.outerHTML을 해주면 첫번째 노드의 값을 string으로 반환하는데 이를 이용하면 된다. 그리고 appendComponent는 생각해보니 필요가 없었다. 그냥 jsx를 만들껄 하는생각이 든다... 시간이 된다면 다음에 만들어봐야겠다.

    const newElements = this.inside ? [...newDom.querySelectorAll('*')].slice(1) : [...newDom.querySelectorAll('*')];
    const currentElements = this.inside
      ? [...this.target.querySelectorAll('*')].slice(1)
      : [...this.target.querySelectorAll('*')];

    if (newElements.length !== currentElements.length) {
      this.target.innerHTML = this.inside ? newDom.firstElementChild.outerHTML : newMarkup;
      return;
    }

    for (let i = 0; i < newElements.length; i++) {
      const newEl = newElements[i];
      const curEl = currentElements[i];
      if (newEl.childElementCount !== curEl.childElementCount) {
        this.target.innerHTML = this.inside ? newDom.firstElementChild.outerHTML : newMarkup;
        return;
      }

 

또 오류... 기본기의 부족함을 많이 느끼는중

currentElements는 그냥 모든 태그를 가져오는게 맞다. 다시 생각해보니...

그리고 newElements부분이 잘못되었다. 모든 태그를 가져오고 slice를 해버리면 원하지 않는 값을 가져온다. 이게 가능한 경우는 children이 없는 경우이다. 그래서 firstElementChild로 가져와야한다. 

그리고 개수가 다를때 전체 렌더링을 새로할때도 잘못됬었다. 위의 방법으로 하면 맨위태그가 중복되게 된다. 각각의 children을 map으로 outerHTML해서 합쳐줘야 한다. 이제 진짜 끝... 이기를 제발

newMarkup부분도 newDom.firstElementChild.outerHTML을 해줘야 한다. 왜냐하면 하위태그에 inside가 있는 경우도 있기 때문이다. 

    const newElements = this.inside
      ? [...newDom.firstElementChild.querySelectorAll('*')]
      : [...newDom.querySelectorAll('*')];
    const currentElements = [...this.target.querySelectorAll('*')];

    if (newElements.length !== currentElements.length) {
      this.target.innerHTML = this.inside
        ? [...newDom.firstElementChild.children].map(el => el.outerHTML).join('')
        : newDom.firstElementChild.outerHTML;
      return;
    }

    for (let i = 0; i < newElements.length; i++) {
      const newEl = newElements[i];
      const curEl = currentElements[i];
      if (newEl.childElementCount !== curEl.childElementCount) {
        this.target.innerHTML = this.inside
          ? [...newDom.firstElementChild.children].map(el => el.outerHTML).join('')
          : newDom.firstElementChild.outerHTML;
        return;
      }

 

이번에는 오류는 아니다... setstate를 하고나서 update가 되고 그 다음에 함수를 실행하려고 했다. 그런데 방법이 없었다. 처음에는 nextRender, nextUpdate와 같은 함수를 만들어서 처리하려고 했는데 특정 조건일 때 구현이 불가능했다. 그래서 setstate의 두번째 파라미터에 콜백함수를 넣어줬다.

  setState(nextState, cb) {
    this.componentDidUpdate({ ...this.state }, { ...this.state, ...nextState });
    this.state = { ...this.state, ...nextState };
    this.update();
    if (cb) {
      cb();
    }
  }

 

전체코드

class Component {
  constructor(target, props = {}) {
    this.target = target;
    this.props = props;
    this.setup();
    this.render();
    this.componentDidMount();
    this.setEvent();
  }

  setup() {}

  render() {
    this.target.innerHTML = this.markup();
    if (this.props?.class) {
      this.addClass();
    }
    this.appendComponent(this.target);
    if (this.target.nodeName === 'INSIDE') {
      this.inside = true;
      this.changeInside();
    }
  }

  addClass() {
    const el = this.target.firstElementChild;
    const classArr = this.props.class.split(' ');
    classArr.forEach(className => {
      el.classList.add(className);
    });
  }

  changeInside() {
    const temp = this.target.firstElementChild;
    this.target.replaceWith(...this.target.childNodes);
    this.target = temp;
  }

  appendComponent() {}

  componentDidMount() {}

  componentDidUpdate() {}

  markup() {
    return '';
  }

  setEvent() {}

  addEvent(eventType, selector, callback) {
    const children = [...this.target.querySelectorAll(selector)];
    const isTarget = target => children.includes(target) || target.closest(selector);
    this.target.addEventListener(eventType, event => {
      if (!isTarget(event.target)) return false;
      callback(event);
    });
  }

  setState(nextState, cb) {
    this.componentDidUpdate({ ...this.state }, { ...this.state, ...nextState });
    this.state = { ...this.state, ...nextState };
    this.update();
    if (cb) {
      cb();
    }
  }

  update() {
    const newMarkup = this.markup();

    const newDom = document.createRange().createContextualFragment(newMarkup);

    this.appendComponent(newDom);

    const newElements = this.inside
      ? [...newDom.firstElementChild.querySelectorAll('*')]
      : [...newDom.querySelectorAll('*')];
    const currentElements = [...this.target.querySelectorAll('*')];

    if (newElements.length !== currentElements.length) {
      this.target.innerHTML = this.inside
        ? [...newDom.firstElementChild.children].map(el => el.outerHTML).join('')
        : newDom.firstElementChild.outerHTML;
      return;
    }

    for (let i = 0; i < newElements.length; i++) {
      const newEl = newElements[i];
      const curEl = currentElements[i];
      if (newEl.childElementCount !== curEl.childElementCount) {
        this.target.innerHTML = this.inside
          ? [...newDom.firstElementChild.children].map(el => el.outerHTML).join('')
          : newDom.firstElementChild.outerHTML;
        return;
      }
      if (!newEl.isEqualNode(curEl)) {
        if (newEl.tagName !== curEl.tagName) {
          curEl.replaceWith(newEl);
        } else {
          if (curEl.firstChild?.nodeName === '#text' && newEl.firstChild?.nodeName === '#text') {
            if (curEl.firstChild.nodeValue !== newEl.firstChild.nodeValue) {
              curEl.firstChild.nodeValue = newEl.firstChild.nodeValue;
            }
          } else if (curEl.firstChild?.nodeName === '#text') {
            curEl.removeChild(curEl.firstChild);
          } else if (newEl.firstChild?.nodeName === '#text') {
            const text = document.createTextNode(newEl.firstChild.nodeValue);
            curEl.appendChild(text);
          }

          const curAttributes = curEl.attributes;
          const newAttributes = newEl.attributes;

          [...curAttributes].forEach(curAttr => {
            if (!newAttributes.getNamedItem(curAttr.name)) curEl.removeAttribute(curAttr.name);
          });

          [...newAttributes].forEach(newAttr => {
            const currentAttribute = curAttributes.getNamedItem(newAttr.name);
            if (!currentAttribute || currentAttribute.value !== newAttr.value)
              curEl.setAttribute(newAttr.name, newAttr.value);
          });
        }
      }
    }
  }
}

export default Component;

 

참고자료 https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Component/

 

Vanilla Javascript로 웹 컴포넌트 만들기 | 개발자 황준일

Vanilla Javascript로 웹 컴포넌트 만들기 9월에 넥스트 스텝 (opens new window)에서 진행하는 블랙커피 스터디 (opens new window)에 참여했다. 이 포스트는 스터디 기간동안 계속 고민하며 만들었던 컴포넌트

junilhwang.github.io

유튜브(블로그와 조금 다른 부분 존재)

컴포넌트 만들기

https://www.youtube.com/watch?v=IJPkmFtr_2I&ab_channel=%EC%9C%A4%EC%9C%A4 

컴포넌트 예시

https://www.youtube.com/watch?v=ME5uk1-iKiA&ab_channel=%EC%9C%A4%EC%9C%A4 

728x90
LIST
댓글
공지사항