Hyunjung Im
Frontend Developer
2023-01-14
react 훅들과 클로저과 관련있다는 걸 이제야 알았고.. 반성의 의미로 포스트를 작성해본다. 💩
클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경의 조합이다. 간단히 말해 “함수가 선언될 시 그 주변 환경을 기억하는 것” 이다.
var fn;
function foo() {
const a = 2;
function baz() {
console.log(a);
}
fn = baz;
// 전역변수 fn에 foo함수의 스코프를 가지는 baz함수를 주입시켰다.
}
function bar() {
fn();
}
foo();
bar(); // console -> 2
fn안의 함수는 foo()함수의 환경을 기억하고 있어 변수 a의 값이 출력된다.
function CoolModule() {
const something = "cool";
const another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join("!"));
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
const foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1!2!3!
CoolModule()
의 내부 스코프를 렉시컬 스코프(당연히 클로저도 따라온다)로 가진다.CoolModule()
함수내의 변수는 외부에서 접근할 수 없지만 안의 doSomething
, doAnother
함수를 이용해 값을 출력해볼 수 있다.🧐 왜 모듈까지 설명하게 되었을까? 그 이유는 useState의 구현이 모듈로 이루어져있기 때문이다.
여기서는 Deep dive: How do React hooks really work?의 예제를 사용했습니다.
(번역된 글도 있으니 꼭 읽어보시길 추천 [번역] 심층 분석: React Hook은 실제로 어떻게 동작할까?)
실제 구현이 궁금하다면 React의 useState 내부 동작 방식과 클로저글을 추천합니다.
(😇)
const [state, setState] = useState(initialValue);
➢ useState
는 배열을 return
하는 useState
함수의 값을 구조분해 할당을 사용하여 getter인 state
, setter인 setState
를 값을 사용하는 방식이다.
state
값이 바뀌면 리렌더링 된다는 건 알겠는데..🧐 왜 setState
로 값을 바꾸자마자 바꾼 값을 state
로 출력할 수 없을까?
-> 미리 말하자면 setState를 작동시킨 후 state와 setState는 서로 다른 클로저를 참조한다. setState가 참조하고 있는 건 이미 업데이트 된 클로저를 참조하고 state가 참조하고 있는 클로저는 setState를 실행시키기 전의 클로저 즉 렌더링이 일어나고 나서의 클로저를 참조하고 있다. (렌더링 후의 state, setState의 클로저는 같다. setState로 업데이트를 시켰을 때 클로저가 달라지는 것이다.)
아주 기본적인 형태의 useState를 만들어보자.
function useState(initialValue) {
let _val = initialValue; // 지역 변수 _val를 선언하고, initialValue를 할당합니다.
function state() {
// state함수는 내부 함수이고, 클로저입니다.
return _val; // state()는 부모 함수에 정의된 _val을 참조합니다.
}
function setState(newVal) {
// same
_val = newVal; // _val를 변경합니다.
}
return [state, setState]; // 외부에서 사용하기 위해 값은 노출하지 않고 함수를 노출합니다.
}
const [foo, setFoo] = useState(0);
console.log(foo()); // logs 0 - the initialValue we gave
setFoo(1); // sets _val inside useState's scope
console.log(foo()); // logs 1 - new initialValue, despite exact same call
state
값을 가져올 때 함수호출을 이용해서 가져오지 않는다. 그리고 이런 상태라면 setState
되자마자 state
의 값이 바뀌게 될 것 같다.function useState(initialValue) {
let _val = initialValue;
function setState(newVal) {
_val = newVal;
console.log("setState", _val);
}
return [_val, setState];
}
const [foo, setFoo] = useState(0);
console.log(foo); // 0
setFoo(1); // log -> 'setState', 1
console.log(foo); // 0
setFoo(2); // log -> 'setState', 2
console.log(foo); // 0
_val
자체를 return
했기 때문에 여기서 foo의 클로저는 더 이상 업데이트 되지 않고 갇혀있다.setFoo()
를 아무리 많이 호출해도 setFoo
의 클로저와 foo
의 클로저는 다르기 때문에 값은 더 이상 업데이트 되지 않는다.// 01
const MyReact = (function () {
let _val; // 모듈 MyReact 스코프 안에 state를 잡아놓습니다.
return {
render(Component) {
// component 함수가 인자로 들어가는 render 메서드를 정의.
const Comp = Component();
Comp.render();
return Comp;
},
useState(initialValue) {
_val = _val || initialValue; // 매 실행마다 새로 할당됩니다.
function setState(newVal) {
_val = newVal;
}
return [_val, setState];
}
};
})();
모듈 패턴을 이용한 작은 React 복제본.
React
와 마찬가지로 컴포넌트 상태를 추적한다.(이 예제에서는 컴포넌트 _val
상태만 추적함).MyReact
는 함수 구성 요소를 ‘렌더링’하여 올바른 클로저로 매번 내부 _val
의 값을 할당할 수 있다.// 02
function Counter() {
const [count, setCount] = MyReact.useState(0);
return {
click: () => setCount(count + 1),
render: () => console.log("render:", { count })
};
}
let App;
App = MyReact.render(Counter); // render: { count: 0 }
App.click(); // setCount(count + 1)을 실행합니다.
App = MyReact.render(Counter); // render: { count: 1 }
// state가 바뀌면 rerendering이 일어나니 똑같이 rendering을 일으켜 값을 확인합니다.
MyReact
의 render
를 실행시켜 클로저를 업데이트 시켜줌으로써 useState
의 setState
로 인해 바뀐 state
_val
값을 업데이트 할 수 있게되었다.
근데 우리가 쓰는 setState
에서는 안에 콜백함수를 이용할 수 있었다. 그것과 똑같이 만들어보자.
// 01
// 나머지는 위와 같고, 주석이 써져있는 부분만 다름
const MyReact = (function () {
let _val;
return {
render(Component) {
const Comp = Component();
Comp.render();
return Comp;
},
useState(initialValue) {
_val = _val || initialValue;
function setState(newVal) {
_val = typeof newVal === "function" ? newVal(_val) : newVal; // newVal이 함수면 인자 _val을 넣어 실행해주고 값이라면 새로 할당해줍니다.
}
return [_val, setState];
}
};
})();
setState
에 콜백함수를 넣을 수 있게 하기 위해 setState
의 newVal
매개변수의 타입으로 삼항연산자 조건을 걸어주었다._val
을 인자로 함수 실행, 함수가 아니라면 값 할당function Counter() {
const [count, setCount] = MyReact.useState(0);
return {
click: () => {
// test를 위해서 두 개의 setState를 만들었다.
setCount((prev) => {
// test용 console.log
console.log("첫 번째 setCount");
console.log("prev", prev);
console.log("count", count);
return prev + 1;
});
setCount((prev) => {
// test용 console.log
console.log("두 번째 setCount");
console.log("prev", prev);
console.log("count", count);
return prev + 1;
});
},
render: () => console.log("render:", { count })
};
}
let App;
App = MyReact.render(Counter); // render: { count: 0 }
App.click();
// 첫 번째 setCount
// prev 0
// count 0
// 두 번째 setCount
// prev 1
// count 0
App = MyReact.render(Counter); // render: { count: 2 }
App.click();
// 첫 번째 setCount
// prev 2
// count 2
// 두 번째 setCount
// prev 3
// count 2
App = MyReact.render(Counter); // render: { count: 4 }
useState는 클로저를 이용해 만들어졌기 때문에 useState()의 값을 할당받은 후 setState를 한 번이라도 동작시키게 되면 state의 클로저와 setState의 클로저는 같은 스코프를 참고하지 않는다. state의 클로저를 set한 결과물에 맞춰주려면 rendering이 일어나 올바른 클로저로 업데이트 되어야 한다.
출처
읽어볼 글