본문 바로가기
Front-End/React.js

state 관리 대상 줄이기를 고민하다가 => useMemo, useCallback까지

by kk님 2023. 3. 17.

useState를 사용해서 state를 관리할 때, 렌더링을 고민하게 되면서
state 변수와 관련되어 있지만
직접적으로 렌더링에 관여하지 않아도 되는 함수 혹은 변수에 관심이 생겼다.

'state와 구분해서 사용한다면 렌더링 되는 횟수를 줄일 수 있지 않을까?' 같은 의문에서 포스팅을 시작했던 것 같은데..

어떤 변수 혹은 함수를 state로 관리하지 않을 수 있을까? 모든 것을 state로 관리하지는 않아도 될텐데.
일단 index.js에서 React.StrictMode를 삭제한다. 그렇지 않으면 두번 실행되어서 값에도 영향을 미치게 된다.

//src/index.js

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

.

//App.jsx

import { Fragment, useState } from "react";
import Main from "./Components/Main";

console.log("App 컴포넌트");
export default function App() {
  const [number, setNumber] = useState(1);
  console.log(number, "inApp");

  const handleButton = () => {
    console.log(number, "handleButton");
    setNumber((prev) => prev + 1);
  };

  return (
    <Fragment>
      <button onClick={handleButton}>버튼</button>
      <Main />
    </Fragment>
  );
}

.

//Main.jsx

import { Fragment } from "react";

//1번
let x = 1;
console.log("x", x);
function Main() {
  //2번
  let y = 10;
  console.log(`렌더링 외부 변수 x:${x} 내부 변수 y:${y}`);

  return (
    <Fragment>
      <div>{(x += 1)}</div>
      <div>{(y += 1)}</div>
    </Fragment>
  );
}

export default Main;

 

1번 위치는 Main 함수 밖으로 아예 빼서 선언하고 사용하는 방법

2번 위치는 Main 함수 안에서 선언하고 사용하는 방법


*내가 생각하는 예상 시나리오*
1. 처음 화면이 렌더링 되었을 때 값 자체는 number는 1, x는 1, y 는 10, 화면에 보여지는 x는 2, y는 11

2. 버튼을 클릭해서 화면이 리-렌더링 되었을 때 number는 2, x는 2, y 는 10, 화면에 보여지는 x는 3, y는 11

실행해보니 1번 위치는 파일이 한번 생성되면, 그 이후에는 렌더링 되어도 다시 실행되지 않는다. 'x 1'과 'App 컴포넌트'에 해당하는 출력이 한번 등장한 것 외에 또 등장하지 않는다.

그렇지만 2번 위치는 리-렌더링이 발생하면 다시 실행된다. 무조건 함수를 다시 실행하기 때문인데, 결국 마지막 return문까지 재실행 된다.

그렇다면 1번 위치는 렌더링에 영향을 받지 않는 변수(또는 함수)를 따로 빼주면 좋을 것 같고 2번 위치는 렌더링 될 때마다 재실행 되기 때문에 그때마다 초기화가 필요한 변수(또는 함수), 아니면 리-렌더링되었을 때 초기화되는 state와 연관된 .. 변수또는 함수가 포함되어야 할 것 같다.


두번째 의문

그러면 함수, 객체도 다시 생성되는 걸까?

//Main.jsx
import { Fragment } from "react";

//1번
let x = 1;
console.log("x", x);
let prevFunc = null;
let prevObj = null;
function Main() {
  //2번
  let y = 10;
  const z = {};
  console.log(`렌더링 외부 변수 x:${x} 내부 변수 y:${y}`);
  const print = () => {
    console.log("print");
  };

  console.log("비교 prevFunc===print: ", prevFunc === print, prevFunc, print);
  prevFunc = print;

  console.log("비교 prevObj===z: ", prevObj === z, prevObj, z);
  prevObj = z;
  
  return (
    <Fragment>
      <div>{(x += 1)}</div>
      <div>{(y += 1)}</div>
    </Fragment>
  );
}

export default Main;

Primitive type이었기 때문에 주소값 비교가 가능했는데, 함수는 동일한 기능을 하고 마찬가지로 객체도 객체끼리는 동일하지만, 비교문에서는 둘다 모두 false이다.

 

그러면 useMemo와 useCallback을 좀 이해할 수 있을 것 같다.. 이젠 이해 해야해

1. useMemo

useMemo는 함수를 실행 후 그 return된 값을 재사용한다. 계산이 복잡한 경우에 보통 쓰인다.

1번 위치에 함수를 선언했다고 하더라도, getFunc()를 통해 함수값을 다시 사용한다면 결국 연산 시간에 영향을 미칠 수 있다. 만약 해당 함수가 state에 영향을 받고 있다고 한다면 useMemo를 통해 값을 재사용 할 수 있다.

봉-인! 아직 안써요!

느낌임.

2. useCallback

useCallback은 state가 바뀌었을 때만 함수를 생성한다.

렌더링을 최적화 하기 위해 useCallback을 써서 함수를 다시 생성한다

이.. 설명은.. 좀 이상한.. 느낌인데,

'왜 최적화 하기 위해 함수를 다시 생성하지?!'

 

아니 최적화.. 다시 생성하지 않으려면 그냥 1번 위치에 함수를 선언하면 안되나?

라고 생각했는데. 보면 state가 바뀌었을 때 함수를 생성한다 는 설명이 있다.

그러면 이 함수는 state에 영향을 받는 함수라는 의미이고, state는 Main() 함수 안에서만 선언, 할당이 가능하다.

import { useState } from "react";

function Main(){
    const [state, setState] = useState(1)
    const exFunc = () => {
        setState((prev)=>prev+1)
    }
    return(<div>{state}</div>)
}
export default Main;

아래는 불가능. 'setState' is not defined.' ESLint 에러를 확인할 수 있다.

import { useState } from "react";

const exFunc = () => {
    setState((prev)=>prev+1)
}
function Main(){
    const [state, setState] = useState(1)
    return(<div>{state}</div>)
}
export default Main;

그러면 Main 함수 안에 함수를 선언하고 useCallback을 사용한다면 결과는 어떻게 될까

import { useCallback, useState } from "react";

let prevFunc = null;
function Main() {
  const [state, setState] = useState(1);
  const exFunc = useCallback(() => {
    setState((prev) => prev + 1);
  }, []);

  console.log("prevFunc===exFunc?", prevFunc === exFunc, prevFunc, exFunc);
  prevFunc = exFunc;
  return <div>{state}</div>;
}
export default Main;

true

위에서 실험한 내용을 포함해서 생각해보면 일단 1번 위치에서 선언할 수 없는 경우 useCallback을 해주면 함수를 다시 생성해서 할당하지 않는다.
'렌더링을 최적화 하기 위해 useCallback을 써서 함수를 재사용한다.'가 더 내가 이해하기 쉬운 표현인 것 같다.

 

1번 위치에서 선언할 수 있는 함수인 경우, useMemo를 사용해 결괏값을 재사용할 수 있고
1번 위치에서 선언할 수 없는 함수인 경우, useCallback을 사용해 함수를 재사용할 수 있다.
(1) 최대한 함수 바깥으로 뺄 수 있는지 생각하기
(2) 바깥으로 빼지 못하면 재생성을 막는 방법 생각하기 => 재사용하는 방법 (useCallback)
(3) 함수 호출시 재호출을 막는 방법 생각하기 => 반환값을 재사용 하는 방법 (useMemo)
이렇게 일단 정리해볼 수 있을 것 같다.
아직 잘 이해하지 못한건, useCallback에서 의존성 배열이 들어가는 경우 함수를 재생성하는 부분인데.. 좀 더 생각해봐야 할 것 같다.

'Front-End > React.js' 카테고리의 다른 글

state 란? 무엇인가 . . .🙄  (0) 2023.03.22
React.memo 실험  (0) 2023.03.17
useState의 setState((prev)=> !prev)  (0) 2023.03.14
커스텀 훅  (0) 2023.03.14
프로젝트 디렉토리 구조  (0) 2023.03.13