useCallback, useMemo

코드위의승부사·2020년 6월 29일
1

useCallback

useCallback hook return a memoized callback

This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.
that meant the reference won't be changed on every render call.
이는 불필요한 렌더링을 방지하기 위해 레퍼런스 평등에 의존하는 최적화된 하위 구성요소에 콜백을 전달할 때 유용하다.

function App() {
  const [count, setCount] = React.useState(0);
  const memoizedIncrement = React.useCallback(() => {
    setCount(count => count + 1);
  }, [setCount]);
  const memoizedDecrement = React.useCallback(() => {
    setCount(count => count - 1);
  }, [setCount]);
  return (
    <>
      Count: {count}
      <button onClick={memoizedDecrement}>Decrement</button>
      <button onClick={memoizedIncrement}>Increment</button>
    </>
  );
}

useCallback hook이 count를 업데이트 시키는 2가지의 새로운 함수만드는데 사용됬다.
setCount함수를 캐시해서 리액트가 setCount가 변하더라도 매번 렌더되는걸 방지한다.
콜백안에 참조된 모든 값들은 의존성배열에 나타나야한다.

useCallback 정확히 사용하는 법

함수 동일성 체크 이해하기

hook이 해결하는 문제점인 functions equality check 알아보기

function factory() {
  return (a, b) => a + b;
}

const sum1 = factory();
const sum2 = factory();

sum1(1, 2); // => 3
sum2(1, 2); // => 3

sum1 === sum2; // => false
sum1 === sum1; // => true

sum1과 sum2는 두 수를 합쳐주는 함수이다. factory함수에 의해 만들어졌고 같은 함수이지만 다른 객체를 갖는다. 두 함수의 ===값은 false이다.
함수를 포함한 모든 객체는 그 자신과만 같다.

useCallback()의 목적

동일한 코드를 공유하는 다른 함수의 인스턴스는 종종 리액트 컴포넌트 요소 내부에 생성된다.
리액트 컴포넌트 안에서 생성되는 함수(callback, event handler)는 매 렌더링마다 재생성된다.

import React from 'react';

function MyComponent() {
  // handleClick is re-created on each render
  const handleClick = () => {
    console.log('Clicked!');
  };

  // ...
}

MyComponent가 매번 렌더링 될 때마다 handleClick은 다른 함수 객체를 갖는다.
inline 함수들은 가볍고, 각 렌더링마다 재생성되는 함수는 문제가 아니다.
컴포넌트 당 적은 줄의 inline 함수는 받아들여질 수 있다.
그러나 함수의 한가지 인스턴스를 유지해야 하는 경우들이 있다.
1. React.memo()안의 컴포넌트 에서 받아들여진 콜백 prop
2. 함수가 다른 훅의 의존성으로 사용된 경우(e.g useEffect(..., [callback])

위와 같은 경우 useCallback(callbackFun, deps)가 필요하다. 동일한 의존성 값을 제공하는 훅은 렌더링간 동일한 함수 인스턴스를 반환한다.

import React, { useCallback } from 'react';

function MyComponent() {
  // handleClick is the same function object
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);

  // ...
}

위의 handleClick 변수는 Mycomponent의 렌더링 사이에서 같은 객체를 갖는다.

좋은사례

많은 수의 아이템들을 렌더해야 하는 경우

import React from 'react';

function MyBigList({ items, handleClick }) {
  const map = (item, index) => (
    <div onClick={() => handleClick(index)}>{item}</div>;
  );
  return <div>{items.map(map)}</div>;
}

export const MyBigList = React.memo(MyBigList);

리스트들의 리렌더링을 막기 위해 React.memo를 사용해야 한다.

MyBigList의 부모 컴포넌트는 아이템 클릭을 핸들하는 함수와 리스트들을 제공해야한다.

import React, { useCallback } from 'react';

import useSearch from './fetch-items';

function MyParent({ term }) {
  const handleClick = useCallback((item) => {
    console.log('You clicked ', item);
  }, [term]);

  const items = useSearch(term);

  return (
    <MyBigList
      items={items}
      handleClick={handleClick}
    />
  );
}

handleClick 콜백은 useCallback()에 의해 memoizied되고, term 변수가 같게 유지되는 한 useCallback()은 동일한 함수 객체를 반환한다.
MyParent 컴포넌트가 리렌더링 되더라도, handleClick은 같게 유지되고, MyBigList의 memoization을 깨뜨리지 않는다.

나쁜사례

import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = useCallback(() => {
    // handle the click event
  }, []);

  return <MyChild onClick={handleClick} />;
}

function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

handleClick memoize가 이해가 가는가?
useCallback() 사용에는 많은 작업이 필요하다. MyCoponent가 매번 렌더링 될때마다 useCallback() 훅이 호출된다. 내부적으로 리액트는 같은 함수객체를 반환할 수 있다. 하지만 인라인 함수는 모든 렌더에 여전히 생성되므로 useCallback()을 그냥 스킵하게된다.
비록 useCallback()이 같은 함수 객체를 리턴하더라도, 어떤 이익도 가질수 없다 왜냐면 최적화 비용 그자체가 더 많이 소요되기 때문이다.
또한, 코드의 복잡성 또한 높아진다.

그냥 간단하게 각 리렌더들이 새로운함수를 만들게 하는 걸 받아들여라.

import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = () => {
    // handle the click event
  };

  return <MyChild onClick={handleClick} />;
}

function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;

최적화에 대해 생각하기 전에 항상 이 문구가 상기된다.

Profile before optimizing

memoize 콜백 함수는 memoized 자식 컴포넌트들에 대해 제공될때 적합하다.
useCallback을 활용할 상황이 맞든 안맞든 profile을 작성하고 적용할지 결정해라.

useMemo

useMemo는 함수로 부터 계산된 값들을 캐시한다.
첫번째 argument 값을 계산하는 함수이고, 두번째 argument는 리턴값을 계산하는데 사용되는 의존성값을 가지고있는 배열이다.
useMemo는 렌더링 도중에 실행된다. 성능최적화를 위해서 사용된다.
리액트는 이전의 메모된 값을 잊어버리고 다음 렌더링때 다시 계산할것이다.
(화면외의 컴포넌트의 메모리를 확보하기 위해)

function App() {
  const [count, setCount] = React.useState(0);
  const doubleCount = React.useMemo(() => 2 * count, [count]);
  return (
    <>
      Double Count: {doubleCount}
      <button onClick={() => setCount(oldCount => oldCount - 1)}>
        Decrement
      </button>
      <button onClick={() => setCount(oldCount => oldCount + 1)}>
        Increment
      </button>
    </>
  );
}
  1. useMemo hook을 전달된 함수에 사용해 count로 두번 리턴 받고, count를 2번째 argument로 넘긴다.
  2. useMemo는 count가 변하기 전까지 doubleCount의 값을 캐시한다.
  3. 버튼의 props로 전달된 onClick에 함수를 전달하여 setCount를 호출한다.
  4. 버튼을 클릭해서 업데이트가 발생하면 doubleCount의 값을 화면에서 나타내준다.

useRef

리액트가 렌더링한 실제 DOM node들의 references의 역할로 실제 DOM 접근이 요구 될때 유용하게 쓰인다.(focusing/measuirng 엘레멘트하는경우)

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

위 코드와 우리가 이해했던 내용들로 useRef가 DOM 노드들의 ref를 저장하는 것으로 알고 있다면 오해다.

useRef는 함수형 컴포넌트에 컴포넌트 인스턴스의 전체 lifetim의 데이터 액세스 권한을 준다.
이미 useState를 활용해서 데이터를 저장하고 저장된 데이터의 변화에 따라 새로운 렌더를 만들지만, useRef에 저장된 데이터는 변하지 않는다.
두가지 모두 함수형 컴포넌트들에서 데이터를 저장하는 방법으로 사용된다.

count에 따라 반복해서 렌더되겠지만, count 증가에 따라 렌더가 트리거 되는걸 원지 않을 경우(무한 루프가 그곳에 기다리고 있다면)

클래스형 컴포넌트를 사용할 때, 우리는 컴포넌트 인스턴스의 프로퍼티를 사용할 수 있다.

class MyCountingComponent extends React.Component {
  constructor () {
    this.count = 0;
  }

  render () {
    this.count++;
    return (<div />);
  }
}

this.count는 count를 저장하고 각 렌더마다 증가되지만 매번 리렌더링 되지는 않는다.

count는 컴포넌트 인스턴스의 전체적인 생명주기에서 인스턴스에 저장된 같은 변수이다.

함수형컴포넌트로 생각해보자면,

const MyCountingComponent = () => {
  let count = 0;
  count++;

  return (<div />);
}

count는 매 렌더 후에도 계속 1일 것이다. 문제는 매 렌더 마다 새로운 count변수가 생성된다. 컴포넌트 인스턴스의 전체 생명주기에서 사용될수 있는 count변수의 레퍼런스가 필요하다. 그 변수의 참조값을 사용해서 간단하게 증가시킬 수 있다.

useRef의 해결점

const MyCountingComponent = () => {
  const count = useRef(0);
  count.current++;
  return (<div />);
}

처음 렌더 시에 useRef는 현재 프로퍼티를 0으로 설정한 객체를 만들 것이다. 이 후의 렌더에서 useRef는 항상 이 참조값을 갖기 때문에, 이 변화가 렌더사이에서 계속 유지된다.
이 것이 현재 속성을 가진 객체를 사용하는 이유다. 객체로 쌓여진 숫자를 사용하는 대신 단지 count를 위해 숫자를 사용한다면 렌더 사이에서 지속적인 값을 가질 수 없을것이다.

References

profile
함께 성장하는 개발자가 되고 싶습니다.

0개의 댓글