React Hook 에 대해 공부하다보면, 아래와 같은 문구를 만나게 된다.
막연하게 Hook 의 실행 순서와 선언 범위에 대한 규칙만 서술되어 있고, 왜 하지 말아야 하는지에 대한 내용은 서술되어 있지 않다. Hook 은 클로저와 아주 밀접한 관련이 있는데 클로저의 특징 때문에 생긴 제약사항이다.
외부에서 변수 접근을 하지 못 하게 만들고, 함수 자체를 리턴하게 하고 싶을 때, 아래와 같이 생각해볼 수 있다.
function getAdd() {
let foo = 1
return function () {
foo += 1
return foo
}
}
함수 자체를 리턴하고 함수 내에서 사용하는 변수를 내부에 선언한다. 이렇게 구현하면 getAdd 를 통해 생성된 함수 객체를 호출할 때마다 내부에 선언되어 있는 변수에 계산을 할 수 있게 된다. 클로저다.
이 함수를 모듈 패턴을 통해 아래와 같이 구성할 수도 있다.
const add = (function getAdd() {
let foo = 1;
return function () {
foo += 1;
return foo;
}
})()
사실 함수를 반환하면서 내부에 있는 변수들을 외부에서 제어하고자 한다면 고차함수들 대부분이 클로저일 수 밖에 없다.
React Hook 들도 모두 클로저이다.
const React = (function () {
function useState(initVal) {
let _val = initVal
const state = () => _val
const setState = (newVal) => {
_val = newVal
}
return [state, setState]
}
return { useState }
})()
const [count, setCount] = React.useState(1)
console.log(count()) // 1
setCount(2)
console.log(count()) // 2
useState 를 구현해보면 위와 같다. 초기값을 인자로 받아 내부 변수에 세팅하고, 그 변수를 활용한 함수들을 리턴하는 것을 볼 수 있다. 내부 상태 값을 반환하는 함수와 갱신하는 함수를 배열 형태로 리턴하면 React 에서 흔히 사용하는 useState 와 유사하다.
하지만 hook 을 여러 번 사용한다면 위와 같은 구조는 정상적으로 동작하지 않게 된다. 위 React 모듈에 있는 값은 _val 하나이기 때문이다. 갱신하는 함수를 호출할 때마다 _val 변수가 덮어씌워져 호출이 되지 않는 것이다.
이를 방지하기 위해 배열에 hook 함수들을 배치하고 인덱스를 활용하여 사용해 접근하도록 개선했다.
const React = (function () {
let hooks = []
let idx = 0
function useState(initVal) {
const state = hooks[idx] || initVal
const setState = (newVal) => {
hooks[idx] = newVal
}
idx += 1
return [state, setState]
}
function render(Component) {
const C = Component()
C.render()
console.log(idx) // 2, 4, 6
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.type('pear')
var App = React.render(Component) // { count: 'pear', text: 'apple' }
갱신이 정상적으로 되는 것 같지만, React.render 를 하는 순간 계속해서 idx 를 추가하게 되어서 이상하게 값이 수정되는 것을 볼 수 있다. 이미 증가된 idx 를 통해 갱신을 하는 것이 문제점이 되어 버렸다.
문제점을 해결하기 위해 아래와 같이 수정한다.
render 함수 내부에 idx 값을 초기화하고, 각 모듈 내의 함수에서 사용할 index 를 고정해서 공급해주면 여러 hook 들이 각자 고유한 index 를 가지게 되어 그에 맞는 상태를 변경할 수 있게 된다.
const React = (function () {
let hooks = []
let idx = 0
function useState(initVal) {
const state = hooks[idx] || initVal
const _idx = idx // freeze
const setState = (newVal) => {
hooks[_idx] = newVal
}
idx += 1
return [state, setState]
}
function render(Component) {
idx = 0
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.type('pear')
var App = React.render(Component) // { count: 2, text: 'peer' }
[출처]
본 포스팅은 (2019년 JS 컨퍼런스에서 발표한) React Hooks 와 Closure 의 관계를 잘 설명한 동영상을 재해석하였다.
'언어 > React' 카테고리의 다른 글
[React] 리소스 지연 로딩 (IntersectionObserver API) (0) | 2023.10.15 |
---|---|
[React] debounce 최적화 (0) | 2023.10.15 |
[React] Suspense 와 React-Query 사용 시 주의사항 (0) | 2023.01.01 |
[React] Hook 사용 시 유의사항 (0) | 2022.12.30 |