리액트 useCallback

hyun·2022년 12월 24일
4

React Hooks

목록 보기
4/5
post-thumbnail

이 포스트에서 다룰 것

useMemo에 이어, 리액트의 최적화 훅인 useCallback에 대해 학습한다. 입사하고 나서 부랴부랴 공부하는 리액트(ㅠㅠ)! 매일 매일 쌓아가면 언젠가는 친해질 거라 믿는다.

useCallback이란?

useMemo와 마찬가지로 메모이제이션 기법을 통해 성능을 향상시키는 훅이다. useMemo가 일반 값(숫자, 문자열, 객체)을 재사용할 때 사용하는 훅이라면 useCallback은 함수를 재사용하기 위해 사용하는 훅이다.

메모이제이션(Memoization)

자주 사용되는 값을 받아오기 위해 반복적으로 계산을 해야 하는 상황에서, 특정 값을 캐싱하는 것을 말한다. 해당 값이 또 필요할 때마다 메모리에서 꺼내서 재사용하는 것이다.

useCallback은 인자로 전달한 콜백 함수 그 자체를 메모이제이션한다. 다음의 예제를 살펴보자.

const addOne = useCallBack((num)=>{
  return num+1
}, [item])

useCallback으로 함수를 감싸주면, 함수를 사용할 때마다 새로 생성하는 것이 아니라 필요할 때마다 메모리에서 가져와서 재사용하게 된다. 자바스크립트에서의 함수는 객체이기 때문에 addOne 변수는 사실상 함수 객체를 담은 것이다. 리액트에서 함수형 컴포넌트는 말 그대로 함수이다. 함수형 컴포넌트가 렌더링이 된다는 것은 함수가 호출되고, 컴포넌트 내의 모든 변수가 초기화된다는 것을 기억하자.

function App() {
  const addOne = (num)=>{
  		return num+1;
  }
}

App 컴포넌트 안에 있는 addOne 변수는 컴포넌트가 렌더링될 때마다 초기화되기 때문에 새로 만들어진 함수 객체가 다시 할당될 것이다. 이를 useCallback으로 감싸면?

function App() {
  const addOne = useCallBack((num)=>{
  		return num+1;
  }, [item]);
}

컴포넌트가 다시 렌더링이 되더라도 addOne이 초기화되지 않는다.
그 말은,

  • 컴포넌트가 처음 렌더링 될 때만 함수 객체를 만들어 초기화해 주고
  • 이후에 렌더링될 때는 함수 객체를 새로 만들어서 할당하는 것이 아니라 이전에 할당받은 함수 객체를 재사용한다는 의미이다.

함수(객체)가 재생성되어 불필요한 렌더링이 일어나지 않으니 효율이 좋아진다.

useCallback의 구조

useCallback(()=> {
	// 함수 내용
}, []); // 의존성 베열

useCallback은 두 개의 인자를 받는다. 첫 번째는 메모이제이션 할 콜백함수, 두 번째는 의존성 배열이다.

const addOne = useCallBack((num)=>{
  return num+1
}, [item])

위에서 사용한 예시를 다시 살펴보자. 함수를 useCallback으로 감싸주면 addOne 변수는 메모이제이션된 함수를 갖게 된다. 이렇게 메모이제이션 된 함수는 의존성 배열의 값이 변경되지 않는 이상 다시 초기화되지 않는다. 만약 의존성 배열 안에 있는 값이 변경된다면 그제서야 새로 만들어진 함수 객체로 초기화된다.

useCallback 활용법을 예제를 통해 조금 더 깊게 알아보자!

예제

import { useState, useEffect } from 'react';

function App() {
  const [num, setNum] = useState(0);
  const myfunc = () => {
    console.log(`myfunc: num: ${num}`);
    return;
  }
  
  // myfunc가 바뀔 때만 콘솔이 찍힌다
  useEffect(()=>{
    console.log('myfunc가 변경됨✨');
  }, [myfunc]);
  
  return (
    <div>
      <input type="number" value={num} onChange={e => setNum(e.target.value)} />
      <button onClick={myfunc}> Call myfunc </button>
    </div>

  );
}

export default App;

input을 통해 num을 변경하고, 버튼을 통해 현재 num의 값을 콘솔에 찍는 앱을 만들었다. 그리고 useCallback의 효과를 알아보기 위해 useEffect 코드를 추가했다.

처음 앱이 렌더링될 때, useEffect가 불린다. 그런데 input을 통해 num을 변경할 때도 useEffect가 호출되고 있다. num을 변경하는 것은 myfunc와 관련이 없는데도 말이다.

🤔 왜 그럴까?

리액트 앱에서 state가 변경되면 컴포넌트가 다시 렌더링되므로, num이 변경될 때마다 컴포넌트가 다시 렌더링되면서 myfunc 함수에도 새로운 함수 객체가 할당된다. 함수의 모양은 같아도 객체의 주소값은 다르므로 useEffect가 불리는 것이다.

그러면 useCallback을 사용해서 컴포넌트가 재렌더링이 되더라도 myfunc가 바뀌지 않도록 해보자.

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

function App() {
  const [num, setNum] = useState(0);
  
  // useCallback 사용
  const myfunc = useCallback(() => {
    console.log(`myfunc: num: ${num}`);
    return;
  }, []);

  // myfunc가 바뀔 때만 콘솔이 찍힌다
  useEffect(()=>{
    console.log('myfunc가 변경됨✨');
  }, [myfunc]);

  return (
    <div>
      <input type="number" value={num} onChange={e => setNum(e.target.value)} />
      <button onClick={myfunc}> Call myfunc </button>
    </div>

  );
}

export default App;

myfuncuseCallback으로 감싸주었다. 의존성 배열에 아무것도 넣지 않았으므로 인자로 들어간 콜백 함수는 컴포넌트가 처음 렌더링 될 때 만들어져서 메모이제이션될 것이다. 그리고 myfunc 안에는 메모이제이션된 함수의 주소가 들어간다. 이후에 컴포넌트가 재렌더링이 되어도 myfunc에는 새로운 객체가 만들어져서 할당되지 않는다.

결과를 확인해보면 num이 변경되어도 useEffect가 불리지 않는다. 즉 myfunc가 변하지 않았다는 것을 알 수 있다. 그런데 버튼을 통해 콘솔에 num값을 찍어보면, 숫자를 증가시켰음에도 0이 찍힌다. 우리가 함수를 메모이제이션할 당시의 num state는 0이었기 때문이다. 그러면 이제 코드가 의도대로 동작하게끔 바꿔보자.

  const myfunc = useCallback(() => {
    console.log(`myfunc: num: ${num}`);
    return;
  }, [num]);

useCallback의 두 번째 인자인 의존성 배열에다 num을 넣었다. 이제 num이 변경될 때마다 myfunc가 새로운 함수로 갱신될 것이다. 다른 state가 변화해서 앱 컴포넌트가 재렌더링 되어도 영향을 안 받는지 확인하기 위해 새로운 state인 otherNum을 추가했다.

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

function App() {
  const [num, setNum] = useState(0);
  const [otherNum, setOtherNum] = useState(0);
  const myfunc = useCallback(() => {
    console.log(`myfunc: num: ${num}`);
    return;
  }, [num]);

  // myfunc가 바뀔 때만 콘솔이 찍힌다
  useEffect(()=>{
    console.log('myfunc가 변경됨✨');
  }, [myfunc]);

  return (
    <div>
      <h1> num </h1>
      <input type="number" value={num} onChange={e => setNum(e.target.value)} />
      <button onClick={myfunc}> Call myfunc </button>

      <h1> otherNum </h1>
      <input type="number" value={otherNum} onChange={e => setOtherNum(e.target.value)} />
    </div>
  );
}

export default App;

의도한 대로 잘 된다🎄

profile
프론트엔드를 공부하고 있습니다.

1개의 댓글

comment-user-thumbnail
2022년 12월 25일

좋은 글 잘보고 갑니다~

중간에 "그러면 이제 코드가 의도대로 동작하게끔 바꿔보자." 이후에 의존성 배열에 num이 들어가도록 설명하셨는데 코드 예제에는 빠져있어요!

답글 달기