본문 바로가기
개발/React

리액트 - React.memo, useMemo, useCallback으로 최적화하기

by 피로물든딸기 2024. 4. 7.
반응형

리액트 전체 링크

 

참고

- debounce vs throttle로 최적화하기 with useMemo (lodash-es)

- 게으른 초기화로 useState 초기화하기

 

아래 코드를 실행해 보자.

import React, { useState } from "react";

const MySpan = ({ input }) => {
  console.log("MySpan is Rendered...");
  return <span>input 0 : {input}</span>;
};

const MemoTest = () => {
  const [value0, setValue0] = useState("");
  const [value1, setValue1] = useState("");
  const [value2, setValue2] = useState("");
  const [number, setNumber] = useState(0);

  const handleChange0 = (e) => {
    console.log("change 0");
    let input = e.target.value;
    setValue0(input);
  };

  const handleChange1 = (e) => {
    console.log("change 1");
    let input = e.target.value;
    setValue1(input);
  };

  const handleChange2 = (e) => {
    console.log("change 2");
    let input = e.target.value;
    setValue2(input);
  };

  const upperCase = (input) => {
    console.log("upper case", input);
    return input.toUpperCase();
  };

  const lowerCase = (input) => {
    console.log("lower case", input);
    return input.toLowerCase();
  };

  const upperInput = upperCase(value1);
  const lowerInput = lowerCase(value2);
  const plusNumber = () => {
    console.log("plus number");
    setNumber((prev) => prev + 1);
  };

  return (
    <div
      style={{
        margin: 5,
        display: "flex",
        flexDirection: "column",
        width: 200,
      }}
    >
      <input
        type="text"
        value={value0}
        onChange={handleChange0}
        placeholder="value 0"
      />
      <input
        type="text"
        value={value1}
        onChange={handleChange1}
        placeholder="value 1"
      />
      <input
        type="text"
        value={value2}
        onChange={handleChange2}
        placeholder="value 2"
      />
      <MySpan input={value0} />
      <span>input 1 : {upperInput}</span>
      <span>input 2 : {lowerInput}</span>
      <button onClick={plusNumber}>+</button>
      <span>number : {number}</span>
    </div>
  );
};

export default MemoTest;

 

위의 코드의 기능은 다음과 같다.

 

- value0에 값을 입력하면 MySpanvalue0이 그대로 출력된다. ( const MySpan = ({ input }) => { ... } )

- value1에 값을 입력하면 input 1 :대문자로 출력된다. ( const upperInput = upperCase(value1); )

- value2에 값을 입력하면 input 2 :소문자로 출력된다. ( const lowerInput = lowerCase(value2); )

- + 버튼을 누르면 number 1을 더한다. ( const plusNumber = () => { setNumber((prev) => prev + 1); }; )

 

그러나 다음과 같이 렌더링에 의해 위의 기능과 관련된 로그모두 출력되는 것을 알 수 있다.

 

1) value0을 수정하면, change 0 로그가 출력되고,

  수정하지 않은 value1, value2와 관련된 "upper / lower case" 로그가 출력된다.

2) value1, value2를 수정하면 change 1, 2가 출력되고, value0과 관련된 "My span is ..." 로그가 출력된다.

3) + 버튼을 누르면 "plus number"가 출력되고, "upper / lower case", "My span is ..."  로그가 출력된다.

 

리액트에서 useState set 메서드로 인해 값이 바뀌는 경우, 렌더링이 일어나게 된다.

그리고 이 렌더링에 의해 해당 상태를 사용하는 컴포넌트가 모두 다시 렌더링된다.

따라서 다른 값이 변경되지 않더라도 MySpan, upperInput, lowerInput 렌더링으로 로그가 출력되고 있다.


React.memo

 

React.memo함수 컴포넌트를 메모이제이션 할 수 있다.

즉, 값의 변경이 없는 경우 이전에 렌더링된 결과를 재사용한다.

 

React.memoMySpan에 적용해 보자.

const MySpan = ({ input }) => {
  console.log("MySpan is Rendered...");
  return <span>input 0 : {input}</span>;
};

const MySpanMemo = React.memo(MySpan);

 

다음과 같이 컴포넌트를 변경한다.

  <MySpanMemo input={value0}/>

 

이제 value0이 변경될 때"MySpan is Rendered..." 가 출력된다.


useMemo

 

useMemo도 마찬가지로 메모이제이션을 이용한다.

의존성 배열의 값이 변경되지 않으면, 이전 값을 사용하게 된다. (useEffect의존성 배열과 같다)

const upperInput = useMemo(() => upperCase(value1), [value1]);

 

value1이 변경될 때만, "upper case" 로그가 출력된다.

 

useMemo 컴포넌트에도 사용할 수 있다. 

예를 들어 React.memoMySpan을 최적화 한 예시를, useMemo를 이용하여 아래와 같이 처리할 수 있다.

 const MySpanUseMemo = useMemo(() => <MySpan input={value0} />, [value0]);

 

의존성 배열value0이 있으므로, value0이 변경되지 않는다면, MySpanUseMemo는 이전 값을 사용한다.

  {/* <MySpanMemo input={value0} /> */}
  {MySpanUseMemo}

useCallback

 

useCallback은 인수로 넘겨받은 함수를 기억한다. 

즉, useMemo 함수를 반환하는 경우라면 useCallback을 사용하면 된다.

 

컴포넌트가 다시 렌더링이 될 때, 선언된 함수도 새롭게 렌더링 한다.

먼저 useState로 선언한 number로그에 추가해 보자.

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

 

디버깅을 위해 lowerCase의 로그는 임시로 지운다.

  const lowerCase = (input) => {
    // console.log("lower case", input);
    return input.toLowerCase();
  };

 

plus number에서 number가 계속 증가하는 것을 알 수 있다.

 

이제 useCallback으로 plusNumber를 선언해 보자.

의존성 배열( [ ] )이 비어 있으므로, 컴포넌트가 처음 렌더링 될 때만 함수를 만들게 된다.

  const plusNumber = useCallback(() => {
    console.log("plus number", number);
    setNumber((prev) => prev + 1);
  }, []);

 

최초 설정된 number 0만 출력되고 있다.

즉, 컴포넌트 렌더링이 발생해도, plusNumber가 다시 렌더링 되지 않았다.

 

전체 코드는 다음과 같다.

import React, { useCallback, useMemo, useState } from "react";

const MySpan = ({ input }) => {
  console.log("MySpan is Rendered...");
  return <span>input 0 : {input}</span>;
};

const MySpanMemo = React.memo(MySpan);

const MemoTest = () => {
  const [value0, setValue0] = useState("");
  const [value1, setValue1] = useState("");
  const [value2, setValue2] = useState("");
  const [number, setNumber] = useState(0);

  const MySpanUseMemo = useMemo(() => <MySpan input={value0} />, [value0]);

  const handleChange0 = (e) => {
    console.log("change 0");
    let input = e.target.value;
    setValue0(input);
  };

  const handleChange1 = (e) => {
    console.log("change 1");
    let input = e.target.value;
    setValue1(input);
  };

  const handleChange2 = (e) => {
    console.log("change 2");
    let input = e.target.value;
    setValue2(input);
  };

  const upperCase = (input) => {
    console.log("upper case", input);
    return input.toUpperCase();
  };

  const lowerCase = (input) => {
    console.log("lower case", input);
    return input.toLowerCase();
  };

  const upperInput = useMemo(() => upperCase(value1), [value1]);
  const lowerInput = lowerCase(value2);
  const plusNumber = useCallback(() => {
    console.log("plus number", number);
    setNumber((prev) => prev + 1);
  }, []);

  return (
    <div
      style={{
        margin: 5,
        display: "flex",
        flexDirection: "column",
        width: 200,
      }}
    >
      <input
        type="text"
        value={value0}
        onChange={handleChange0}
        placeholder="value 0"
      />
      <input
        type="text"
        value={value1}
        onChange={handleChange1}
        placeholder="value 1"
      />
      <input
        type="text"
        value={value2}
        onChange={handleChange2}
        placeholder="value 2"
      />
      <MySpanMemo input={value0} />
      {MySpanUseMemo}
      <span>input 1 : {upperInput}</span>
      <span>input 2 : {lowerInput}</span>
      <button onClick={plusNumber}>+</button>
      <span>number : {number}</span>
    </div>
  );
};

export default MemoTest;

React.memo, useMemo, useCallback 모두 렌더링 시, 불필요한 리렌더링을 방지하지만,

이를 위해 메모리에 이전 값을 저장하기 때문에 메모리 점유율이 높아지게 된다.

따라서 꼭 필요한 상황에 최적화를 하자.

반응형

댓글