⚛ 메모이제이션 hooks의 오용

Reactstate라는 상태값이 변경되면 rerendering이 일어난다.

아마 대부분의 최신 프레임워크들은 비슷할 것이다.

이러한 특징이 장점도 있지만, 불필요하게 렌더링이 되거나 동일한 변수 혹은 메소드를 반복해서 생성하는 경우가 있다.

불필요한 재생산이 일어나는것을 예방하기 위해 React에서는 메모이제이션 hook들을 제공하고 있다.

useMemo, useCallback, memo

캐싱이라는 장점만을 생각하고 이런 hook들을 계속해서 사용해왔는데, 그 사용이 잘못된 사용임을 알게되었고, 확실하게 사용하기 위해서 정리해보려고 한다.

useMemo

변수나 상수를 기억한다. 계산된 속성이라고 보아도 될까? useMemo를 사용한다면 아래와 같이 표현될 수 있다.

const fruits = useMemo(() => ['apple', 'banana', 'peach'], [])

사실 위의 사용은 매우 부적절한 사용이다.

첫번째 인자로 콜백함수를 사용하여 반환되는 값, 두번째 인자로는 의존하는 배열을 설정한다.

useCallback

useMemo가 변수나 상수에 대한 메모이제이션을 하기 위한 hook이라면 useCallback은 메소드를 위한 hook이다.

사실 useMemo만으로도 메소드를 메모이제이션 할 수 있다.

const func = useCallback(
  val => {
    // doSomething...
    doSomething()
  },
  [doSomething]
)

첫번째 인자로 사용되는 callback함수로 변수를 받을 수 있으며, useMemo와 동일하게 의존배열이 두번째 인자로 전달된다.

또 하나의 비용

두가지 모두 두번째 인자 내의 속성이 변화가 되면 새롭게 생성하는 방법이다.

비어있는 배열이 전달된다면, 처음에만 생성되고 이후에는 저장되어있는 값을 사용한다.

조금 극단적으로 표현이 되긴 했지만, 위처럼 아무런 계산식이 존재하지 않는 콜백함수를 통해 배열을 반환하는것이 굳이 필요할까?

useMemo, useCallback을 사용하는것 자체도 어찌보면 하나의 비용이소모되는것이다.

위처럼 단순한 변수나 상수를 선언하는데 이러한 hook을 사용하는것은 오히려 역효과만 발생하는 오용이라고 볼 수 있을것 같다.

실제로 위의 글을 따르면, useMemo로 성능개선을 경험할 수 있는 경우는, 계산되는 로직의 복잡성이 100 미만이면, 오히려 초기 useMemo를 사용하는 비용이 더 크게 작용되어 역효과만 난다고 한다.

위 글의 복잡도는 render를 하기 전, 반복문의 횟수를 기준으로 측정한것으로 보이며, 각각의 횟수에 따라 useMemo를 사용했을 때, 사용하지 않았을 때의 render완료 시간을 비교한것 같다.

useCallback의 경우에도, render가 되어 계속해서 생성되는 메소드가 하위 컴포넌트의 rerender에 영향을 주는것이 아니라면, 사용할 필요가 없다.

const addOne = (num: number) => num + 1

const callbackSomething = useCallback(() => {
  // do Something...
  addOne(1)
}, [])

변화가 있는 상태값과 관련이 없는 부수적인 역할을 하는 함수들은 절대 변할일이 없기 때문에, 처음 생성될 때 참조하는것을 그대로 사용할 수 있어서, 의존성배열에 추가해줄 필요가 없다.

위에 설명된 상황들의 변수, 상수, 함수들은 useMemo, useCallback을 사용하기보다는 별도의 constants 디렉토리나, 외부에서 선언하여 참조하는것처럼 다른 방법을 찾는것이 좋아보인다.

const arr = [1,2,3]
const addOne = (num : number) => num+1;

function Component(){
  const callback = useCallback(() => {
    const res  = addOne(arr[0]);
    // do something...
  }, [])
  return (
    <Child callback={callback}>
  )
}

적합한 사용

useMemo, useCallback을 통해 불필요한 렌더링을 줄여줄 수 있을 때 사용하는것이 좋다.

React에서는 하위로 속성이 전달이 되면서, 전달받은 속성이 변화가 있을 때 새롭게 렌더링이 된다.

아마 Javascript를 사용해왔다면 알 수 있는 특징이 있다. 참조

true === true // true
false === false // true
1 === 1 // true
'a' === 'a' // true
{} === {} // false
[] === [] // false
() => {} === () => {} // false

문자열, 숫자열과 같은 자료형들은 보여지는대로 같다고 인식되지만, 배열, 객체, 함수와 같은 자료형들은 보여지는 모습이 같더라도 다르다고 인식된다.

이들은 참조의 속성을 갖고있기 때문이다.

const a = { id: 'a' }
const b = { id: 'b' }
const c = a
a === c // true
a === b // false

이러한 특징들을 생각하고 사용하면 간단하다.

function Parent() {
  const method = () => {
    // doSomething
  }
  return <Children method={method} />
}

React.memo(function Children({ arr, method }) {
  // render
})

Parent컴포넌트에서는 Children컴포넌트에게 method이라는 메소드을 속성으로 전달한다.

Children컴포넌트에서는 무분별하게 렌더링되는것을 막기 위해 memo를 사용하여 method이 변경될 때만 렌더링이 되도록 설정을 해주었다.

만약, Parent컴포넌트 또한 rerender가 일어났을 때 동일한 구조의 메소드 전달하게 되는데, 이러면 Childrenrerender가 일어나지 않을까?

그렇지 않다. 값은 동일하지만 서로 참조하는 배열이 다르기 때문에 rerender가 발생한다.

function Parent() {
  const method = useCallback(() => {
    // doSomething
  }, [])
  return <Children method={method} />
}

React.memo(function Children({ arr, method }) {
  // render
})

이러한 상황일 때, 전달이 되는 속성들이 생성이 되고, 캐싱이 되도록 한다면 Parentrerender되더라도 Children에 전달되는 속성은 동일하게 참조되는 값들이 전달되기 때문에 rerender를 막을 수 있다.

만약 재생산될 필요가 있다면, 의존성 배열을 수정해주도록 하자

useMemo의 경우 많은 비용이 소모되는 계산을 통해 생성되는 변수나 상수일 때, useCallback의 경우, 자신이 의존하고있는 상태값 외의 변경이 잦은 컴포넌트에서 하위로 메소드를 전달할 때 좋다.

참조


@SangMin
👆 H'e'story

🚀GitHub