js로 만든 useEffect
저번 포스팅에서, useState를 만들었고, 이번에는 useEffect를 만들어 보았다.
const useEffectStore = [];
let firstRender = false;
function useEffect(callback, dependencies) {
const prevDependencies = useEffectStore[stateStore.stateIndex] || [];
if (dependencies === undefined) {
setTimeout(callback, 0);
return;
}
if (dependencies.length === 0 && !firstRender) {
setTimeout(callback, 0);
firstRender = true;
}
let isChanged = false;
if (dependencies) {
for (let i = 0; i < dependencies.length; i++) {
if (dependencies[i] !== prevDependencies[i]) {
isChanged = true;
break;
}
}
}
if (isChanged) {
setTimeout(callback, 0);
useEffectStore[stateStore.stateIndex] = dependencies;
}
}
useState보다는 비교적 구현이 간단했다.
처음 시작될 때
이전에 저장된 의존성 배열이 없으니까 기본값([])을 가져오고, 만약 useEffect에 의존성 배열이 아예 없으면 매번 실행이 된다.
의존성 배열이 [] 빈 객체라면
최초 렌더링에만 실행되도록 firstRender라는 변수를 사용해서 체크한다. 그리고 이후, 렌더링에서는 실행되지 않도록 한다.
의존성 배열이 있는 경우
이전에 저장된 값과 현재 전달된 의존성 배열을 비교하고, 다른 부분이 있다면 변경된 것으로 생각하고 콜백을 실행한다. 그리고 이후 해당 의존성 배열의 값을 최신화 시킨다.
const [a, setA] = useState("True");
const [b, setB] = useState(0);
useEffect(() => {
console.log("🙌🙌🙌🙌🙌🙌🙌🙌🙌🙌🙌🙌🙌🙌");
}, [a()]);
...
function handleUpdateText() {
setA(a() === "True" ? "false" : "True");
}
function increase() {
setB(b() + 1);
}
...
document.getElementById("app").innerHTML = `
<button id="a"> 의존성에 넣을 예정 ${a()}</button>
<button id="b">의존성에 넣지 않을 예정 ${b()}</button>
`;
document.getElementById("a").addEventListener("click", handleUpdateText);
document.getElementById("b").addEventListener("click", increase);
id=a
에 대해서만, useEffect가 실행이 되고 있다. 즉, 동작도 실제, useEffect처럼 동작한다.
불변, 참조값 처리
사실 제일 중요한 점검이라고 할 수 있는 부분은 참조값에 대한 부분이었다. 의존성 배열에 들어가는 부분은 결국 변경을 감지하는데, 이는 곧 메모리 주소의 변경을 감지하는 역할이다. 원시 값의 경우는 불변이라 setState()
의 내부의 값이 동일하면, 변경되지 않는다. 하지만, 참조객체의 경우 아래 코드처럼 만든다면, 새로운 객체를 생성하는 것(새로운 메모리를 주소를 받음)이 된다.
const [obj, setObj] = useState({ value: 0 });
...
function increaseObject() {
setObj({ value: obj().value});
}
그래서 위와같은 경우는 새로운 객체를 할당하기 때문에 useEffect에서는 메모리 주소가 바뀌었다고 인식해야한다. 그래서 실행이 되어야 한다.
다행히 잘 동작을 한다. 그다음은 속성을 바꾸게 된다면, useEffect가 실행되면 안되는데 잘 동작하는지 체크해 보겠다.
즉, 의존성 배열이 바라보는 객체 주소가 아닌, 속성을 바꾸고, 그 속성은 불변하니 useEffect가 실행되면 안된다.
function App() {
stateStore.resetStateIndex();
console.log("✅✅✅ 랜더링 ✅✅✅");
const [b, setB] = useState(0);
const [obj, setObj] = useState({ value: 0 });
useEffect(() => {
console.log("🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯");
}, [obj()]);
console.log(obj());
function increaseObject() {
setObj((prev) => {
prev.value = prev.value + 1;
return prev;
});
// setObj({ value: obj().value });
setB(b() + 1);
}
document.getElementById("app").innerHTML = `
<button id="b">의존성에 넣지 않을 예정 ${b()}</button>
<button id="c">참조값</button>
`;
document.getElementById("c").addEventListener("click", increaseObject);
}
App();
최초 렌더링을 제외하고, 속성을 바꾸었을때, 참조값이 바뀌지 않았기 때문에 useEffect가 실행이 되지 않는 것을 볼 수 있다. 잘 구현한 것 같다.
사실
setObj((prev) => {prev.value = prev.value + 1; return prev;});
이것이 곧 useRef이다.
useRef까지 만들어보자
const useRefStore = [];
function useRef(initialValue) {
const refIndex = stateStore.stateIndex;
if (useRefStore[refIndex] === undefined) {
useRefStore[refIndex] = { current: initialValue };
}
return useRefStore[refIndex];
}
컴포넌트가 처음 실행되면, useRefstore 배열에 { current:initialValue }
를 저장한다. 이후 호출하게 된다면, useRefStore[refIndex]
를 반환하므로 같은 객체를 유지한다. 따라서 동작함에 있어서도, useEffect의 의존성 배열에 값을 넣어도 리렌더링을 발생시키지 않으며, 이후 useState로 렌더링을 시키면 그때, 해당 렌더링 주기에 useRef의 속성값이 화면에 렌더되는것을 확인할 수 있었다.
function App() {
stateStore.resetStateIndex();
console.log("✅✅✅ 랜더링 ✅✅✅");
const [a, setA] = useState("True");
const [b, setB] = useState(0);
const objRef = useRef({ value: 0 });
useEffect(() => {
console.log("🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯");
}, [objRef.current]);
function handleUpdateText() {
setA(a() === "True" ? "false" : "True");
}
function increase() {
setB(b() + 1);
}
function increaseObject() {
objRef.current.value += 1;
console.log("Updated objRef:", objRef.current);
}
document.getElementById("app").innerHTML = `
<h1>Count: ${objRef.current.value}</h1>
<button id="a"> 의존성에 넣을 예정 ${a()}</button>
<button id="b">의존성에 넣지 않을 예정 ${b()}</button>
<button id="c">참조값</button>
`;
document.getElementById("a").addEventListener("click", handleUpdateText);
document.getElementById("b").addEventListener("click", increase);
document.getElementById("c").addEventListener("click", increaseObject);
}
App();
1. 아래처럼, 현재 useRef의 내부 속성을 10번 버튼 동작이 일어나도 내부 값은 변경되지만 렌더링은 발생하지 않는다.
2. 다만, 이후 useState의 값을 바꿨을때는 렌더링이 발생하고, 그때 ref의 값이 화면에 나오는 것을 확인할 수 있다.
실제로 useRef를 구현하기에는 아직 무리가 있음
const buttonRef = useRef(null);
...
function handleHover() {
console.log(" 버튼 위에 마우스 올림", buttonRef.current);
}
...
buttonRef.current = document.getElementById("myButton");
buttonRef.current.onmouseover = handleHover;
추가로 위와 같이 document.getElementById
를 통해서 ref가 돔에 접근하는 것처럼
만들 수는 있지만 실제로는 가상돔에 접근하는 것이 아니라, 실제 DOM을 직접 조작하는 것에 불과하다. React의 useRef는 가상 DOM과 연동되어, 컴포넌트가 리렌더링될 때도 ref.current가 유지되지만, document.getElementById 방식은 React의 렌더링 사이클과 무관하게 DOM을 직접 탐색하는 것이기 때문에 가상 DOM을 거치지 않는다. 나중에 가상돔 또한 구현하게 된다면, 한 번 연결해보는것도 나쁘지 않을 것 같다.