완성본 코드
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 |