useCallback : 함수를 재사용 하는 Hook

hzn·2022년 11월 27일
1

React

목록 보기
9/15
post-thumbnail

useCallback

  • 메모이제이션 기법을 이용한 Hook
  • useMemo가 값의 재사용을 위해 사용하는 Hook이라면, useCallback은 함수의 재사용을 위해 사용하는 Hook이다.
  • 컴포넌트가 리렌더링 되더라도 함수가 의존하고 있는 값들이 바뀌지 않았다면, 함수를 메모리 어딘가에 저장해뒀다가 다시 꺼내 쓸 수 있다.
    (재렌더링을 막는 것이 아니라 재렌더링 되더라도 함수가 새로 만들어지는 것을 막는 것!)

useCallback을 사용하지 않았을 때

function Calculator({x, y}){
			const add = () => x + y;
		return <>
    		  <div>{add()}</div>
  			   </>;
}
  • Calculator 컴포넌트 내에 add라는 함수가 선언돼 있음
  • add 함수는 props로 넘어온 xy의 값을 더해 <div> 태그에 값을 출력하고 있다.
  • add 함수는 해당 컴포넌트가 렌더링이 될 때마다 새롭게 만들어진다.

useCallback을 사용했을 때

import React, { useCallback } from "react"; // useCallback 불러오기

function Calculator({x, y}){
		const add = useCallback(() => x + y, [x, y]);
 		return <>
    		    <div>{add()}</div>
  				</>;
}
  • useCallback Hook을 사용하면 그 함수가 의존하는 값들(xy)이 바뀌지 않는 한 기존 함수를 계속해서 반환.
  • xy의 값이 이전과 동일하다면 다음 렌더링 때 이 함수를 다시 사용
  • useCallback은 useMemo에 비해 괄목할 만한 최적화를 느낄 수 없다. 함수를 호출하지 않는 Hook이 아니라 그저 메모리 어딘가에서 함수를 꺼내서 호출하는 Hook이기 때문.
  • 따라서 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있다.
  • 이보다는 🌟
    자식 컴포넌트의 props로 함수를 전달해줄 때
    🌟 useCallback을 사용하는 것이 좋다.

useCallback과 참조 동등성

  • useCallback은 참조 동등성에 의존한다.
  • 리액트는 JavaScript 언어로 만들어진 오픈소스 라이브러리기 때문에 기본적으로 JavaScript 문법을 따라간다.
  • JavaScript에서 함수는 객체이다. 객체는 메모리에 저장할 때 값을 저장하는 게 아니라 값의 주소를 저장하기 때문에 반환하는 값이 같을지라도 일차연산자로 비교했을 때 false가 출력된다.
    function doubleFactory(){
       return (a) => 2 * a;
    }
    
    const double1 = doubleFactory();
    const double2 = doubleFactory();
    
    double1(8); // 16
    double2(8); // 16
    
    double1 === double2;  // false
    double1 === double1;  // true
  • double1double2는 같은 함수를 할당했음에도 메모리 주소 값이 다르기 때문에 같다고 보지 않는다.
  • useCallback을 사용하지 않으면 리렌더링 시 함수를 새로 만들어 호출한다. (이전의 함수와는 다른 주소값을 가진 함수)
  • 반면 useCallback을 이용해 함수를 저장했다가 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용하는 것과 같다.
  • 따라서 리액트 컴포넌트 함수 내에서 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있다.

useCallback 예제

useCallback 사용 안 한 경우

  • App.js
import { useState } from "react";
import "./styles.css";
import List from "./List";

export default function App() {
  const [input, setInput] = useState(1);
  const [light, setLight] = useState(true);

  const theme = {
    backgroundColor: light ? "White" : "grey",
    color: light ? "grey" : "white"
  };

  const getItems = () => { // 재렌더링 될 때마다 함수 새로 만들어짐
    return [input + 10, input + 100];
  };

  const handleChange = (event) => {
    if (Number(event.target.value)) {
      setInput(Number(event.target.value));
    }
  };

  return (
    <>
      <div style={theme} className="wall-paper">
        <input
          type="number"
          className="input"
          value={input}
          onChange={handleChange}
        />
        <button
          className={(light ? "light" : "dark") + " button"}
          onClick={() => setLight((prevLight) => !prevLight)} // 1. 버튼 누르면 light(state) 변경돼서 재렌더링

          {light ? "dark mode" : "light mode"}
        </button>
        <List getItems={getItems} /> // 자식 컴포넌트(List)에  함수 prop으로 내려줌
      </div>
    </>
  );
}

  • List.js (input에 따라 변하는 숫자 리스트..)
import { useState, useEffect } from "react";

function List({ getItems }) {
  const [items, setItems] = useState([]); // items라는 state 선언(초기값은 [] => 아래 useEffect 최초 실행으로  getItems()로 state 값 바뀜)

  useEffect(() => {
    console.log("아이템을 가져옵니다.");
    setItems(getItems());
  }, [getItems]); // getItems(의 주소값)가 바뀔 때마다 실행 // 현재는 light/dark mode 버튼 눌러도 재렌더링 되어 getItems 함수가 새로 만들어지므로 그때마다 이 useEffect 구문이 실행된다. 

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

export default List;
  • 자식 컴포넌트 <List>에 prop으로 함수 getItems를 전달해서 useEffect의 의존성 배열의 요소로 쓰고 있음.
  • 현재는 light/dark mode 버튼 누르면 재렌더링 되어 getItems 함수가 새로 만들어지므로 (= 새로운 주소값으로 바뀌므로) 그때마다 이 useEffect 구문이 실행된다.

useCallback 사용한 경우

  • App.js
import { useState, useCallback } from "react"; // useCallback 불러오기
import "./styles.css";
import List from "./List";

export default function App() {
  const [input, setInput] = useState(1);
  const [light, setLight] = useState(true);

  const theme = {
    backgroundColor: light ? "White" : "grey",
    color: light ? "grey" : "white"
  };

  const getItems = useCallback(() => { 
    return [input + 10, input + 100];
  }, [input]); // useCallback 사용 => input 값이 변하지 않으면 재렌더링 돼도 getItems 함수는 계속 같은 주소값. input 값이 변하는 경우(input 창에서 숫자 바꾼 경우)만  getItems 함수가 새로 만들어진다. 

  const handleChange = (event) => {
    if (Number(event.target.value)) {
      setInput(Number(event.target.value));
    }
  };

  return (
    <>
      <div style={theme} className="wall-paper">
        <input
          type="number"
          className="input"
          value={input}
          onChange={handleChange}
        />
        <button
          className={(light ? "light" : "dark") + " button"}
          onClick={() => setLight((prevLight) => !prevLight)} // 1. 버튼 누르면 light(state) 변경돼서 재렌더링

          {light ? "dark mode" : "light mode"}
        </button>
        <List getItems={getItems} /> // 자식 컴포넌트(List)에  함수 prop으로 내려줌
      </div>
    </>
  );
}

  • List.js (input에 따라 변하는 숫자 리스트..)
import { useState, useEffect } from "react";

function List({ getItems }) {
  const [items, setItems] = useState([]); // items라는 state 선언(초기값은 [] => 아래 useEffect 최초 실행으로  getItems()로 state 값 바뀜)

  useEffect(() => {
    console.log("아이템을 가져옵니다.");
    setItems(getItems());
  }, [getItems]); // getItems(의 주소값)가 바뀔 때마다 실행 // light/dark mode 버튼 눌러서 재렌더링 되어도 getItems 함수가 같은 주소값이기 때문에 실행되지 않는다. (input 창에서 숫자 바꿀 경우에만 getItems가 새로 만들어져서 새로운 주소값을 내려받기 때문에 useEffect 구문이 실행된다.  

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

export default List;

0개의 댓글