React - 숙련 : 최적화를 위한 도구들 - 2. useCallback

최문길·2023년 12월 19일
0

react

목록 보기
9/14

React.memo는 컴포넌트를 메모이제이션하였다면,

useCallback은 인자로 들어오는 함수 자체를 기억(메모이제이션) 한다.

Q . 근데 함수를 메모이제이션 할 필요가 있을 까??

A .

  • 컴포넌트가 호출 될 때마다 컴포넌트 안에 있는 함수도 다시 새로 그려진다.
  • 비동기 통신을 하는 함수가 들어있는 컴포넌트가 계속해서 화면에 보여진다면 비동기 통신 함수도 새롭게 그려지며 의도치 않게 호출이 되는 상황이 야기 될수가 있는 문제점이 있다.

위의 이유로 ( 내 생각임... 틀리면 feedback 주세요 )
useCallback으로 함수 자체를 memoization하는 것이 필요하다고 생각 된다.

useCallback 어떻게 사용할까

import {useCallback} from "react"
useCallback(()=>{
  ... // 이 안에 memoization할 함수를 넣어주세요
},[])// 의존성 배열인데 함수의 상태를 새롭게 그려줄 녀석입니다.

useCallback 사용해보기

CASE 1

// App.jsx
function App () {
console.log("App 컴포넌트가 렌더링되었습니다!")
 const [count, setCount] = useState(0);
  
  // 1을 증가시키는 함수
  const onPlusButtonClickHandler = () => {
    setCount(count + 1);
  };

	// count를 초기화해주는 함수
  const initCount = () => {
    setCount(0);
  };

  return (
    <>
      <h3>카운트 예제입니다!</h3>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>

      <div>
        <Box1 initCount={initCount} />
        <Box2 />
        <Box3 />
      </div>
    </>
  );
}

...

Box1.jsx

...

function Box1({ initCount }) {
  console.log("Box1이 렌더링되었습니다.");
  const onInitHandler = () => {
    initCount();
  };

  return (
    <div style={boxStyle}>
      <button onClick={onInitHandler}>초기화</button>
    </div>
  );
}


export default React.memo(Box1)

+ 버튼을 누르고 난 후 초기화 버튼을 누를 때 모두

App 컴포넌트와 Box1 컴포넌트가 리렌더링 되는 것을 보는데,

:????: 뭐지?? React.memo를 통해서 Box1.jsx는 메모이제이션을 했는데....

이유는 App.jsx가 Box1.jsx에서 초기화 버튼을 누름으로서 리렌더링이 시작되고

props로 전달 받은

function Box1({ initCount }) {
  const onInitHandler = () => {
    initCount();
  };
  .
  .
  ...
}

onInitHandler 함수 코드가 다시 만들어지기 때문이다.

자바스크립트에서는 함수도 객체의 한 종류이다.
따라서 모양은 같더라도 다시 만들어지면 그 주솟값이 달라지고 이에 따라 하위 컴포넌트인 Box1.jsxprops가 변경됐다 고 인식하여서 Box1 컴포넌트가 리렌더링 일어난다.

useCallback을 사용해서

// App.jsx에서

// 변경 전
const initCount = () => {
  setCount(0)
}

// 변경 후 

const initCount = useCallback( () => {
  setCount(0)
},[])

이렇게 하면 Box1.jsx 컴포넌트는 리렌더링이 일어나지 않게 된다.

More

count를 쵝화 할 때, 콘솔 을 찍어보면

// count를 초기화해주는 함수
const initCount = useCallback(() => {
  console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
  setCount(0);
}, []);
...

현재 count가 7일 때 [초기화] 버튼을 누를 꺼니까 콘솔에는 7에서 0으로 가 보여야하는 것이 내 예상이였지만

허허허 ... 당황 스럽다.

저렇게 위의 사진처럼 0에서 0으로 변경되는 이유는, useCallbackcount가 0일 때의 시점을 기준으로 메모리에 함수를 저장했기 때문이다.

이 때문에 어쩐지 useCallback의 두 번째 인자값으로 [] dependency array가 들어간다고 했었다 .

// initCount가 무엇의 의존해서 새롭게 그려지게 할건지를 생각해 본다면 
//count의 값이 변할 때마다 새롭게 그려져야 한다 .
const initCount = useCallback(() => {
  console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
  setCount(0);
}, [count]); // dependency 배열에 count

내 예상대로 흘러가게 된다.

CASE 2

컴포넌트 안에서 API를 호출하는 코드를 fetchTodo 함수를 만들어주고

useEffect 안에서 호출을 할 때 dependecny array에 fetchTodo라는 함수가 변경 될 때만 호출되도록 로직을 짠다면
함수도 자바스크립트에서 1급 객체이므로 useEffect 안에서 fetchTodo가 호출되어 reRendering 이 일어나면 다시 새롭게 fetchTodo함수가 그려지게 되고 그렇다면 useEffect는 다시 호출되고 이렇게 무한루프에 빠지게 된다....

function App() {
  const [todos,setTodos] = useState(null);
  const fetchTodo = async () =>{
    const {data} = await axios.get('http://localhost:4000/todos')
    setTodos(data)
  }
}
useEffect(()=>{
  fetchTodo()
},[fetchTodo]) 
//리렌더링 될때마다 fetchTodo자체가 새롭게 그려지므로 
// useEffect가 무한대로 실행된다...

이 때 해결 할 수 있는것이 useCallback으로 함수를 캐싱하는 것이다.

function App() {
  const [todos,setTodos] = useState(null);
 const fetchTodo = useCallback(()=>{
  const {data} = axios.get("http://localhost:4000/todos")
  setTodos(data)
 },[todos])
 
useEffect(()=>{
  fetchTodo()
},[fetchTodo]) 

useCallback의 dependancy array안에 todos가 바뀌면 새롭게 호출하게 끔 하면 무한루프가 해결이 된다.

0개의 댓글