FrontEnd/React

React Hooks - useCallback (실습포함)

NandaNanda 2024. 3. 28. 13:40

useCallbck과 useMemo의 차이점은? useCallback도 useMemo와 같이 컴포넌트 최적화를 위해 사용된다. 다만 useMemo는 결과로 return된 값을 memoization하지만 useCallback같은 경우  콜백함수 그 자체를 메모이제이션 해준다.

(잠깐 복습. 컴포넌트 최적화를 위해 사용되는 대표적 2가지 Hooks ==>> useMemo, useCallback

 두번째 인자로 배열을 취하는 훅스 useEffect, useMemo, useCallback.

useEffect는 component life cycle과 밀접하게 연관됨. 2번째 인자로 아무것도 안주면 componentDidUpdate의 역할을, 빈배열을 주면 componentDidMount의 역할을 함.

useMemo 함수의 결과값을 반환함. useCallback 함수객체를 반환)

1. useCallback과 Memoization

useCallback은 Memoization 기법으로 컴포넌트의 성능을 최적화시켜주는 도구이다.
우선 Memoization이 뭔지 간단히 알아보자.
Memoization이란 자주 사용되는 값을 받아오기 위해 반복적으로 계산을 해야한다면 이전에 이미 계산해둔 값을 캐싱해서 메모리에서 꺼내서 재사용하는 최적화기법이다. 이 때 필요할때마다 매번 계산을 하는 것이 아니라 useMemo를 통해 캐싱해둔 값을 메모리에서 꺼내와서 재사용할 수 있다.

useMemo(() => {
	return value
},[item])

이렇게 useMemo의 인자로 콜백함수를 넣어주면 이 함수가 return하는 값을 Memoization 해준다.

그렇다면 useCallback은 무엇일까?

useCallback도 useMemo와 똑같은데 대신 인자로 전달한 콜백함수 그 자체를 메모이제이션 해준다. 재사용하고 싶은 함수를 useCallback으로 감싸주면 필요한 곳에서 재사용할 수 있다.

2. useCallback 구조와 특징

useCallback은 두개의 인자를 받는다. 첫번째 인자로는 메모이제이션 해줄 콜백함수 그리고 두번째 인자는 의존성 배열이다.

const calculate = useCallback((num) => {
	return num + 1;
}, [item])

이렇게 함수를 useCallback으로 감싸주면 이제 calculate 변수는 메모이제이션 된 함수를 가지고 있게 된다. 이 메모이제이션 된 calculate 함수는 두번째 인자인 의존성 배열 내부의 값이 변경되지 않는 이상 다시 초기화되지 않는다. 만약에 의존성 배열 내부의 값이 변경된다면 그제서야 calculate 함수는 새로 만들어진 새로운 함수로 초기화 된다.

또한 함수형 컴포넌트는 렌더링이 되면 그 컴포넌트 함수가 다시 호출되기 때문에 컴포넌트 내부 모든 변수가 초기화 된다. 하지만 이때 useCallback을 사용해서 함수를 감싸서 메모이제이션을 해주면 컴포넌트가 다시 랜더링 되더라도 내부 변수가 초기화되는 것을 막을 수 있다.
그래서 컴포넌트가 맨처음 랜더링 될때만 함수 객체를 만들어서 초기화해주고 이후에는 이미 할당받은 함수 객체를 계속 가지고 있으면서 재사용 할 수 있다.

 

useCallback 실습

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

const App=()=>{
  const[number, setNumber]=useState(0);

  const someFunction=()=>{
    console.log(`some func:number ${number}`);
    return;
  };

  useEffect(()=>{
    console.log("someFunction이 변경되었습니다.")
  },[someFunction]);


  return(
    <div>
      <input type='number' value={number} onChange={(e)=>setNumber(e.target.value)}></input>
      <br/>
      <button onClick={someFunction} >Call function</button>
    </div>
  )
}
export default App;

위와 같이 코드를 구성함년 아래와 같은 화면을 얻을 수 있다.

그리고 input창에 숫자를 입력할 때마다 setNumber가 호출되어 컴포넌트 전체가 리랜더링되면서 someFunction함수는 매번 재정의 되어 그때마다 useEffect의 console문구가 콘솔창에 나타나게 된다. 이 현상을 해결하기 위해 아래와 같이 useCallback을 사용할 수 있다.

위와 같이 의존성 배열을 빈배열로 주면 맨 처음 랜더링이 일어날때만 useCallback함수가 실행되고 그 이후에는 더이상 실행되지 않는다. 그리고 someFunction이란 변수에는 함수

()=>{
    console.log(`some func:number ${number}`);
    return;
  }

이 저장된다.  그런데 문제가 있다. input창에 숫자를 올리고 이후에 버튼을 클릭하면 올라간 숫자가 출력이 되지 않는다. 왜? 기존 number=0이 함수에 들어가 있기 때문이다. 이 현상을 해결하기 위해서 useCallback훅의 의존성 배열에 number를 넣어 number가 변할때마다 반환되는 함수를 변경해 줄수 있다. 물론 이에 따라 someFunction의 값이 바뀌고 someFunction을 의존성 배열의 인자로 가지고 있는 useEffect훅이 호출된다.

 

useCallback 실습.

Q.아래 그림과 같이 사이즈와 테마를 모두 변경할 수 있는 페이지를 만들어보자. 기본적으로 주워지는 코드는 아래와 같다. 아래의 코드에서  Theme(테마)을 바꾸는 버튼을 추가하여 테마를 변경할 수 있게 한다. 그리고 테마를 변경할때가 아닌 사이즈를 변경할 때만 사이즈 변경!!이라는 내용이 콘솔에 찍히게 변경하라!

//App코드
import React, { useState } from 'react';
import Box from './components/Box';

const App=()=>{
  const [size, setSize]=useState(100);

  const createBoxStyle=()=>{
    return{
      backgroundColor:'pink',
      width: `${size}px`,
      height:`${size}px`,
    };
  };
  return(
    <div>
      <input type="number" value={size}onChange={e=>{setSize(e.target.value)}}></input>
    <Box createBoxStyle={createBoxStyle}></Box>
    </div>
  );
};

export default App;

//Box코드
import React,{ useEffect, useState } from "react";

const Box=({createBoxStyle})=>{
    const[style, setStyle]=useState({});
    useEffect(()=>{
        console.log("박스 키우기");
        setStyle(createBoxStyle());
    },[createBoxStyle]);

    return (
        <div style={style}></div>
    )
}
export default Box;

 

그런데 여기서 테마를 변경하는 테마변경 버튼을 추가해서 눌러보면 콘솔창에 박스 사이즈 변경!!이라는 콘솔이 찍히게 된다.
사이즈가 변경될 때만 useEffect가 실행되게 하려면 createBoxStyle 함수를 useCallback으로 감싸주면 된다. 그리고 두번째 인자인 의존성 배열에 size를 넣어주게 되면 사이즈가 변경되지 않는 이상 memoize 된 createBoxStyle 함수를 재사용 하기 때문에 useEffect가 다시 불리지 않게 된다.

정답코드

//App코드
import React, { useCallback, useState } from 'react';
import Box from './components/Box';

const App=()=>{
  const [size, setSize]=useState(100);
  const [isDark, setIsDark]=useState(false);

  const createBoxStyle=useCallback(  ()=>{
    return{
      backgroundColor:'pink',
      width: `${size}px`,
      height:`${size}px`,
    };
  },[size]);
 
  return(
    <div style={{background:isDark?"black":"white"}}>
      <input type="number" value={size}onChange={e=>{setSize(e.target.value)}}></input>
      <button onClick={()=>setIsDark(!isDark)}>Change Theme</button>
    <Box createBoxStyle={createBoxStyle}></Box>
    </div>
  );
};

export default App;
 
//Box코드는 그대로임

 

 

==============================================================================

useCallback에서 의존성 배열에 언제 빈배열을 주고 언제는 값을 주어야 하나?

아래 코드에는 useCallback이 사용된 change와 insert라는 함수가 있습니다.

change에는 의존성 배열이 빈 배열이고, insert에는 의존성배열에 string과 stringList가 들어가 있습니다.

 

const change = useCallback((e) => {
	setString(e.target.value); 
}, []); 

const insert = useCallback((e) => {
	const newList = stringList.slice(); 
    	newList.push(string); 
    	setStringList(newList); 
}, [string, stringList]); 

 

둘의 차이가 무엇이길래, 하나는 의존성 배열이 비어있고, 하나는 값이 포함되어 있을까요?

 

change는 두번째 인자에 빈 배열을 주었기 때문에, 최초 렌더링 시에만 함수가 생성되고 이후에는 생성되지 않고, insert는 string과 stringList가 변경될 때만 함수를 재생성합니다. 

 

즉, insert의 경우 string과 stringList가 변하면 함수도 재생성되어야 하는데, 배열로 전달받은 두 state에 의존적이라고 볼수 있습니다.

하지만 change는 state를 사용하지 않고, 단지 state를 변경하는 것이므로, state에 의존적이지 않기 때문에 state가 변함에 따라 change 함수를 재생성해줄 필요는 없어서 빈 배열을 넣어주게 됩니다.

 

정리하자면, 해당 함수에서 state를 사용한다면, 말그대로 state에 의존하는 함수이므로 두번째 인자의 배열 안에 state를 추가해줘야 하고, state에 의존적이지 않은 함수라면 빈 배열로 넣어주면 됩니다.