useCallback

kim98111·2022년 1월 25일
0

React

목록 보기
17/29
post-thumbnail

React.memo의 문제점

자바스크립트에서 객체 타입의 값은 평가될 때마다 언제나 새로운 값을 생성하기 때문에 객체 타입의 값을 prop으로 전달할 때 React.memo를 사용하더라도 컴포넌트가 언제나 재평가되기 때문에 React.memo를 사용하는 의미가 없어집니다.

예를 들어, 함수의 경우 함수 내부 코드가 변경되지 않았지만, 컴포넌트가 재평가될 때마다 함수 정의도 재평가되어 새로운 함수 객체가 생성됩니다. 그러므로 React.memo를 사용하더라도 props으로 함수를 전달하는 경우 컴포넌트가 언제나 재평가가 됩니다.

이를 해결하기 위해서는 react에서 제공하는 useCallback 훅을 사용합니다. useCallback 훅은 컴포넌트 내부에 정의되어 있는 함수를 "리액트에 저장"하는 훅입니다.
즉, react에 함수를 저장해서 이 함수를 매번 재평가할 때마다 재생성하지 않고 특정 조건에만 생성하도록 알리는 것입니다.

컴포넌트 내부에 정의된 함수를 컴포넌트가 처음 실행될 때 react의 내부 저장소 어딘가에 "저장"하고, 컴포넌트가 재평가가 된다면 특정 조건에 부합하지 않을 때
이전에 메모리에 저장한 함수 객체를 "재사용"함으로써 더이상 함수 객체를 재생성하지 않게 됩니다. 이러한 동작을 useCallback 훅이 담당합니다.

useCallback

useCallback 훅을 사용하는 방법은 첫 번째 인수로 "재생성하고 싶지 않은 함수"useCallback 훅의 인수로 전달하면서 호출합니다. 두 번째 인수로는 "dependencies 배열"을 전달합니다.

// 첫 번째 인수로는 함수를 전달하고, 두 번째 인수로는 dependencies 배열을 전달합니다.
const func = useCallback(function, [...dep]);

useCallback훅을 통해서 첫 번째 인수로 전달한 함수 객체가 생성되어 react 내부에 저장되고, 함수 객체를 반환합니다.

이후 컴포넌트가 재평가될 때 dependencies 배열의 요소값이 이전값과 일치하지 않은 경우에만 함수를 재평가하여 함수 객체를 새로 생성하고, 그렇지 않은 경우에는 react에 저장된 함수 객체를 재사용합니다.


// App.js
import React, { useState } from 'react';
import DemoOutput from './DemoOutput.js';
import Button from '../UI/Button.js';

function App() {
    console.log('App Running');

    const [showParagraph, setShowParagraph] = useState(false);
    
    const toggleParagraphHandler = () => {
        setShowParagraph(prevShowParagraph => !prevShowParagraph);
    }
    
    return (
        <div>
            <h1>Hi there!</h1>
            <Output show={false}/>
            <Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
        </div>
    );
}

export default App;

App 컴포넌트는 이전 글에서 사용하던 예시를 그대로 들고 왔습니다. 이전 예시에서 Button 컴포넌트에 onClick prop으로 전달한 toggleParagraphHandler 함수는 컴포넌트 재평가될 때마다 재생성되어 Button 컴포넌트에 React.memo를 사용하더라도 재평가가 되었습니다.

이제는 toggleParagraphHandler 함수를 useCallback 훅의 인수로 전달하여, 언제나 재생성하지 않고 특정 조건에 부합할 때만 재생성하도록 만들 수 있습니다.

// App.js
import React, {useState, useCallback} from 'react';
,,,

function App() {
    ,,,
    const toggleParagraphHandler = useCallbakc(() => {
        setParagraph(prevShoParagraph => !prevShoParagraph);
    });
}

export default App;

useCallback 훅의 인수로 전달한 함수 객체를 react의 내부 저장소 어딘가에 저장한 다음에, 인수로 전달한 함수 객체를 반환합니다. 이후 App 컴포넌트가 재평가되어 다시 실행될 때 useCallback 훅은 react가 저장한 함수를 찾아서 동일한 함수 객체를 재사용합니다.

따라서 어떤 함수의 로직이 절대 변경되지 않는다면, useCallback 훅을 사용하여 그 함수를 "재생성되지 않도록 저장"하면 됩니다.


useCallback을 사용할 때 또 다른 주의점으로 useEffect 훅과 유사하게 두 번째 인수로 "dependencies 배열"을 전달해야 합니다.

dependencies 배열에는 함수에서 사용하는 상태 혹은 props를 "모두 포함"해주어야 합니다. 만약 넣지 않는다면 함수 내에서 해당 값들을 참조할 때 가장 최신의 값을 보장할 수 없게됩니다. 단, 상태 변경 함수나, 컴포넌트 외부에서 선언한 변수, 내장 API 등은 추가하지 않아도 됩니다.

여기서 왜 dependencies 배열이 왜 필요한지 의문이 생길 수 있습니다. 컴포넌트의 리렌더링 주기에서 함수는 언제나 같은 로직을 갖기 때문입니다. 자바스크립트에서 함수는 "클로저"라는 것을 명심해야 합니다. 즉, 해당 환경에서 사용할 수 있는 값에 대해 클로저를 형성합니다. 구체적인 예시로 그 의문점을 해결해보겠습니다.

만약 Button이라는 컴포넌트가 두 개가 있다고 가정해보겠습니다.

// App.js
import React, { useState } from 'react';
import DemoOutput from './DemoOutput.js';
import Button from '../UI/Button.js';

function App() {
    console.log('App Running');

    const [showParagraph, setShowParagraph] = useState(false);
    const [allowToggle, setAllowToggle] = useState(false);
    
    const toggleParagraphHandler = useCallback(() => {
        if (allowToggle) {
            setShowParagraph(prevShowParagraph => !prevShowParagraph);
        }
    }, []);
    
    const allowToggleHandler = () => {
        setAllowToggle(true);
    }
    
    return (
        <div>
            <h1>Hi there!</h1>
            <Output show={false}/>
            <Button onClick={allowToggleHandler}>Allow Toggle!</Button>
            <Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
        </div>
    );
}

export default App;

Allow Toggle이라는 문자열 콘텐츠를 가진 Button 컴포넌트를 클릭하면 allowToggleHandler가 호출되고 내부에서는 setAllowToggle 상태 변경 함수를 호출하여 allowToggle 상태를 true로 업데이트합니다.
그리고 Toggle Paragraph라는 문자열 콘텐츠를 가진 Button 컴포넌트를 클릭하면 먼저 allowToggle 상태값이 true인지 확인하고, true인 경우에만 setShowParagraph 상태 변경 함수를 호출하여 showParagraph라는 상태값을 true로 업데이트합니다.

하지만 우리의 예상과는 달리 Allow Toggle을 클릭하고 Toggle Paragraph 버튼을 클릭해도 toggleParagraphHandler 이벤트 핸들러가 호출되지 않습니다. 그 이유는 자바스크립트에서 함수는 클로저이고 현재 useCallback 훅을 제대로 사용하고 있지 않기 때문입니다.

useCallback 훅에 인수로 전달한 함수는 "클로저"입니다. 그 말은, 인수로 전달한 함수의 정의 평가될 때(App 힘수 컴포넌트가 실행될 때) 자바스크립트는 기본적으로 상위 컨텍스트인 App 함수 컴포넌트에서 정의된 변수를 함수 내부에서 참조하는 경우 클로저가 되며, 클로저는 상위 컨텍스트에 정의한 변수를 참조하거나 변경할 수 있습니다. 여기서는 if 문에서 사용하고 있는 allowToggle 변수가 됩니다.
다음에 toggleParagraphHandler가 실행될 때, 내부에서 참조하는 allowToggle 변수의 값은 "함수 정의가 평가된 시점"의 값을 기억하고 있습니다. 여기서 문제가 발생하게 됩니다.

우리는 useCallback 훅을 사용하여 인수로 전달한 함수를 메모리 어딘가에 저장하도록 합니다. 그러면 App 컴포넌트가 재평가되어 재실행될 때 리액트는 해당 함수를 재생성하지 않습니다. useCallback 훅은 어떤 상황에서도 재생성하지 말라고 리액트에게 명령했기 때문입니다.
따라서 함수 내부에서 사용하는 allowToggle 값은 최신값이 아닌 여전히 App 컴포넌트가 "처음 실행되었을 때의 값을 참조"하기 때문에 문제가 발생하게 됩니다.

이러한 상황처럼 우리는 진짜로 함수를 "재생성"하고 싶을 수 있습니다. 함수 외부에서 내부로 전달되는 값이 변경되었을 경우가 있기 때문입니다.
우리는 useCallback 훅의 두 번째 인수로 dependencies 배열의 요소에 allowToggle을 추가하여 문제를 해결할 수 있습니다.

이는 리액트에게 우리가 일반적으로는 그 함수를 재생성하지 않도록 하지만, dependencies 배열의 요소인 allowToggle이 "변경될 때만" 해당 함수를 다시 생성하도록 명령합니다. 이렇게 작성하면 함수 내부에서는 언제나 최신의 allowToggle 값을 사용하게 됩니다. 그러나 allowToggle 값이 변경되지 않은 경우 함수를 재생성하지 않을 겁니다.

profile
Frontend Dev

0개의 댓글