렉시컬 스코프, 클로저에 대한 공부를 하고
hook의 동작 원리에 대해 궁금해져 공부해 본 것에 대한 기록이다.
개요
지난 JS 기록에서 렉시컬 스코프와 클로저에 대해 다뤘다.
위의 기록에서 나아가 React Hook의 동작 원리를 이해해보려고 한다.
먼저 useState, useEffect 순서로 원리를 이해하고,
이를 바탕으로 다른 Hook들이 어떻게 작동하는지에 대한 원리를 이해할 것이다.
useState
useState는 리액트의 대표적인 가장 중요한 훅이라고 할 수 있다.
상태값과 상태를 수정할 수 있는 함수를 배열로 반환한다.
실제 useState와 유사한 기능을 하는 useState함수를 만들어보자.
function useState(initialValue) {
var _val = initialValue;
function setState(newValue) {
_val = newValue;
}
return [_val, setState];
}
var [v, setV] = useState(0);
console.log("old", v);
setV(1);
console.log("new", v);
useState 함수는 initialValue를 초기값으로 갖는 변수와
해당 변수를 수정하는 setState를 반환한다.
기존 useState와 유사한 모습이다.
하지만 콘솔에는 아래와 같이 출력된다.
위에서 v 변수는 초기 useState 호출에서 참조한 값이다.
따라서 setState에 의해 값이 변한다고 해도 참조한 값이 변하지 않는다.
이를 해결하기 위해서는 다른 클로저 안으로 클로저를 이동시켜야 한다.
모듈 패턴을 이용하여 useState를 가지는 MyReact를 만들어보자.
const MyReact = (function () {
let _val;
return {
render(Component) {
const Comp = Component();
Comp.render();
return Comp;
},
useState(initialValue) {
_val = _val || initialValue;
function setState(newValue) {
_val = newValue;
}
return [_val, setState];
}
};
})();
MyReact는 변수 _val을 가지며
render와 useState 메서드를 반환하고, useState는 다시 내부에서 _val을
MyReact의 _val이나 전달받은 initialValue로 설정한다.
덕분에 외부에서 _val을 호출하게 되면
그때마다 _val은 MyReact의 _val을 참조할 것이다.
그렇게 되면 setState의 변경값을 반영할 수 있다. 직접 보자.
function Counter() {
const [count, setCount] = MyReact.useState(0);
return {
click: () => setCount(count + 1),
render: () => console.log(count)
};
}
let App;
App = MyReact.render(Counter);
App.click();
App = MyReact.render(Counter);
생성한 MyReact 모듈의 useState를 통해 Counter 함수에서 setState 실행과 state 출력 함수를 반환한다.
위처럼 이제 변한 값을 참조하는 것을 볼 수 있다.
하지만 위의 useState는 싱글톤 형태로 하나의 상태만을 반영한다.
즉, 두 개 이상의 상태를 관리할 수 없다.
function Counter() {
const [count, setCount] = MyReact.useState(0);
const [testCount, setTestCount] = MyReact.useState(0);
return {
click: () => {
setCount(count + 1);
setTestCount(testCount + 2);
},
render: () => {
console.log("count", count);
console.log("testCount", testCount);
}
};
}
let App;
App = MyReact.render(Counter);
App.click();
App = MyReact.render(Counter);
코드를 위와 같이 수정했다.
testCount가 count와 별개로 증가할 것 같지만 그렇지 않다.
위와 같이 count와 testCount는 같은 값을 갖는다.
같은 변수를 참조하니 당연한 결과다.
이 버그를 해결하기 위해서는 상태값을 변수가 아닌 배열로 관리해야 한다.
const MyReact = (function () {
let hooks = [];
let currentHook = 0;
return {
render(Component) {
const Comp = Component();
Comp.render();
currentHook = 0;
return Comp;
},
useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue;
const setStateHookIndex = currentHook;
const setState = (newState) => (hooks[setStateHookIndex] = newState);
return [hooks[currentHook++], setState];
}
};
})();
MyReact를 위와 같이 수정해준다.
결국 hook들은 배열에 담긴다. 서로 다른 hook이라는 것을 기억하기 위해서 말이다.
그리고 렌더링 될 때마다 currentHook을 0으로 만들어
본인의 값 즉, 자리를 찾아가는 형식이다.
이렇게 hook을 배열로 구분해 주면
여러 개의 상태를 관리할 수 있다.
위의 코드에서 setStateHookIndex가 의미하는 것은 무엇일까?
currentHook을 그대로 사용하게 되면 선언된 당시의 currentHook인 0의 값을 참조하게 된다.
setStateHookIndex를 없애게 되면 setCount, setTestCount 모두 인덱스가 0인 count의 값을 변경시킨다.
따라서 setTestCount에 의해 계속해서 0의 값을 갖는 testCount에서 2를 증가시킨 2가 계속해서 출력된다.
useEffect
이번엔 useEffect를 구현해 보자.
useEffect는 deps 배열을 받아 배열 내부 상태값이 변할 때마다 콜백 함수를 실행시킨다.
즉 배열 내부의 상태값을 hook 배열에 저장해야 한다.
useEffect(callback, depArray) {
const hasNoDeps = !depArray
const deps = hooks[currentHook] // type: array | undefined
const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
if (hasNoDeps || hasChangedDeps) {
callback()
hooks[currentHook] = depArray
}
currentHook++
},
만약 deps가 없을 경우에는 매번 실행하는 것이므로 위와 같이 작성하면 된다.
이제 Hook은 상태 관련 동작 및 부수 작용을 캡슐화하는 방법이라는 말을 이해할 수 있게 됐다.
Hook은 배열로 상태를 관리하며 상태에 관한 부수 작용을 해준다.
참고
마무리
아직 클로저, 렉시컬 스코프에 대한 이해가 완전하지 않아
100프로 이해했다고는 할 수 없을 것 같다.
계속 복습하고 훈련하며 완전히 이해할 수 있을 때까지 공부해야겠다.
잘못된 정보에 대한 피드백은 환영입니다.
감사합니다.
'Frontend > React' 카테고리의 다른 글
[React] fetch web APIs vs Axios (0) | 2023.02.06 |
---|---|
[React] Formik 라이브러리(Field,Formk, 버튼 선택) (0) | 2023.01.23 |
[ReactJS] React 스터디(1)(node, VirtualDOM, CRA, npm&yarn, 컴포넌트) (0) | 2023.01.09 |
[React] 폰트 깜빡임 현상 해결(Global Style, CSS) (0) | 2022.12.29 |
[React] Recoil(Atom, Selectors, Atom Effects) (0) | 2022.12.18 |