관리 메뉴

CASSIE'S BLOG

useMemo & useCallback 본문

PROGRAMMING/React

useMemo & useCallback

ITSCASSIE1107 2023. 6. 11. 22:11

1. 개요

useMemo와 useCallback이라고 하는 hook 함수가 있지만 둘의 개념을 살펴보면 두 함수다 비슷한 역할을 가지고 있는 것 같아, 언제 어느 상황에 useMemo 또는 useCallback을 써야할지 판단을 하기 위해 정리를 해보려고 합니다.

(심지어 useCallback 안에 useMemo를 쓰는 경우도 있었음)

 

 

 

2. Memoization

먼저 메모이제이션이라고 하는 개념에 대해 확실히 알아가야하는데, 메모이제이션은 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법입니다.

이것을 적절하게 활용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화 할 수 있습니다.

 

 

 

3. useMemo

useMemo는 메모이제이션된 값을 반환하는 함수입니다.

useMemo(() => fn, [deps])

여기서 deps로 지정한 값이 변하게 된다면 () => fn 함수를 실행하고, 그 함수의 반환 값을 반환해줍니다.

deps는 dependency의 약어로, 의존성을 뜻하며 useMemo가 이 deps에 의존하고 있다는 것을 말합니다.

 

 

import React, { useState, useCallback, useMemo } from "react";

export default function App() {
  const [ex, setEx] = useState(0);
  const [why, setWhy] = useState(0);

  // useMemo 사용하기
  useMemo(() => {console.log(ex)}, [ex]);

  // 두 개의 버튼을 설정했다. X버튼만이 ex를 변화시킨다.
  return (
    <>
      <button onClick={() => setEx((curr) => (curr + 1))}>X</button>
      <button onClick={() => setWhy((curr2) => (curr2 + 1))}>Y</button>
    </>
  );
}

한 가지 예로 위와 같이 useMemo를 사용할 수 있습니다.

여기서 X라는 버튼을 클릭했을 때 setEx에 의해서 ex의 값을 1씩 증가시키는데, ex의 값이 변하기 때문에 useMemo에서  의존성으로 등록한 ex가 변화된 것을 감지해 지정한 함수가 실행되고, console.log로 인해 ex값이 출력되게 됩니다.

 

 

3-1. useMemo를 쓰는 이유?

import React, { useState, useCallback, useMemo } from "react";

export default function App() {
  const [ex, setEx] = useState(0);
  const [why, setWhy] = useState(0);

  // 버튼 클릭시 ex값이 출력된다.
  console.log(ex); 
  
  return (
    <>
      <button onClick={() => setEx((curr) => (curr + 1))}>X</button>
      <button onClick={() => setWhy((curr2) => (curr2 + 1))}>Y</button>
    </>
  );
}

컴포넌트의 state 값이 변하면 리렌더링이 일어나는 것은 모두 알고 계실 겁니다.

*렌더링 과정에 대해 이해가 가지 않는다면 아래 링크를 참고해주세요

https://narup.tistory.com/272

 

[React] 렌더링(리렌더링)

1. 개요 React를 공부하면서 어떻게 보면 가장 기초이고, 가장 중요한 부분인데 사실 이제야 포스팅한다는 것은 이제까지 리액트를 잘못 사용해왔다는 반증이기도 한데... 조금 더 심도있게 리액

narup.tistory.com

 

버튼을 클릭할 경우 setState로 인해 state값이 변화되어 리렌더링이 일어나면 console.log가 찍히도록 위처럼 코드를 작성하면 되는데, useMemo를 사용하는 이유는 뭘까요?

 

사실, 위처럼 코드를 작성할 경우 X버튼, Y버튼, 혹은 컴포넌트가 부모 컴포넌트에 의해서 리렌더링이 될 경우 상태 값에 관계 없이 console.log가 찍히는 연산이 발생하게 됩니다.

 

만약, 단순하게 console.log가 아닌 복잡한 연산, 연산할 때마다 약 0.3초씩 걸린다고 가정하면 어떨까요? 답답하겠죠?

 

따라서 ex값이 변할 경우에만 연산을 실행할 수 있도록 useMemo를 사용해 ex라는 변수에 의존하도록 등록하는 것이지요.

 

결과, 리렌더링이 발생할 경우, 특정 변수가 변할 때에만 useMemo에 등록한 함수가 실행되도록 처리하면 불필요한 연산을 하지 않게 됩니다!

 

 

 

 

4. useCallback

useCallback은 메모이제이션된 함수를 반환하는 함수이다.

 

앞서 보았던 useMemo는 메모이제이션된 값을 반환해서 참 직관적이었는데, 이 useCallback은 메모이제이션된 "함수"를 반환하는 특징을 가지고 있습니다.

useCallback(fn, [deps])

useCallback 또한 deps, 의존성이 있는 값이 변하면 fn에 등록한 함수를 반환하는 기능을 가지고 있습니다.

useMemo(() => console.log(), [test])
const memoizedCallback = useCallback(() => console.log(), [test])

일단은 useMemo와 useCallback의 모양은 위와 같습니다.

useCallback이 함수를 반환하기 때문에 그 함수를 가지는 const 변수에 초기화하는 것이 일반적인 모양입니다.

 

useMemo같은 경우는 deps값이 변하면 이 함수를 실행해라! 라는 느낌으로 활용이 가능한데,

useCallback은 어떨 때 사용해야 할까요?

 

useCallback 사용처는 아래와 같습니다.

1) 자식 컴포넌트에 props로 함수를 전달할 경우

2) 외부에서 값을 가져오는 api를 호출하는 경우

 

 

 

4-1) 자식 컴포넌트에 props로 함수를 전달하는 경우

먼저 함수는 값이 아닌 참조로 비교된다!는 점을 알고 있어야합니다.

const functionOne = function() {
  return 5;
};
const functionTwo = function() {
  return 5;
};
// 서로의 참조가 다르기 때문에 false
console.log(functionOne === functionTwo);

동일한 값을 반환하지만 참조가 다르기 때문에 false가 나옵니다.

위와 같이 컴포넌트에서 특정 함수를 정의할 경우 각각의 함수들은 모두 고유한 함수가 됩니다.

이런 고유한 함수가 생성될 경우, 부모를 통해 props에 함수를 전달받는 자식 컴포넌트에서는 props가 변경되었다고 판단해 리렌더링이 발생하게 됩니다.

 

function App() {
  const [name, setName] = useState('');
  const onSave = () => {};

  return (
    <div className="App">
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <Profile onSave={onSave} />
    </div>
  );
}

useCallback을 사용하지 않을 경우, name이 변경되어 리렌더링이 발생하면 onSave함수가 새로 만들어지고, Profile 컴포넌트의 props로 onSave함수가 새로 전달되게 됩니다.

이때 Profile 컴포넌트에서 useMemo를 사용해도 이전 onSave와 이후 onSave가 같은 값을 반환하지만 참조가 다른 함수가 되어버리기 때문에 리렌더링이 일어나게 됩니다.

 

부모 컴포넌트만 수정하려고 했지만 연쇄적으로 하위 컴포넌트들 모두 렌더링이 일어나게 되어버리지요.

 

따라서 아래와 같이

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


function App() {
  const [name, setName] = useState('');
  const onSave = useCallback(() => {
    console.log(name);
  }, [name]);

  return (
    <div className="App">
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <Profile onSave={onSave} />
    </div>
  );
}

useCallback을 사용해서 onSave라는 함수를 재사용하는 것으로 자식 컴포넌트의 리렌더링을 방지할 수 있습니다!

 

 

4-2) 외부에서 값을 가져오는 api를 호출하는 경우

import React, { useState, useEffect } from "react";

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  const fetchUser = () =>
    fetch(`https://your-api.com/users/${userId}`)
      .then((response) => response.json())
      .then(({ user }) => user);

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

위의 코드는 fetchUser 함수가 변경될 때만 외부에서 api를 가져와 useEffect가 실행됩니다.

사실 이 코드는 정상적인 코드가 아닌데요.

앞서 말씀 드렸다시피 Profile이라는 컴포넌트가 리렌더링이 발생할 경우 fetchUser 함수에는 새로운 함수가 할당되게 됩니다. 그러면 useEffect()함수가 호출되어 user 상태값이 바뀌고, state 값이 바뀌었기 때문에 다시 리렌더링이 일어납니다.

무한루프에 빠져버리게 되는 것이지요.

 

이때 useCallback을 사용할 경우 fetchUser 함수의 참조값을 동일하게 유지시킬 수 있습니다.

import React, { useState, useEffect } from "react";

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  const fetchUser = useCallback(
    () =>
      fetch(`https://your-api.com/users/${userId}`)
        .then((response) => response.json())
        .then(({ user }) => user),
    [userId]
  );

  useEffect(() => {
    fetchUser().then((user) => setUser(user));
  }, [fetchUser]);

  // ...
}

api의 옵션으로 사용되는 userId가 변동될 때만 fetchUser에 새로운 함수가 할당되도록 설정하고, 그것이 아니면 동일한 함수가 실행되게 되서 무한 루프에 빠지지 않도록 할 수 있습니다!

 

반응형