useCallback을 이용한 성능 최적화



1. useCallback이란?

useCallback은 함수 메모이제이션(Memoization)을 통해 컴포넌트가 리랜더링될 때마다 동일한 함수를 반복적으로 생성하는 문제를 해결합니다. 특정 의존성 배열에 기반하여, 의존성이 변경되지 않으면 기존 함수를 재사용합니다. 이를 통해 불필요한 함수 생성과 자원의 낭비를 줄여 성능을 최적화할 수 있습니다. 특히, 자식 컴포넌트에 콜백 함수를 전달하거나, 복잡한 연산을 포함한 함수를 사용할 때 유용합니다.


2. 설명

1) UseCallbackEx 컴포넌트

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

// useCallback
// - 매번 함수를 생성하지 않고, 함수를 기억해두었다가 필요할 때마다 함수를 재사용.
export default function UseCallbackEx() {
    const [text, setText] = useState('');

    // [before]
    // useCallback 안썼을 때
    // text 상태가 업데이트 되면 = 컴포넌트 리랜더링 = 코드를 다시 읽는다는 뜻
    // = onChangeText 함수도 다시 생성 => 불필요한 작업
    const onChangeText = (e) => {
        setText(e.target.value);
    }

    // [after]
    // useCallback 훅으로 함수를 기억하여
    // 컴포넌트가 리랜더링 되어도, 의존성 배열에 있는 값이 바뀌지 않는 한, 기존 함수를 재사용
    const onChangeTextUseCallback = useCallback((e) => {
        setText(e.target.value);
    }, []);

  return (
    <div>
        <h1>useCallback ex</h1>
        <input type="text" onChange={onChangeTextUseCallback} />
        <div>작성한 : {text || '없음'}</div>
    </div>
  );
}


  • useCallback 미사용
    • onChangeText 함수는 text 상태가 변경될 때마다 새롭게 생성됩니다. 이는 컴포넌트가 리랜더링될 때마다 동일한 코드가 반복적으로 실행되어 함수가 다시 만들어지게 되며, 그 결과 불필요한 자원 낭비가 발생합니다. 예를 들어, 이 함수가 자식 컴포넌트에 전달된다면, 자식 컴포넌트도 불필요하게 리랜더링될 수 있습니다.
  • useCallback 사용
    • useCallback을 사용하면 onChangeTextUseCallback 함수가 메모이제이션됩니다. 이로 인해 컴포넌트가 리랜더링되더라도 의존성 배열에 포함된 값(text)이 변경되지 않는 한, 이전에 생성된 함수가 재사용됩니다. 이는 불필요한 함수 생성과 자식 컴포넌트의 불필요한 리랜더링을 방지하며, 성능을 개선하는 데 도움을 줍니다.


2) UseCallbackEx2 컴포넌트

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

export default function UseCallbackEx2({ postId }) {
    const [post, setPost] = useState({});

    // [before]
    const getPost = async () => {
        console.log('data fetching...');
        // 데이터 요청
        const res = await axios.get(
            `https://jsonplaceholder.typicode.com/posts//${postId}`
        );
        setPost(res.data);
    };

    // useEffect 의존성 배열에 "함수"를 넣으면
    // 컴포넌트가 리랜더링 -> 함수가 재생성 (주소값 변경) ->  getPost 재호출 => 갯수만큼 계속 랜더링 (무한랜더링)
    // useEffect(() => {
    //     getPost();
    // }, [getPost]) // 무한 랜더링으로 인해 주석 처리


    // [after]
    // useCallback 훅으로 메모이제이션 -> 의존성 배열에 있는 postId가 변경되지 않는 한 컴포넌트는 리랜더링 되지 않음
    const getPostUseCallback = useCallback(async () => {
        console.log('data fetching...');
        const res = await axios.get(
            `https://jsonplaceholder.typicode.com/posts//${postId}`
        );
        setPost(res.data);
    }, [postId]);

    useEffect(() => {
        getPostUseCallback();
    }, [getPostUseCallback]);

    return (
        <div>
            <h1>UseCallbackEx2</h1>
            {post.id ? post.title : '로딩 중...'}
            <br />
        </div>
    );
}


  • useCallback 미사용
    • getPost 함수는 컴포넌트가 리랜더링될 때마다 새롭게 생성됩니다. 만약 이 함수가 useEffect의 의존성 배열에 포함되어 있다면, 리랜더링 시마다 함수가 다시 생성되고, 그로 인해 useEffect가 무한히 실행될 가능성이 있습니다. 이는 심각한 성능 문제를 일으킬 수 있으며, 실제로는 원하는 동작이 아닐 가능성이 큽니다.
  • useCallback 사용
    • useCallback을 사용하여 getPostUseCallback 함수를 메모이제이션하면, postId가 변경되지 않는 한 함수는 재사용됩니다. useEffect의 의존성 배열에 이 메모이제이션된 함수를 포함시키면, postId가 변경될 때에만 데이터 요청이 수행되며, 불필요한 데이터 요청과 무한 리랜더링 문제를 방지할 수 있습니다. 이는 특히 외부 API 호출이 빈번한 상황에서 성능 최적화에 매우 유용합니다.


3. useMemouseCallback의 차이점

React에서 useMemouseCallback은 둘 다 메모이제이션(Memoization)을 통해 성능을 최적화하는 데 사용되지만, 목적과 사용 방법에는 차이가 있습니다.

1) useMemo

  • useMemo는 컴포넌트가 리랜더링될 때 특정 계산의 결과를 저장해 두고, 동일한 입력값이 주어질 경우 해당 결과를 재사용합니다. 주로 연산 비용이 큰 작업이나 복잡한 계산을 반복적으로 수행하는 경우에 사용됩니다.

  • 예시

    • 대량의 배열 데이터를 필터링하여 새로운 배열을 생성하는 작업의 경우 반복될 때마다 시간이 많이 소요될 수 있습니다. useMemo를 사용하면, 입력값이 변경되지 않는 한 이전에 계산된 결과를 재사용하여 불필요한 연산을 줄일 수 있습니다.
    const filteredData = useMemo(() => {
        return Array.filter(item => item.isActive);
    }, [Array]);
    
    • Array가 변경되지 않는 한 filteredData는 새로 계산되지 않고 이전 값을 그대로 재사용합니다. 이를 통해 컴포넌트 리랜더링 시 불필요한 연산을 방지할 수 있습니다.

2) useCallback

  • useCallback은 특정 함수의 재생성을 방지하기 위해 사용됩니다. 컴포넌트가 리랜더링될 때마다 함수가 새로 생성되는 것을 방지하여, 불필요한 리랜더링이나 자원 낭비를 줄입니다. 특히, 자식 컴포넌트에 함수가 props로 전달될 때 유용합니다.

  • 예시

    • 만약 부모 컴포넌트가 자식 컴포넌트에게 어떤 콜백 함수를 전달해야 한다면, useCallback을 사용하여 함수가 매번 새로 생성되는 것을 막을 수 있습니다. 이는 자식 컴포넌트가 불필요하게 리랜더링되는 것을 방지할 수 있습니다.
    const click = useCallback(() => {
        console.log('Button clicked');
    }, []);
    
    • click 함수는 의존성 배열이 빈 상태이므로, 컴포넌트가 리랜더링되더라도 동일한 함수가 재사용됩니다. 이는 자식 컴포넌트가 이 함수를 props로 받아 사용할 때 특히 유용합니다.

3) 주요 차이점

(1) 목적

  • useMemo는 값을 메모이제이션하여 연산 비용을 줄이기 위한 것이고,
  • useCallback은 함수 자체를 메모이제이션하여 불필요한 함수 재생성을 막기 위한 것입니다.

(2) 사용 예시

  • useMemo는 복잡한 계산이나 대량의 데이터 처리 등에서 사용되며,
  • useCallback은 콜백 함수가 자주 리랜더링되어 불필요한 자원 낭비를 초래할 때 사용됩니다.

(3) 반환 값

  • useMemo는 계산된 을 반환하며,
  • useCallback은 메모이제이션된 함수를 반환합니다.


4. useCallback 사용 시 주의 사항

  • useCallback은 의존성이 자주 변경되거나, 함수의 생성 비용이 낮다면 useCallback을 사용하는 것이 오히려 성능을 저하시킬 수 있습니다. 따라서, 성능 이점을 얻을 수 있는 상황에서만 사용하는 것이 좋습니다.

  • 자식 컴포넌트에 콜백 함수를 전달할 때, 해당 자식 컴포넌트의 리랜더링을 방지하기 위해 사용되는 것이 일반적입니다. 그렇지 않은 경우에는 굳이 사용하지 않아도 됩니다.