클로저를 공부하면서 정보은닉, 커링을 이용한 함수 지연 실행 등의 활용법이 있는 것을 알고 있었지만, 실 사례에서 redux의 미들웨어에 사용되는 것 외에는 와닿는 부분이 없었다. 그러던 중 React Hook이 클로저를 이용해 구현되었다는 것을 보고, 공부해보려 한다.
클로저가 무엇인지 리마인드 해보자.
클로저는 반환된 내부함수가 자신의 렉시컬 스코프를 기억하여 외부에서 호출되어도 그 스코프에 접근할 수 있는 것을 말한다.
const outerFunc = () => {
let x = 10; // 자유 변수
// 클로저
const innerFunc = (y) => {
x = x + y;
console.log(x);
}
return innerFunc;
}
const addFunc = outerFunc();
addFunc(5); // 15
addFunc(10); // 25
함수형 컴포넌트는 렌더링이 필요할 때 마다 함수를 다시 호출한다.
따라서, 상태관리를 위해 함수가 다시 호출되더라도 이전의 상태를 기억하고 있어야 하는데, 이 때 클로저가 사용된다.
다른 분이 작성한 useState 구현 코드를 참조하여 하나씩 이해해보고자 한다.
const React = (function () {
// Hook이 여러개 일 경우, 각 상태를 관리하기 위해 배열을 만든다.
// (Hook이 조건문 내에서 사용되면 안되는 이유)
let states = [];
// 해당 Hook의 State를 찾아가기 위한 인덱스
let idx = 0;
// React의 호출이 끝나더라도, 내부함수인 useState에서
// states를 참조하고 있기 때문에 외부에서 접근할 수 있다 (클로저)
function useState(initialVal) {
const state = states[idx] || initialVal;
// setState가 호출되는 시점에 증가된 인덱스를 사용하지 않도록 방지하기 위해
// setState가 참조하는 인덱스의 스코프를 바꿈.
const _idx = idx;
const setState = (newVal) => {
states[_idx] = newVal;
};
// useState가 호출될 때마다 인덱스를 증가시켜 해당하는 state를
// states 배열에서 꺼내올 수 있도록 한다.
idx++;
return [state, setState];
}
function render(Component) {
// render가 호출되면 useState를 호출하기 때문에
// 인덱스가 계속 증가하는걸 방지하기 위해 0으로 초기화 한다.
idx = 0;
// render가 호출되면 Component()를 호출한다.
// (렌더링이 필요할 때 마다 함수를 다시 호출한다)
const C = Component();
C.render();
return C;
}
return { useState, render };
})();
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' }
App.click();
var App = React.render(Component); // { count: 2, text: 'apple' }
App.click();
var App = React.render(Component); // { count: 3, text: 'apple' }
App.type("orange");
var App = React.render(Component); // { count: 3, text: 'orange' }
App.type("peach");
var App = React.render(Component); // { count: 3, text: 'peach' }
위 코드에서 Hook에 대한 state들을 배열로 관리하는 것을 볼 수 있는데, 이게 Hook이 조건문 내에서 사용되면 안되는 이유이다.
useState가 호출될 때 마다, 배열에 state가 순서대로 저장되기 때문에, 조건문에 따라 Hook의 실행 순서가 바뀐다면, 다른 인덱스의 state를 참조하는 상황이 발생한다.