ivory's Log
그게 무엇이라도 항상 쉬운 일이다.

ivory's DevLog

useMemo, useCallback 파헤치기 - useCallback편

ivorycode 2021. 11. 23. 10:31
반응형

useCallaback편으로 돌아왔다. 이미지 출처: www.google.com

useCallback

메모이제이션(Memoization)

useCallback은 useMemo와 아주 비슷하다. 특히 둘 다 메모이제이션을 한다는 공통점이 있다. 지난 포스팅에서도 언급했었지만, 너무나도 중요한 개념이니 다시 한번 짚고 넘어가 보자!! 참고로 useCallback도 useMemo처럼 React의 렌더링 성능 최적화를 위해 사용하는 hook이다.

메모이제이션이란 프로그래밍을 할 때 반복되는 결과를 메모리에 저장해 두고, 다음에 같은 결과가 나올 때 다시 계산할 필요 없이 빨리 실행하는 기법이다. 일종의 캐싱과 같다.

 

🆘  캐싱(Caching)

캐싱이란 캐시(Cache)라고 하는 좀 더 빠른 메모리 영역으로 데이터를 가져와 접근하는 방식을 말한다. 캐싱은 오랜 시간이 걸리는 작업의 결과를 저장하여 시간과 비용을 회피하는 기법이므로 고성능 애플리케이션을 만들 때, 가장 중요한 요소로 꼽힌다.

 

useCallback 예제

CodeSandbox 최종 예제 코드
- 최종 예제 코드를 작성했습니다. 디렉토리와 js파일명을 참고해 주시고, 포스팅 내용과 함께 확인해주세요!!
https://codesandbox.io/embed/flamboyant-galileo-uuwuh?fontsize=14&hidenavigation=1&theme=dark

 

useCallback 적용 전 코드

(코드블럭 정렬이 잘 안된다.... 위의 최종 예제 코드와 함께 확인해주세요!!) 코드를 간략하게 파악해보자.

// src/offUseCallback.js

import React, { useState } from "react";
import List from "./List";

const OffUseCallback = () => {
  const [number, setNumber] = useState(1);
  const [dark, setDark] = useState(false);

  const getItems = () => {
    return [number, number + 1, number + 2];
  };

  const theme = {
    backgroundColor: dark ? "#3264c8" : "#fff",
    color: dark ? "#fff" : "#3264c8"
  };

  return (
    <div style={theme}>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value, 10))}
      />
      <button onClick={() => setDark((e) => !e)}>on / off</button>
      <List getItems={getItems} />
    </div>
  );
};

export default OffUseCallback;

- offUseCallback.js

getItems라는 함수는 호출될 때마다 각각의 배열 요소에 저장된 각각 number의 값을 그대로 출력, +1, +2를 해준다. 그리고 이 함수를 input 창을 통해 숫자를 입력할 경우 페이지의 숫자 항목들이 변경되고, button을 누를 경우 setDark 하여 theme 스타일이 바뀌도록 하였다. 마지막으로 getItems함수를 List 컴포넌트에 props로 전달해주었다.

 

//src/List.js

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

const List = ({ getItems }) => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    setItems(getItems());
    console.log("getItems, 숫자 변동됨.");
  }, [getItems]);

  return items.map((item, index) => <div key={index}>{item}</div>);
};

export default List;

 

- List.js

빈 배열의 state를 설정해주고, useEffect의 의존성 배열에 getItems가 발생할 때마다 console.log 하는 형식이다. 해당 컴포넌트가 리턴하는 요소는 변경된 숫자가 저장된 배열을 map을 통해 동일하게 출력해준다.

 

우선 위의 코드의 결과를 살펴보자.

 

버튼을 누를때도 console이 발생한다. 이미지 출처: 내가 작성한 CodeSandbox.io 코드

useMemo때와 비슷한 결과다. 숫자를 변경할 때도, 버튼을 누를 때도 getItems 함수가 호출되면서, console을 출력하고 있었다. 즉, useMemo와 마찬가지로 useCallback을 사용하지 않았을 때, 부모 컴포넌트에서 전달받은 props인 getItems라는 함수가 버튼 이벤트가 발생하고 부모 컴포넌트가 리렌더링 되면서 변경된 props로 인식되면서 함수를 다시 호출했던 것이다. 이럴 때, useCallback을 적용하면 불필요한 렌더링을 줄여줄 수 있다. 바로 적용해보자!

 

useCallback 적용 후 코드

(코드블럭 정렬이 또 안된다.... 위의 최종 예제 코드와 함께 확인해주세요!! 꼭!!!)

// src/showUseCallback.js

import React, { useState, useCallback } from "react";
import List from "./List";

const ShowUseCallback = () => {
  const [number, setNumber] = useState(1);
  const [dark, setDark] = useState(false);

  const getItems = useCallback(() => {
    return [number, number + 1, number + 2];
  }, [number]);

  const theme = {
    backgroundColor: dark ? "#3264c8" : "#fff",
    color: dark ? "#fff" : "#3264c8"
  };

  return (
    <div style={theme}>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value, 10))}
      />
      <button onClick={() => setDark((e) => !e)}>on / off</button>
      <List getItems={getItems} />
    </div>
  );
};

export default ShowUseCallback;

- showUseCallback.js

getItems 함수에 useCallback hook을 매핑시켜 주고, 의존성 배열에 number를 넣어주었다.

 

- List.js

List 컴포넌트는 offUseCallback 컴포넌트에 사용했던 컴포넌트와 동일하다!!

 

코드의 결과를 살펴보자.

 

불필요한 렌더링이 사라졌다. 이미지 출처: 내가 작성한 CodeSandbox.io 코드

 

숫자만 변경될 때만 console을 출력하고 있었고, 버튼을 누를 땐 getItems함수를 호출하지 않는 것을 보니 이번엔 잘 작동하는 것 같다. useCallback 역시 복잡한 로직의 재계산이나 계산과정이 너무 무겁고 복잡할 때 사용하면 성능 최적화의 도움받을 수 있다.

 

useCallback 사용 유무 최종 비교

 

최종 비교 화면 이미지 출처: 내가 작성한 CodeSandbox.io 최종 코드

 

포스팅을 마무리하려고 하는데, useCallback과 useMemo는 서로 너무나도 비슷한 결과를 보이는데, 왜 따로 구분했는지 궁금했다. 사용하는 때가 다른지, 성능의 차이가 있는지 등 서로의 차이점에 대해 궁금하여 찾아봤고, 기억하기 위해 아래에 바로 기록해 보았다.

 

useMemo vs useCallback

차이점의 핵심은 메모이제이션과 반환하는 값

useMemo
- 함수의 값만 메모이제이션해서 반환한다.
- 자식 컴포넌트에서 많은 계산량이나 복잡한 계산식의 값 등, 특정 props값들을 최적화하고 싶을 때 사용한다.

useCallback
- 함수 자체를 메모이제이션해서 반환한다.
- 부모 컴포넌트에서 많은 계산량이나 복잡한 계산식의 함수를 자식 컴포넌트에 props로 전달해줄 때 사용한다.

 

최적화는 무조건 좋다??!

이런 궁금증도 순간적으로 스쳤다. 결국 프로그램이나 웹페이지를 개발하면서 수많은 코드들이 쌓일 텐데, 전부 코드를 최적화시켜두면 해결되지 않을까?? 이 미련하고 단순한 생각을 스스로 반박하기 위해(?) 최적화 코드와 관련된 지식(useMemo, useCallback 사용 한정)을 찾아봤다. 우선 결론부터 말하면 내 생각이 틀렸다. useMemo, useCallback을 공부하면서 알게 된 점이 무엇인가? 바로 복잡한 계산과정에서 사용하면 최적화 효과를 볼 수 있다는 것이었다. 그리고 이 코드를 작성하기 위해 React에서 제공하는 hook을 호출해야 하고, 함수를 만들고, 의존성 배열을 체크하여 값을 메모이제이션하는 과정을 거친다. 이 작은 과정 하나하나가 모두 메모리에 할당되는 과정이었고, 추가적인 비용이 발생한다고 한다.

 

우리가 우스갯소리로 자주 하는 말이 있다.

'세상에 공짜가 어디 있어????'

컴퓨터는 말한다. 공짜는 없다고. 선천적으로 소모되는 컴퓨팅 자원, 개발자가 후천적으로 입력한 코드에서 소모되는 자원 등 소모되는 자원은 반드시 생기기 마련이다. 때문에 무조건적으로 최적화 코드를 작성하면 더 많은 메모리 소모를 야기할 수도 있으니 적재적소에 사용하는 판단이 중요하다!!(물론 이 판단이 쉽지 않으니 더 열심히 공부해야겠다!!)

 

정리

- useCallback는 useMemo와 마찬가지로, React의 렌더링 성능 최적화를 위해 사용하는 hook이다.

- 재계산 로직이 복잡하거나 메모리 사용이 과도할 때, useCallback를 적용하면 성능 최적화의 도움받을 수 있다.

- useCallback은 함수 자체를 메모이제이션하며, useMemo는 함수의 값만 메모이제이션하는 차이점이 있다.

반응형

'ivory's DevLog' 카테고리의 다른 글

[JavaScript] - Closure  (0) 2021.12.01
useMemo, useCallback 파헤치기 - useMemo편  (0) 2021.11.10
왕초보와 ESLint 설정해보기  (0) 2021.11.03
Redux의 흐름과 예제  (1) 2021.09.28
Redux  (0) 2021.09.27