반응형

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

 

반응형
반응형

특정 기간동안 함수가 너무 많이 호출될 경우, 함수 실행을 건너뛸 수 있는 debounce 나 throttle 기법을 고려할 수 있다.

그 중 debounce 는 이벤트를 그룹화하여 많은 이벤트가 발생해도 모두 무시하고, 하나의 이벤트만 실행시키도록 하는 기법이다. lodash 라이브러리에서 지원한다.

 

input 태그에서 사용자가 입력을 수시로 변할 때, 백엔드에 input 데이터를 보내는 예제를 생각해보자.

 

const Input = () => {
  const [value, setValue] = useState();

  const sendRequest = (value) => {
    // 백엔드에 input 데이터를 보냄
  };

  const debouncedSendRequest = debounce(sendRequest, 500);

  const onChange = (e) => {
    const value = e.target.value;
    setValue(value);
    debouncedSendRequest(value);
  }

  return <input onChange={onChange} value={value} />
}

 

위와 같이 코드를 생각해볼 수 있다. onChange 함수에서 value 를 세팅하고 debounced 된 함수를 호출하는 형태이다.

하지만 위 코드는  value 값이 바뀔 때마다 리렌더링이 되면서 아래와 같은 문제점이 생긴다.

 

  • sendRequest, debouncedSendRequest 함수가 계속 파괴되고 생성된다.
  • timer 로 인해 바로 파괴되는 것이 아니라 timer 시간 동안 유지되었다가 참조하는 곳이 없어서 가비지 컬렉션에 의해 정리된다.

 

const Input = () => {
  const [value, setValue] = useState("initial");

  const sendRequest = useCallback((value) => {
    // 백엔드에 input 데이터를 보냄
  }, []);

  const debouncedSendRequest = useMemo(() => {
  	return debounce(sendRequest, 500);
  }, [sendRequest]);

  const onChange = (e) => {
    const value = e.target.value;
    setValue(value);
    debouncedSendRequest(value);
  }

  return <input onChange={onChange} value={value} />
}

 

리렌더링이 되어서 함수가 파괴가 되어 참조를 못 하는 문제를 해결하기 위해 sendRequest 함수 자체에는 useCallback 훅으로 감싸고, debouncedSendRequest 함수는 useMemo 훅으로 감싸서 코드를 수정해보았다. 정상적으로 동작하는 것처럼 보인다.

 

debouncedSendRequest(value) 구문보면 계속해서 value 인자를 주고 있는데 이 부분을 useCallback 의 종속성 인자로 바꾸어 보면, 결국 처음 코드와 같게 된다.

 

리바운싱할 때마다 디바운스 함수를 생성하지 않게 하는 것이 관건인데 보통 useRef 훅으로 함수를 감싸는 것을 추천한다. 아래와 같은 형태가 되는데 만약 useRef 내부 함수에서 state 값을 참조하면 클로저가 되어버리기 때문에 주의해야 한다.

 

const ref = useRef(debounce(() => {
    // value 가 scope 를 벗어난 외부 변수이기 때문에 초기값으로 세팅된다 (클로저)
    console.log(value);
}, 500));

 

value 상태 값을 항상 최신으로 유지하기 위해 함수를 다시 호출하고 ref 에 다시 할당해야 한다.

(즉, 클로저라는 문제 때문에 value 값이 변경될 때마다, ref.current 값을 교체해주어야 된다.)

 

useEffect(() => {
    ref.current = debounce(() => {
    }, 500);
}, [value]);

 

또 처음 코드와 같게 된다. 클로저로 변형된 함수들을 Ref 로 묶고, debouncedCallback 함수만 useMemo  로 결과 값만 재 사용한다면 위 useMemo 와 useCallback 의 이점을 동시에 사용할 수 있게 된다.

 

[최종 코드]

  const [value, setValue] = useState("initial");
  const ref = useRef();

  const onChange = () => {
  };

  useEffect(() => {
    ref.current = onChange;
  }, [onChange]);

  const debouncedCallback = useMemo(() => {
    const func = () => {
      ref.current?.();
    };
    return debounce(func, 1000);
  }, []);

 

[출처]

How to debounce and throttle in React without losing your mind (developerway.com)

반응형
반응형

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 으로 맵핑하여 인자로 전달한다.

반응형
반응형

[문제]

 

 

[원인]

Resolve | 웹팩 (webpack.kr) resolve 부분을 살펴보면 아래와 같은 문구가 보인다.

 

 

node.js 의 핵심모듈들을 Webpack 5 부터 포함하고 있지 않는다. 직접 에러가 나는 모듈을 설치하고 resolve fallback 에 추가해야 한다.

 

참고로, CRA(Create React App) 로 만든 React 의 경우, Webpack 설정파일이 숨겨져 있어 덮어 씌워야 한다. 그럴 바에는 React 와 Webpack, Babel, TypeScript 를 직접 설정하여 구축하는 것이 더 나을 수도 있다. 아래 방법은 CRA 없이 React 구축한 상태에서 해결한 방법이다.

 

[해결방법]

(1) npm install buffer 로 buffer 모듈 설치

(2) webpack.config.js 파일에서 아래 문구 삽입

 

  plugins: [
  ...
    new webpack.ProvidePlugin({
      Buffer: ['buffer', 'Buffer'],
    }),
  ...
  ]

plugins 에 buffer 플러그인 추가

 

  resolve: {
  ...
    fallback: {
      buffer: require.resolve('buffer'),
    },
  ...  
  },

resolve 에 buffer fallback 추가

 

반응형

+ Recent posts