본문으로 바로가기
728x90

프로젝트를 사용하다 useCallback을 사용해서 리렌더링을 방지하게 만들지, 그냥 함수로 정의하는게 더 메모리에 효율적일지 어렵다.
kentcdodds의 featured article에 useMemo 그리고 useCallback에 관한 좋은 글을 참조하여 정리해보려 합니다.

useCallback 왜 별로일까?

const dispense = candy => {
  setCandies(allCandies => allCandies.filter(c => c !== candy))
}
const dispenseCallback = React.useCallback(dispense, [])

일반적으로 useCallback이 한 번에 감싼 코드를 많이 봤겠지만, 우리에게 아주 익숙한 useCallback 함수입니다. 우선 실행되는 모든 코드는 비용이 발생합니다. 우선 useCallback을 사용하면 코드 한 줄이 더 발생합니다.

이 뿐만 아니라 만약 리렌더링이 발생 한다면 처음 dispense 함수는 가비지 컬렉터에서 수집하여 메모리 공간을 비우고 새롭게 만들지만, dispenseCallback은 가비지 컬렉터에서 수집하지 않고 새 함수가 정의되므로 (useCallback은 '[]'에 정의된 logical expressions 또한 정의해야 하기 때문에) 메모리 관점에서 더 나쁩니다.

그리고 memoization은 이전 값의 카피본을 메모리에 들고 정의된 dependencies를 참조하여 동등성 검사를 진행하는 일도 발생합니다.

 

그렇다면 useMemo는?

const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
const initialCandies = React.useMemo(
  () => ['snickers', 'skittles', 'twix', 'milky way'],
  [],
)

만약 매 렌더마다 배열을 초기화하고 싶지 않다면 다음과 같이 useMemo를 사용할 수 있습니다. 하지만 이런 경우에 얻는 이점은 너무나도 미미하여 코드를 더 복잡하게 만들고 property assigning 등을 수행함에 오히려 더 나쁠 수도 있습니다.

이런 경우 우리는 useState를 이용해왔습니다.

const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
function CandyDispenser() {
  const [candies, setCandies] = React.useState(initialCandies)

props의 변경이 잦아 useMemo를 사용해야 하는 경우도 있지만 요점은 어느쪽이든 상관 없을 정도의 효과가 미미하다는 것입니다.

 

그럼 정확히 언제 쓰라는거죠?

1. 참조 비교

function Foo({bar, baz}) {
  const options = {bar, baz}
  React.useEffect(() => {
    buzz(options)
  }, [options]) // we want this to re-run if bar or baz change
  return <div>foobar</div>
}

function Blub() {
  return <Foo bar="bar value" baz={3} />
}

다음의 경우에 매 렌더링마다 options는 다시 정의됩니다. 이를 해결하기 위해 여러가지 옵션들이 있는데 첫 번째 방법은 options를 useEffect 안으로 포함시키는 겁니다. 그리고 dependencies를 bar와 baz로 정의하는 방법입니다.

// option 1
function Foo({bar, baz}) {
  React.useEffect(() => {
    const options = {bar, baz}
    buzz(options)
  }, [bar, baz]) // we want this to re-run if bar or baz change
  return <div>foobar</div>
}

하지만 위 코드는 bar와 baz가 objects/arrays/functions 와 같은 경우라면 메모리 주소가 달라 문제가 됩니다. (아래 참조)

{} === {} // false
[] === [] // false
(() => {}) === (() => {}) // false

그래서 이런 경우에는 렌더가 계속해서 발생할 수 있기에 useCallback과 useMemo를 사용해야 합니다.

function Foo({bar, baz}) {
  React.useEffect(() => {
    const options = {bar, baz}
    buzz(options)
  }, [bar, baz])
  return <div>foobar</div>
}

function Blub() {
  const bar = React.useCallback(() => {}, [])
  const baz = React.useMemo(() => [1, 2, 3], [])
  return <Foo bar={bar} baz={baz} />
}

 

React Memo

function CountButton({onClick, count}) {
  return <button onClick={onClick}>{count}</button>
}

function DualCounter() {
  const [count1, setCount1] = React.useState(0)
  const increment1 = () => setCount1(c => c + 1)

  const [count2, setCount2] = React.useState(0)
  const increment2 = () => setCount2(c => c + 1)

  return (
    <>
      <CountButton count={count1} onClick={increment1} />
      <CountButton count={count2} onClick={increment2} />
    </>
  )
}

아주 유명한 React Memo 예시입니다. 어떤 버튼을 클릭하던 state가 변경되어 CountButton이 리렌더링 될겁니다. 그럼 React.memo(function CountButton({onClick, count})로 하면 방지될까요? 아닙니다.

클릭해서 count1 또는 count2가 변경되면 여전히 DualCounter는 다시 렌더링되고 memo가 감싼 onClick인 increment1와 increment2가 다시 정의되기 때문에 const increment1 = React.useCallback(()=> setCount1(c => c + 1), [])과 같이 increment1과 increment2를 useCallback으로 감싸야만 참조 비교 시 props가 같다고 판단하여 CountButton이 리렌더되지 않습니다. (물론 increment1을 클릭해서 count1이 증가한다면 해당 CountButton은 당연히 리렌더 돼야합니다.)

 

2. 값 비싼 계산 

// 예시1
const a = {b: props.b}
const a = React.useMemo(() => ({b: props.b}), [props.b])

// 예시2
function RenderPrimes({iterations, multiplier}) {
  const primes = calculatePrimes(iterations, multiplier)
  return <div>Primes! {primes}</div>
}

위와 같은 예시1은 사실 useMemo가 전혀 필요하지 않는 경우이다. 반면 예시2는 실제 얼마나 많은 발생이 일어날지 모르겠지만 iterations와 multiplier가 변경되지 않음에도 계속해서 계산할 이유가 없기에 useMemo가 유용하게 사용될 수 있습니다.

결론

사실 memoization기법은 performance optimizing을 위해 사용되는 예시라 필요한지 필요하지 않은지 예상하기 힘든 경우에는 Profiling을 통해 memo의 이점을 측정해보는게 확실할 수 있습니다. 너무 잦은 useCallback과 useMemo를 사용하여 동료들에게 복잡함을 제공하진 않는지, 의존성 배열에 잘못된 값을 넣지 않았는지, 가비지 컬렉터가 처리한 메모된 값을 활용하거나 내장 hook을 메모하는 등 잠재적으로 퍼포먼스에 악영향을 끼치는지 생각해야합니다.

아래 블로그에서 너무 깔끔하게 결론을 내주셔서 가져와봤습니다. (원문: https://mooneedev.netlify.app/Frontend/%EC%96%B8%EC%A0%9C%EC%8D%A8%EC%95%BC%ED%95%A0%EA%B9%8C%20useCallback/)

useCallback 혹은 useMemo를 쓰자!🥳

  • 자식 컴포넌트에 함수를 props로 넘겨주는데 , 해당 자식 컴포넌트에 넘겨주는 함수 때문에 불필요한 리렌더링이 일어난다고 판단될 경우
  • 함수 자체가 매우 복잡하거나, 비용이 많이 드는 경우

useCallback 굳이 써야하니 ? 🙄

  • 일반 함수의 경우, toggle이나 incrememt 같은 단순한 연산들은 굳이 useCallback을 쓸 필요가 있나 재고해 볼 필요가 있다. 이 경우 그냥 일반 함수로 표기해도 성능상 별 문제가 없다.
  • 오히려 함수가 복잡하지 않으면 useCallback을 쓰지 않는 편이 성능상 좋다. 계속해서 함수가 메모리에 남기 때문이다.

useCallback 절대 쓰지마! 🤮

  • setState나 dispatch를 단순 호출할 경우. 불필요한 연산을 더하는 셈이므로 절대 쓰지마!

 

https://kentcdodds.com/blog/usememo-and-usecallback

 

When to useMemo and useCallback

Stay up to date Subscribe to the newsletter to stay up to date with articles, courses and much more! Learn more Stay up to date Subscribe to the newsletter to stay up to date with articles, courses and much more! Learn more All rights reserved © Kent C. D

kentcdodds.com

 

software engineeringfrontend카테고리의 다른글

Vite 5.0  (0) 2023.11.17
JSX is deprecated  (0) 2023.11.17
next.js 13.4 / 13.5.6 / 14.0.2 로컬 구동 시간 비교  (0) 2023.11.14
UX 패턴 분석 - 로딩(번역)  (1) 2023.11.12
처음 써보는 tailwindcss  (1) 2023.10.29