useCallback 알아보기

IT공부중·2021년 2월 8일
1

React

목록 보기
6/10

https://dmitripavlutin.com/dont-overuse-react-usecallback/
이 글을 읽고 제 나름대로 번역한 글입니다.

useCallback은 함수를 메모이제이션 하는 것이다.

어떤 사람이 물었다고 한다. '우리 팀 어떤분은 모든 함수에 useCallback을 사용한다. 팀원의 말로는 자식 컴포넌트의 불필요한 리렌더링을 방지하기 위해 모든 콜백함수를 memoized 해야한다'고 말했다.

하지만 이것은 진실과는 거리가 멀고, 컴포넌트를 느리게 만든다.

JS에서 함수의 비교

useCallback을 알아보기 전에 함수의 비교에 대해서 알아본다.

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에서 만들어진 함수이고 똑같은 역할을 하는데도 같은 함수가 아니다.

자바스크립트의 함수는 일급객체이기 때문에 일반 객체들과 똑같이 쓰일 수 있다. 함수끼리는 주소값으로 비교를 하기 때문에 다른 함수인 것이다.

useCallback의 목적

import React from 'react';

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

  // ...
}

handleClick은 MyComponent의 모든 렌더링마다 다른 함수이다. 함수 재생성은 비용이 적기 때문에 각 렌더링에서 함수를 재생성하는 것은 문제가 되지 않는다.

그러나 몇가지 경우에서 렌더링 사이에 하나의 함수로만 존재하게 만들어야 할 때가 있다.

  1. React.memo 로 감싼 자식 컴포넌트가 해당 함수를 props로 받을 때
  2. 함수가 useEffect 등의 다른 훅의 dependency에 들어가 있는 경우

useCallback은 이럴 때 도움이 된다. 동일한 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';
import useSearch from './fetch-items';

function MyBigList({ term, onItemClick }) {
  const items = useSearch(term);

  const map = item => <div onClick={onItemClick}>{item}</div>;

  return <div>{items.map(map)}</div>;
}

export default React.memo(MyBigList);

엄청 큰 List를 렌더해야한다고 생각해보자.

이 리스트는 수백 수천개의 아이템을 가지고 있다. 쓸모없는 리렌더링을 예방하기 위해 React.memo를 사용했다.

부모 컴포넌트인 MyBigList는 아이템이 클릭했을 때 어떤 아이템이 클릭 되었는지 출력해주는 onItemClick이라는 함수를 자식 컴포넌트인 MyBigList에게 props로 넘겨준다.

import React, { useCallback } from 'react';

export default function MyParent({ term }) {
  const onItemClick = useCallback(event => {
    console.log('You clicked ', event.currentTarget);
  }, [term]);

  return (
    <MyBigList
      term={term}
      onItemClick={onItemClick}
    />
  );
}

onItemClick 는 useCallback에 의해 메모이제이션 되었다. 이제 term 이 바뀔 때까지 항상 같은 함수를 반환할 것이다.

MyParent 컴포넌트가 리렌더링 될 때 onItemClick은 같은 함수이기 때문에 MyBigList의 메모이제이션이 깨지지 않는다.

이것이 올바른 useCallback의 사용법이다.

안 좋은 사용 예

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>;
}

여기에 useCallback의 적용하는 것이 좋은 것일까? 그렇지 않다.

useCallback hook은 MyComponent 호출마다 호출된다.

동일한 함수 객체를 반환하는 useCallback 조차도 렌더링할 때마다 내부 콜백은 다시 생성이 된다.

(단지 deps가 안 바뀌었으니 건너 뛸 뿐이다.)

이렇기 때문에 최적화를 한 것과 안 한 것의 차이가 별로 없기 때문에 별 이점이 없다.

코드의 복잡성만 높이게 된다. useCallback의 함수를 메모이제이션하기 위해 deps를 유지시켜야 한다.

그냥 각 리렌더링에 새로운 함수가 생성 되는 것을 허용해라.

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>;
}

요약

useCallback의 적절한 사용 예는 메모이제이션할게 많은 자식 컴포넌트를 제공할 때 콤백함수를 기억하기 위해서이다.

그리고 다시 스스로에게 물어봐라. useCallback을 사용했을 때 복잡성의 증가 보다 성능 향상이 더 많은 이득을 가져와 주는가??

많은 요소의 메모화를 하려면 React.memo와 useCallback을 같이 잘 사용하면 될 것 같다

profile
3년차 프론트엔드 개발자 문건우입니다.

0개의 댓글