useCallback과 useMemo 사용하기

January·2022년 10월 31일
1

Frontend

목록 보기
23/31
post-thumbnail

렌더링 최적화하자

우선 함수형 컴포넌트는 Component가 렌더링 될 때마다 변수가 초기화되고 함수는 반복적으로 호출된다.

  1. 렌더링
  2. Component 함수 호출
  3. 모든 내부 변수 초기화

그리고 부모에게 받은 props 또는 state가 변경될 시 리렌더링이 발생한다. props와 state가 많고 하나의 값만 변경이 됐을 때에도 컴포넌트 전체가 리렌더링 된다면 효율적이지 못할거다. 이를 해결하기 위한 memoization되는 useCallback과 useMemo 그리고 memo를 알아보자

When does React re-render?

Memoization

리액트 생태계에서 렌더링 최적화를 위해 사용되는 hook api를 알아보기 앞서 useCallback과 useMemo는 메모이제이션된 콜백을 반환하고 값을 반환하기 때문에 메모이제이션(memoization)이 무엇인지 알아본다.

메모이제이션(memoization)은 기존에 수행한 연산의 결괏값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법이다. 중복 연산을 피할 수 있어서 메모리를 조금 더 쓰더라도 어플리케이션의 성능을 최적화할 수 있다.

계산을 메모이제이션 하는 법

메모이제이션은 두가지 이유 때문에 필요하다.

  1. 비싼 연산을 반복하는 것을 피하여 성능을 향상시킨다.
  2. 안정된 값을 제공

안정된 값을 제공한다는 것은 useEffectdeps에 변경이 있을 때마다 실행된다. 컴포넌트 내에서 매번 새롭게 렌더링 될 때마다 계속해서 변경이 이뤄진다. 따라서 값을 안정시키기 위해 메모이제이션을 사용해야 한다.

메모이제이션(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다.
(위키백과)

useCallback

메모이제이션된 함수를 반환한다. useMemo는 함수를 실행해서 값을 반환하는 것과는 다르다. useCallback은 함수 컴포넌트에서 불필요하게 함수를 업데이트하는 것을 방지해준다.

// 예시1
const App = () => {
  const [a, setA] =  useState(0);
  const [b, setB] =  useState(0);
  
  const useCallbackReturn = useCallback(() => {console.log(b)}, [a]);
  
  useCallbackReturn();
  
  return(
  <>
    <button onClick={() => setA((prev) => (prev + 1))}>콘솔</button>
    <button onClick={() => setB((prev) => (prev + 1))}>값변경</button>
  </>
  )
}

예시를 보면 값변경 버튼을 눌러도 콘솔창에 변경된 값이 출력되지 않고 이전 값이 출력된다. 메모이제이션된 함수는 리랜더링 될 때 마다 이전에 저장된 함수로 동작되고 있기 때문이다. 콘솔 버튼을 누르면 함수가 업데이트 되고 값변경을 누른 횟수만큼의 b 값을 콘솔창에서 볼 수 있다.

함수 재사용은 나중에 props로 값을 내려줄 때 Virtual DOM에 새로 랜더링하는 것 조차 하지 않고 컴포넌트의 결과물을 재사용하는 최적화 작업에 필수이다.

함수 안에 사용하는 상태 또는 props가 있다면 꼭 deps배열안에 포함시켜야한다. 그렇지 않다면 함수 내에 값이 가장 최신 값을 참조할 것이라고 보장할 수 없다.

// 예시2
const useCallbackReturn = useCallback(() => {}, [a]);

useCallback은 새로운 함수를 반환한다. a===1a===2일 때에 반환되는 함수는 다른 함수이다. 새로운 무기명 함수를 반환했기 때문인데 값이 같을 뿐 다른 메모리이다.

useMemo

메모이제이션된 값을 반환한다. 메모이제이션되는 값의 재계산 로직이 복잡한 계산일 경우에 useMemo를 사용하는 것이 추천되고 이런 경우에는 성능상 큰 이점으로 적용된다.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

첫번째 파라미터에는 어떻게 연산할지 정의하는 함수를 넣어주고 deps 값이 바뀐다면 정의한 함수를 호출해서 값을 연산해준다.

  1. 렌더링
  2. Componenet 함수 호출
  3. Memorize된 함수 재사용

처음에 계산된 결과 값을 메모리에 저장해서 컴포넌트가 반복적으로 렌더링이 되어도 계속 함수를 다시 호출하지 않는다. 이미 계산된 결과 값을 메모리에서 꺼내와서 재사용을 한다.

값을 재활용하기 위해 메모리를 소비해서 저장을 해놓기 때문에 꼭 필요할 때만 사용해야한다. 불필요한 값을 모두 메모이제이션하면 성능이 안좋아질 수 있다.

React.memo

React.memo는 고차 컴포넌트이다. 컴포넌트가 동일한 props로 동일한 결과를 랜더링해낸다면 React.memo를 호출하고 결과를 메모이징(Memoizing)하도록 래핑하여 경우에 따라 성능 향상을 누릴 수 있다. 즉, React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용한다.

props가 갖는 복잡한 객체에 대해서 얕은 비교만 수행하는 것이 기본 동작이다. React.memo 래핑될때 React는 컴퍼넌트를 렌더링하고 결과를 메모이징(Memoizing)한다. 그리고 다음 렌더링이 일어날 때 props가 같다면, React는 메모이징(Memoizing)된 내용을 재사용한다. 이는 오직 성능 최적화를 위해서 사용된다. 렌더링을 방지하기 위한 사용은 하지 말아야한다. 버그를 만들 수 있다.

결론은 메모이징 한 결과를 재사용 함으로써, React에서 리렌더링을 할 때 가상 DOM에서 달라진 부분을 확인하지 않아 성능상의 이점을 누릴 수 있다.

React.memo는 props 변화에만 영향을 준다. React.memo로 감싸진 함수 컴포넌트 구현에 useState, useReducer, useContext 훅을 사용한다면 state나 context가 변할 때 리렌더링된다.

모든 컴포넌트에 쓰면 이득? x

얕은 비교가 마구잡이로 일어난다면 성능 저하를 일으킬 수 있다. 그렇기 때문에 가장 좋은 사용법은 함수형 컴포넌트가 같은 props로 자주 렌더링 될거라 예상될 때이다. 일반적으로 부모 컴포넌트에 의해 하위 컴포넌트가 같은 props로 리렌더링 될 때가 있다.

  1. 컴포넌트가 같은 props로 자주 렌더링될 때
  2. 컴포넌트가 무겁고 비용이 큰 연산이 있을 때

이 두가지 로 사용해야할 때를 정리할 수 있을거 같다.

꼭 사용되지 말아야 할 곳은?

props가 자주 변경되는 컴포넌트를 래핑한다면 React는 리렌더링 할 때마다 두가지 작업을 수행한다.

  1. 이전 props와 다음 props 동등 비교를 위해 비교 함수를 수행한다.
  2. 비교 함수는 거의 항상 false를 반환한다. React는 이전 렌더링 내용과 다음 렌더링 내용을 비교한다.

이는 props 비교가 불필요해지기 때문에 React.memo로 래핑할 필요가 없다.

props 콜백 함수 주의

const test = () => {
  const [count, setCount] = useState(0)
  return <ChildrenComp propFunction={() => setCount(prev => (prev + 1))}>
}

const ChildrenComp = ({propFunction}) => {
  // 비교 함수 수행시 이전 propFunction과 다음 propFunction은 다르다
}

부모 컴퍼넌트가 자식 컴퍼넌트의 콜백 함수를 정의한다면, 새 함수가 암시적으로 생성될 수 있다. 그래서 메모이제이션이 되도록하려면 useCallback을 사용해야한다.

const test = () => {
  const [count, setCount] = useState(0)
  const propFunction = useCallback(() => {
    setCount(prev => (prev + 1))
  }, [])
  return <ChildrenComp propFunction={propFunction}>
}

useCallback을 이용해서 콜백 인스턴스를 보존시킨다. 콜백 함수를 prop으로 사용하는 컴포넌트에서 메모이징을 할 때 주의해야 한다. 같은 렌더링을 할 때 이전과 동일한 콜백 함수 인스턴스를 넘기는지 확실히 해야 한다.

props가 없으면 memo 동작하지 않는다.

참고

리액트 공식문서
블로그 참고1
블로그 참고2

0개의 댓글