그럴듯한 개발 블로그
반응형

완성본 코드

https://github.com/donghyun1998/vanilla_js_SPA_starter_kit/blob/main/src/utils/useEffect.js

 

컴포넌트의 생명주기를 관리 하는 useEffect 리액트 훅을 구현하려 했다.

 

실제 useEffect는 아래와 같이 동작하지 않는다. 오해 금지

단지 내 vanilla js SPA 환경에서 사용할 함수 추상화 한 것일 뿐

먼저 내 vanilla js SPA 환경에서는 모든 렌더링을 HTMLElement.innerHTML로 한다.

즉 컴포넌트 마운트, 업데이트, 언마운트 모두 innerHTML로 되므로

HTMLElement의 변경 감지가 필요하다.

 

MutationObserver api를 사용하면 해당 HTMLElement의 변경을 감지할 수 있다.

/**
 * MutationObserver를 사용하여 대상 요소의 변화를 감지하는 Hook
 * @param {function()}callback
 * @param {HTMLElement}targetNode
 * @returns {function() | void}
 */

export default function useEffect(callback, targetNode) {
  // 대상 요소가 유효한지 확인
  if (!targetNode) {
    console.error('useEffect: 대상 요소가 지정되지 않았거나 존재하지 않습니다.');
    return;
  }
  // 기본 감시 옵션 설정     속성 변화 감지 |  자식 요소 변화 감지 | 모든 후손 요소 변화 감지 | 텍스트 노드 변화 감지
  const config = { attributes: true, childList: true, subtree: true, characterData: true};

  // MutationObserver 콜백 정의
  const observerCallback = () => {
    callback(); // 변화가 감지될 때 콜백 함수 실행
  };

  // MutationObserver 인스턴스 생성
  const observer = new MutationObserver(observerCallback);

  // 감시 시작
  observer.observe(targetNode, config);

  // 정리(Cleanup) 함수 반환
  return () => observer.disconnect(); // 이 함수를 호출하여 감시를 중지
}

useState때와 비슷하게 클로저로 useEffect 삭제 함수를 리턴해서, 참조가 풀리지 않게 했다.

 

let useEffect멈추는함수 = useEffect(콜백함수(), 감시 할 HTMLElement);

이렇게 사용한다.

useEffect 클로저가 free되지 않게 변수로 참조하고 있는다.

 

그런데 문제가 생겼다.

export default function useEffectPrac($container) {
  let inputData = {name: '김동현', age: 25}
  let [getData, setData] = useState(inputData, this, 'renderCard');
  let clearEffect = useEffect(() => {alert('콜백함수 실행')}, $container.querySelector('.card'));

  this.init = () => {
    this.render();
    this.renderCard();
    $container.querySelector('.increase-age').addEventListener('click', () => {
      let state = {...getData()}; // 깊은 복사 해주려고 구조할당분해 사용
      state.age += 1;
      console.log(state, getData());
      setData(state);
    });
    $container.querySelector('.navigate-to-root').addEventListener('click', () => {
      navigate('/');
    });
  }

  this.render = () => {
    importCss("../style/card-page.css");
    $container.innerHTML = `
      <div>
        <div class="title">useEffect를 사용해 봅시다</div>
        <div class="card"></div>
        <button class="increase-age">한 살 먹기</button>
      </div>
      <button class="navigate-to-root">Go to Root</button>
    `;
  }
  this.renderCard = () => {
    $container.querySelector('.card').innerHTML = `
      <div class="name">제 이름은 ${getData().name} ${getData().age}살 이죠 </div>
    `;
  }

  this.init();
}

이렇게 분명 .card에 useEffect를 걸어줬는데 콜백함수가 호출이 안된다.

왜인가 하니 이미 render 되지 않은 상태에서 useEffect가 호출되어서 해당 엘리먼트를 못 찾는 것이다.. ㅋㅋ

 

아래와 같이 useEffect 호출 시점을 변경하니까 잘 된다.

import {navigate} from "../../utils/navigate.js";
import {importCss} from "../../utils/importCss.js";
import useState from "../../utils/useState.js";
import useEffect from "../../utils/useEffect.js";

export default function useEffectPrac($container) {
  let inputData = {name: '김동현', age: 25}
  let [getData, setData] = useState(inputData, this, 'renderCard');

  this.init = () => {
    this.render();
    this.renderCard();
    $container.querySelector('.increase-age').addEventListener('click', () => {
      let state = {...getData()}; // 깊은 복사 해주려고 구조할당분해 사용
      state.age += 1;
      console.log(state, getData());
      setData(state);
    });
    $container.querySelector('.navigate-to-root').addEventListener('click', () => {
      navigate('/');
    });
    this.clearEffect = useEffect(() => {alert('콜백함수 실행')}, $container.querySelector('.card'));
  }

  this.render = () => {
    importCss("../style/card-page.css");
    $container.innerHTML = `
      <div>
        <div class="title">useEffect를 사용해 봅시다</div>
          <div class="card"></div>        
        <button class="increase-age">한 살 먹기</button>
      </div>
      <button class="navigate-to-root">Go to Root</button>
    `;
  }
  this.renderCard = () => {
    $container.querySelector('.card').innerHTML = `
      <div class="name">제 이름은 ${getData().name} ${getData().age}살 이죠 </div>
    `;
  }

  this.init();
}
항상 해당 엘리먼트가 render 된 이후에 useEffect 호출하도록 하자
(엄격근엄진지)

진짜 중요함

 

이제 useEffect도 사용해서 개발해야겠다 슈웃

 

더 좋은 방법이 생각나면 개선 예정

 

반응형

'<frontend> > vanilla-js-SPA-starter-kit' 카테고리의 다른 글

useReducer 구현  (2) 2024.02.12
바닐라 js SPA 스타터킷  (1) 2024.02.08
useState 구현  (0) 2024.02.01
profile

그럴듯한 개발 블로그

@donghyk2

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!