Skip to content

useCallback의 활용 #17

@cryingdryice

Description

@cryingdryice

React API 레퍼런스: https://ko.react.dev/reference/react/useCallback

useCallback 함수란?


→ React에서 제공하는 Hook으로, 주로 렌더링 성능을 최적화해야 하는 상황에서 사용한다. 함수를 재사용 가능하게 해 불필요한 함수가 새로 생성되는 것을 방지해준다.

import { useCallback } from 'react';

...
const cachedFn = useCallback(fn, dependencies);

useCallback 함수는 2개의 매개변수를 가진다.

첫번째 파라미터 (fn) : 생성하려는 함수.

두번재 파라미터 (dependencies) : 함수에서 참조되는 값들의 배열.
배열안의 값들 중 하나라도 바뀌면 함수가 재생성됨.


아래 예시를 보자. count의 값을 1씩 증가시키는 함수이다.

const [count, setCount] = useState(0);

const increment = useCallback(() => {
  setCount(count + 1);
}, [count]);

increment 함수가 호출될 때 setCount를 통해 count의 상태가 변화한다.

→ 의존성 배열 안의 값인 count가 변화했으므로 increment 함수가 재생성된다.

→ increment 함수는 올바르게 최신 상태 값(count)를 반영할 수 있다.

만약 count를 의존성 배열에 포함하지 않는다면 어떻게 될까?

const [count, setCount] = useState(0);

const increment = useCallback(() => {
  setCount(count + 1);
}, []); // 빈 배열

increment 함수는 초기 렌더링 시의 count값(0)을 참조하게 된다.

→ count의 값이 변화하더라도 함수 내부에서는 그 변화를 알 수 없다.

→ 최신 count 값을 사용할 수 없어 초기 렌더링 시의 count 값인 0만을 참조하게 된다.

두번째 파라미터의 배열은 함수의 재생성 여부를 결정하는 중요한 요소이다.

※ 별개로 위 처럼 두 번째 파라미터에 빈 배열을 사용한다면 최초 랜더링될 때 생성된 함수만을 계속해서 재사용한다.

useCallback 함수가 유용한 상황


어떨 때 useCallback함수를 사용하면 좋을까? 아래 예제 코드를 살펴보자.
count의 값을 증가하거나 text의 값을 변경하는 코드이고, 이번엔 자식과 부모 컴포넌트로 이루어져 있다.

import React, { useState, useCallback } from 'react';

// 자식 컴포넌트
const ChildComponent = React.memo(({ onClick }) => {
  return (
    <button onClick={onClick}>Increment</button>
  );
});

// 부모 컴포넌트
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

	// useCallback을 사용 안할 경우
  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>Text: {text}</p>
      <p>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
      </p>
      <ChildComponent onClick={increment} />
    </div>
  );
}

export default ParentComponent;

부모 컴포넌트는 count와 text 두 가지의 상태를 관리한다.

자식 컴포넌트에 increment 라는 props를 전달한다.

자식 컴포넌트는 React.memo로 래핑되어 있어, 부모로부터 받은 props가 변경되지 않는 한 리렌더링되지 않는다.

만약 count와 관련없는 text가 변경되면 어떻게 될까?

onChange 함수를 통해 text가 변경된다.

→ 부모 컴포넌트가 리렌더링되면서 increment 함수가 재생성된다.

→ increment라는 props가 변화했으므로, 자식 컴포넌트가 불필요하게 리렌더링된다.

→ React.memo를 통해 자식 컴포넌트를 최적화한 의미가 없어지고 성능 저하로 이어진다.

만약 increment를 useCallback 함수를 이용해 선언한다면?

import React, { useState, useCallback } from 'react';
...
// 부모 컴포넌트
function ParentComponent() {
...
	// useCallback을 사용할 경우
  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);
...
}

export default ParentComponent;

text의 값이 변경되어 부모 컴포넌트가 리렌더링된다.

→ count의 값은 바뀌지 않았으므로, increment 함수는 재생성되지 않고, 동일한 참조가 유지되어 자식 컴포넌트는 변경되지 않은 props가 전달된다.

→ React.memo로 인해 자식 컴포넌트는 리렌더링되지 않는다.

→ 성능 최적화

모든 함수를 useCallback으로 선언하면 좋지 않은가?


이쯤에서 필자는 하나의 의문이 들었다.

모든 함수를 useCallback으로 선언하면 언제든지 불필요한 함수가 재생성되는 것을 방지할 수 있지 않을까?

하지만 그건 좋은 방법이 아니라고 한다.

오버헤드 증가

useCallback 함수는 메모제이션을 위해 메모리와 cpu자원을 사용한다.
무분별한 사용은 메모제이션으로 인한 오버헤드가 증가할 수 있다.

코드 복잡성 증가

useCallback 함수는 어떤 성능을 최적화하기 위해 사용되었는지 명시되어야 한다. 그렇지 않으면 협업 환경에서 코드의 가독성이 떨어지고 혼란을 일으켜 유지보수가 어려워질 수 있다.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions