index를 통해 관리하고 있다. 그래서 조건문이나 loop문 등에 포함되면 순서가 보장되지 않기 떄문에이 작동하는 방식 hook 관련 린트에러가 발생한다.
getting closure on react hooks deep dive: how do react hook really work
두번째 글을 번역한 게 첫번째 글
Hook 바닥부터 순차적 작성해보기
클로저를 활용한 add 함수부터 만들어보자.
function getAdd() {
let _count = 0;
return function() {
const addCount = () => (_count += 1);
// const state = _count;
const getCount = () => _count;
const printCount = () => console.log(_count);
return {
// count,
getCount,
addCount,
printCount,
};
};
}
const addEnv1 = getAdd();
const add1 = addEnv1();
add1.printCount();
add1.addCount();
add1.addCount();
add1.printCount();
console.log(add1.getCount());
주석처리된 const state = _count
는 state를 직접 접근하여 변경하지 못하도록 하기 위해 선언했지만, 변경된 값에 접근을 하지 못하는 문제 발생한다. 참조 값이 아니라서 한번 할당하고 끝나기 때문이다. 함수를 통해 _count에 접근하는 것으로 변경하여 문제 해결하였다.
useState 만들기
// 대충 요렇게 사용된다.
const [count, setCount] = useState(1);
console.log(count); // 1
setCount(2);
console.log(count); // 2
function useState(initVal) {
let _val = initVal;
const setState = newVal => (_val = newVal);
// const state = _val // 할당되고 끝나기 때문에 아래와 같이 함수 호출해야한다.
const state = () => _val;
return {
state,
setState,
};
}
const [count, setCount] = useState(1);
console.log(count()); // 1
setCount(2);
console.log(count()); // 2
컴포넌트에 붙여보기
useState를 사용하여 미니 리액트 만들자. 사용방법은 다음과 같다.
리액트의 특징인 제어의 역전처럼 우리는 컴포넌트만 만들어주고 렌더링은 리액트가 하게 한다.
function Component() {
const [count, setCount] = React.useState(1);
return {
render: () => console.log(count),
click: () => setCount(count + 1),
};
}
var App = React.render(Component);
App.click();
var App = React.render(Component);
일단, React라는 네임스페이스에 useState와 render 함수를 작성하자.
const React = (function() {
function useState(initVal) {
let _val = initVal;
const setState = newVal => (_val = newVal);
// const state = _val // 할당되고 끝나기 때문에 아래와 같이 함수 호출해야한다.
const state = () => _val;
return {
state,
setState,
};
}
function render(component) {
//
const C = Component();
C.render();
return C;
}
})();
function Component() {
const [count, setCount] = React.useState(1);
return {
render: () => console.log(count),
click: () => setCount(count + 1),
};
}
var App = React.render(Component); // 1
App.click(); // 2
var App = React.render(Component); // 1
증가하지 않는 이유는 함수가 실행되고 종료될 떄마다 useState의 EC가 새로 생성되면서 _val가 initVal로 초기화된다. 그래서 _val를 클로저 환경에 둬서 상태를 유지하자.
const React = function() {
let _val;
function useState(initVal) {
// let _val = initVal;
_val = _val || initVal;
}
};
훅 여러개 사용해보기
function Component() {
const [count, setCount] = React.useState(1);
const [text, setText] = React.useState('apple');
return {
render: () => console.log({ count, text }),
click: () => setCount(count + 1),
type: word => setText(word),
};
}
var App = React.render(Component); // {count: 1, text: 'apple'} // 사실 React 함수 클로저 환경에서 _val는 'apple'로 덮여쓰여짐.
App.click(); // 이 때 2로 덮여 쓰여짐
var App = React.render(Component); // {count: 2, text: 2}
App.type('banana'); // banana로 덮여쓰여짐.
var App = React.render(Component); // {count: 'banana', text: 'banana'}
이 문제를 해결하기 위해 각 값별로 배열에 담아서 관리한다. 즉, 훅을 담아 둔 배열과 현재 어떤 훅이 어떤 인덱스를 바라보는지 관리가 필요하다.
const React = (function() {
let hooks = [];
let index = 0;
function useState(initVal) {
const state = hooks[idx] || initVal;
const _index = index; // 이 훅이 사용해야 하는 인덱스를 가둬둔다.
const setState = newVal => {
hooks[_index] = newVal;
};
index++; // 다음 훅은 다른 인덱스를 사용하도록 한다.
return [state, setState];
}
function render(Component) {
idx = 0; // 랜더링 시 훅의 인덱스를 초기화한다.
const C = Component();
C.render();
return C;
}
return { useState, render };
})();
위와 같이 각 useState의 index를 내부 함수에서 사용하여 클로저 환경에서 저장하여 상태 관리할 수 있다. 그리고, 렌더링마다 index를 초기화하여 N개의 HOOK을 0~N까지 순서대로 호출하여 상태를 보장한다.
그래서 React hook의 규칙이 왜 있는지 알 수 있다.
hook rule
top level 에서만 hook 호출하기
이 규칙에 따라 렌더링 될 떄마다 항상 동일한 순서로 hook이 호출되는 것을 보장한다.
조건부로 훅을 호출, 루프안에서 훅이 호출된다면 순서 보장 어려움 -> 상태 유지 관리 보장 어려움
useState와 useEffect가 여러번 호출되도 hook상태를 올바르게 유지할 수 있다.
useEffect 구현하기
function Component() {
const [count, setCount] = React.useState(1);
const [text, setText] = React.useState('apple');
// 랜더링 시 최초에 한 번만 실행된다.
// 배열 안에 관찰하고자 하는 상태를 전달하면 그 상태에 반응하여 콜백이 실행된다.
React.useEffect(() => {
console.log('side effect');
}, []);
// ...
}
useEffect의 특징은 두 번쨰 인자인 dependency array의 값중 하나라도 값이 변했다면 콜백을 실행한다.
const useEffect(cb, deps) {
// let isFirstCall = true;
// hooks[index] = deps;
// if(isFirstCall) {
// cb();
// return;
// }
// const isDifferent = deps.some((dep, i) => Object.is(
// dep, hooks[index][i]
// ));
// if(isDifferent) {
// cb();
// }
let oldDeps = hooks[index];
let hasChanged = true;
if(oldDeps) {
hasChanged = oldDeps.some((dep, i) => !Object.is(oldDep, deps[i]))
}
if(hasChanged) {
cb();
}
hooks[idex] = deps;
index += 1;
}
Summary
hook의 상태관리는 클로저로 되고 있다.
순서를 보장해야하는 hook의 규칙에 대해 이해할 수 있다. 클로저 내부의 상태를 index를 통해 관리하고 있다. 그래서 조건문이나 loop문 등에 포함되면 순서가 보장되지 않기 떄문에 hook 관련 린트에러가 발생한다.
Last updated
Was this helpful?