반응형

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 의 관계를 잘 설명한 동영상을 재해석하였다.

 

반응형
반응형

Rules of Hooks – React (reactjs.org)

 

Rules of Hooks – React

A JavaScript library for building user interfaces

reactjs.org

Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)

 

위 React 공식문서를 살펴보면, 반복문 조건문 안이나 nested function 안에 hook 을 호출하면 안 된다고 명시하고 있다.

무조건 React Function Top level 에서 호출해야 한다. 이 규칙을 정해야 hook 의 호출 시점이 일관됨을 보장할 수 있기 때문이다.

 

따라서 Hook 을

 

- 조건문에서 사용하고 싶다면 결과 값을 useState 와 useEffect 에서 핸들링한다.

- 반복문에서 사용하고 싶다면 Hook 에서 반복을 지원하는지 살펴보거나, 반복할 리스트를 map 으로 맵핑하여 인자로 전달한다.

반응형

+ Recent posts