이번에는 useEffect를 추가해 보겠습니다.
기존에 useState를 구현했던 MyReact에 useEffect 메소드를 추가하고,
Counter 함수에는 useEffect를 실험하기 위한 noop이라는 메소드를 추가했습니다.
const MyReact = (function () {
let _val, _deps; // _deps는 의존성 배열을 저장하고 있습니다.
return {
render(Component) {
const C = new Component();
C.render();
return C;
},
useState(initVal) {
_val = _val || initVal;
function setState(newVal) {
_val = newVal;
}
return [_val, setState];
},
useEffect(callback, depArray) {
const oldDeps = deps;
let hasChanged = true;
if (oldDeps) {
hasChanged = depArray.some((dep, i) => !Object.is(dep, _deps[i]));
}
if (hasChanged) cb();
_deps = depArray;
},
};
})();
function Counter() {
const [count, setCount] = MyReact.useState(0);
MyReact.useEffect(() => {
console.log('effect: ', count);
}, [count]);
return {
render() {
console.log('render: ', count);
},
click() {
setCount(count + 1);
},
noop() {
setCount(count);
},
};
}
let App;
App = MyReact.render(Counter); // effect: 0 render: 0
App.click();
// 새로운 count 값을 받기 위해 컴포넌트 함수를 다시 호출합니다.
App = MyReact.render(Counter); // effect: 1 render: 1
App.noop();
App = MyReact.render(Counter); // render: 1
useEffect는 callback 함수와, depArray라는 의존성 배열을 매개변수로 갖고 있습니다.
Counter 컴포넌트를 새로 렌더링할 때마다 useEffect가 호출됩니다.
useEffect는 내부에서 전달된 의존성 배열이 있는지 없는지 확인합니다.
만약 없다면, callback함수가 무조건 호출됩니다.
전달된 의존성 배열이 있다면 기존에 갖고 있던 의존성 배열(_deps)와 비교합니다.
의존성 배열의 값에 변화가 있다면 callback함수가 호출됩니다.
마지막으로, _deps에 변화된 의존성 배열을 반영합니다.
MyReact 모듈은 한가지 치명적인 문제가 있습니다. 싱글톤 형태이기 때문에, 여러가지 훅을 사용할 수 없다는 것입니다.
useState는 _val을, 그리고 useEffect는 deps라는 각각 하나의 변수만을 참조하고 있습니다.
이렇게 되면 여러개의 useState나 useEffect를 사용할 수 없습니다.
이를 개선한 코드는 다음과 같습니다.
훅이 참조하는 값들을 배열에 담겠습니다.
const MyReact = (function () {
let hooks = []; // hook이 참조하는 값들을 저장하는 배열
let idx = 0; // 위 배열을 순회하기 위한 인덱스
return {
render(Component) {
const C = Component();
C.render();
idx = 0; // 다음 렌더링을 위한 리셋, 이 부분을 잘 기억해야 합니다!
return C;
},
useState(initVal) {
hooks[idx] = hooks[idx] || initVal;
const _idx = idx;
// 약간 의문이 드는 코드
// 아래에 설명이 이어집니다!
function setState(newValue) {
hooks[_idx] = newValue;
}
return [hooks[idx++], setState];
},
useEffect(callback, depArray) {
const oldDeps = hooks[idx];
let hasChanged = true;
if (oldDeps) {
hasChanged = depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
}
if (hasChanged) cb();
hooks[idx] = depArray;
idx++;
},
};
})();
function Counter() {
const [count, setCount] = MyReact.useState(0);
const [message, setMessage] = MyReact.useState('hi');
MyReact.useEffect(() => {
console.log('effect: ', count, message);
}, [count, message]);
return {
render() {
console.log('render: ', count, message);
},
newMessage(text) {
setMessage(text);
},
click() {
setCount(count + 1);
},
noop() {
setCount(count);
},
};
}
let App;
App = MyReact.render(Counter);
// effect: 0 hi
// render: 0 hi
App.click();
App = MyReact.render(Counter);
// effect: 1 hi
// render: 1 hi
App.newMessage('bye');
App = MyReact.render(Counter);
// effect: 1 bye
// render: 1 bye
작성하다보면 useState에서 이상하다고 느낀 부분이 있을 수 있습니다.
useState(initVal) {
hooks[idx] = hooks[idx] || initVal;
const _idx = idx;
function setState(newVal) {
hooks[_idx] = newVal;
}
return [hooks[idx++], setState];
}
굳이 setStateHookIndex에 currentHook을 할당하여 새로운 변수를 만들어 쓰고 있습니다.
그냥 currentHook을 쓰면 되지 않을까요? 아래처럼요
useState(initVal) {
hooks[idx] = hooks[idx] || initialValue;
function setState(newVal) {
hooks[idx] = newVal;
}
return [hooks[idx++], setState];
}
setState()가 호출될 때는 컴포넌트가 렌더링이 끝난 후 입니다.
따라서 idx이 다시 0으로 리셋이 되어버린 것이죠.
때문에 변수를 하나 더 선언하고(_idx) 값을 묶어놓아야 합니다.
Top-Level에 작성한다는 것은 함수형 컴포넌트의 맨 윗줄에 작성하라는 뜻이 아닙니다!
조건문, 반복문 등에 중첩되지 않게 작성하라는 뜻입니다.
만약 조건문안에 훅을 작성하여, 훅이 호출될 때가 있고 호출되지 않을 때도 있다면, 훅이 참조하는 배열의 인덱스에 혼란이 올 수 밖에 없겠죠.