조금조금 REACT, Rendering(최적화) - Hook : useMemo, React.Memo, useCallBack

Edwin·2023년 3월 4일
0

조금조금 REACT

목록 보기
15/31
post-thumbnail
  • 본 포스트는 별코딩님의 강의를 기반으로 요약정리되었습니다.

공식문서에서의 useMemo 설명

리액트 공식문서 : useMemo는 의존성이 변경되었을 때에만 메모이제이션된 값만 다시 계산 할 것입니다. 이 최적화는 모든 렌더링 시의 고비용 계산을 방지하게 해 줍니다.

useMemo의 기본코드는 다음과 같다.

  const useMemoName = useMemo(()=>{
    return }, [의존성배열])
  1. 위에서 볼 수 있듯이 useMemo를 사용하는 환경은 [의존성배열]의 값이 변경되었을 때이다. 이를 통해서 중복되는 state에서 하나만 변경되었을 때 다른 하나의 변경이 함께 되지 않도록 조율함으로, 발생되는 비용문제를 해결할 수 있도록 처리하는 방식이 useMemo를 사용하는 이유이다.
  2. 동작방식은 메모리에 값을 저장해 두었다. [의존성 배열]이 변경되었을 때 이를 가져와서 사용하는 것으로, 즉 캐싱이라 부르는 과정을 통해서 state를 관리하는 것을 말한다.

바로 예제를 살펴보자. 예제는 2가지로 나눠진다. 첫째는 불변성이 유지되는 원시타입의 state를 다룰 때와, 둘째는 불변성이 유지되지 않는 객체 타입의 state를 다룰 때이다.

이러한 이유는 함수형 컴포넌트의 배경이 함수라는 점 때문이다. 함수는 해당 과정이 다시 실행되면 해당과정에서 담고 있었던 고유의 값을 상실하고 초기화시키기 때문이다. 이를 방시하기 위해서는 값을 어딘가에 저장했다가 가져올 필요가 발생되기 때문이다.

1) 원시 타입의 state를 관리하기

	const [hardNumber, setHardNumber] = useState(0)
	const [easyNumber, setEasyNumber] = useState(0)

위와 같은 두개의 state를 설정해 두었다고 하자. 버튼을 통하여 각각의 state를 변경할 것인데, harrNumber를 변경해 줄 onclick 함수는 답답한 계산기이다. 왜 답답한 계산기냐면 계산할 때 5억번의 생각을 하기 때문이다. 5억번의 생각이라면 엄청 느릴 것이다. 즉 화면에 리렌더링되어 정보를 반영하기까지 오랜 시간이 걸리다는 것이다. 이 말을 아래에서 살펴보자.

const hardCalurate = (number) => {
  console.log('답답한 계산기');
  for (let i=0; i<500000000; i++) {} // 단순하게 반복문이 5억번 실행될 때까지 기다리는 시간이라고 하자. 
  return number+10000
}
const hardSum = hardCalurate(hardNumber)


const easyCalurate = (number) => {
  console.log('빠른 계산기');
  return number+1
}
const easySum = easyCalurate(easyNumber)

반면에 easyNumber의 상태를 변경하는 onclick 함수는 빠른 계산기이다. 답답하게 5억번의 생각을 반영하지 않고, 즉시 일을 처리한다.

그런데 위의 두개의 계산기에서 어느 하나라도 state가 변경된다면, 해당 컴포넌트는 다시 화면을 리렌더링 하게 된다. 그런데 여기에서 문제가 발생된다. 바른계산기를 실행시킨다 할지라도, 함수형 컴포넌트라는 특성에 따라서 기록된 모든 함수를 다시 실행시키기 때문에 빠른 아이라도 답답한 친구가 자신의 일을 할동안을 기다려주어야 하기 때문이다. 이는 렌더링을 느리게 만든다는 것임으로 별도의 조치가 필요하게 되었고, 여기에서 useMemo가 등장하게 되었다.

방법은 간단하다. 메모리에 hardNumber의 값을 저장해 두었다가. 해당 값이 변경되었을 때에만 답답한 친구가 일할 때를 가르쳐 주는 것이다.

  const hardSum = useMemo(()=>{
    return hardCalurate(hardNumber)
  }, [hardNumber])

빠른 친구는 그대로 두고, 답답한 친구에게만 useMemo를 적용시켰다. 이를 통해서 state 변경에 따른 적용은 다르게 될 것이다. 함수형 컴포넌트가 리렌더링 될 때 두 개의 함수가 함께 사용되는 것이 아니라, 답답한 친구는 해당 state의 값이 변경 될 때에만 실행되도록 쉬게 만드는 것이다. 즉 해당 함수는 [의존성배열]이 변경되었을 때에만 이 답답한 친구를 실행시킬 것이다.

그 결과 빠른 계산기를 실행시키더라도, 답답한 계산기를 할 일이 없기 때문에 쉬고 있는 것이다. 이후에 반약 답답한 계산기의 조건인 [의존성배열]이 변경이 되었을 때에만, 이 답답한 친구는 일을 하게 될 것이다. 비록 5억번의 생각을 하게 되어 느리게 실행되겠지만, 이 친구는 자신의 일을 함수형 컴포넌트가 리렌더링 될 때 실행되는 것이 아니라 자신의 일을 수행해야 될 때에만 실행될 것이다.

여기서 답답한 친구라고 설명했지만 개발의 과정에서 보면, 무거운 단위의 실행이 오래걸리는 함수를 설정할 때가 있을 것이다. 만약 이렇게 useMemo를 설정하지 않으면, 화면이 리렌더링 될 때마다 화면은 힘들어할 것이고, 화면이 힘들어한다는 것을 결국 비용문제와 연결된다. 그만큼 많은 일을 할당시켰기 때문이다.

이럴 때, useMemo를 사용하여 극적인 효과를 얻을 수 있지만, 이 역시 필요한 경우에만 사용해야 한다. 결국 useMemo도 메모리를 사용하여 저장된 값을 캐싱해오기 때문이다. 즉 필요한 경우에 적절하게 사용함으로 렌더링 최적화라는 주제를 얻어보자.

2) 객체 타입의 state를 관리하기

위에서는 state가 원시타입이었을 때였다. 그런데 객체라면 내용이 달라진다. 아래의 예시에서 살펴보자.

  const [isKorea, setIsKorea] = useState(true);
  const location = {country : isKorea ? "한국" : "외국"};

isKorea의 상태(trur : false)에 따라서 location식별자는 값을 다르게 대입받을 것이고, 대입 받은 값을 전달할 것이다.

위에서 보면 특별한 설정이 없이는 두 개의 질문은 하나라도 state가 변경되면 전부 화면을 리렌더링 하기 위해서 각각의 state를 다시 적용하기 위하여 일을 할 것이다. 그런데 이를 분리할 수 없을까?에 대한 부분이 지금 공부하고 있는 useMemo에 대한 부분이다. 그런데 이번에는 적용해야 되는 값이 객체인 것이다.

위의 이미지는 useEffect를 통해서 location의 정보가 변경되었을 때를 확인하기 위해서 콘솔에 정보를 실행하도록 한 것이다. location의 정보를 위와 같이 다루고 싶은 것이다. 즉 location을 결정하는 isKorea의 상태가 변경되기 전까지 location은 언제나 현재의 상태를 유지하고 싶은 것이다. isKorea의 상태가 변경되면 위의 사례에서 true->false로 변경되었을 때, location에 대입될 값을 변경해주고 싶다면, 캐싱을 활용하는 것이다.

const location = useMemo(()=>{
    return   {
      country : isKorea ? "한국" : "외국",
    };
  },[isKorea])

location의 값은 이제 isKorea이 변경되었을 때에만, 저장되어 있던 location의 값을 변경하여 전달해줄 것이다. 즉 하나의 state가 변경되었을 때 동시에 실행되는 것이 아니라 해당 값이 변경되었을 때에만 실행을 함으로 불필요한 렌더링의 반복을 지양하고, 비용을 절감시켜주는 것이다.

이와 같이 리렌더링을 제어하는 최적화 HOOK은 2가지가 더 있다.

위의 최적화 방법은 value가 변경되었을 때이다.

3) 컴포넌트가 변경되었을 때, React.Memo

  • 출처 : 스파르타코딩클럽 - 항해99 강의안

내용은 이러하다. 상위 컴포넌트 1번이 리렌더링 되는 과정에서 하위컴포넌트 6번은 변경된 내용이 없다. 그런데, 이를 다시 반복한다는 것을 불필요한 일이 될 것이다. 컴포넌트 단위의 캐싱을 진행할 때 사용되는 것이 React.Memo이다. 그런데 해당 Hook은 리액트 자체 기능이기 때문에 상단에 React가 임포트 되어 있다면, 추가적인 임포트를 실행하줄 필요가 없다는 말이다.

import React from 'react'

하위 컴포넌트가 임포트 되는 상황에서 값을 변경해 주면 된다.

// 컴포넌트 단위의 캐싱이 불필요한 경우
export default Components;

// 컴포넌트 단위의 캐싱이 필요한 경우
export default React.memo(Components);

3) 함수가 변경되었을 때, useCallback

Callback 이름은 익숙한 이름이다. 그렇다면 useCallback는 언제 사용할 수 있게 되는 것일까? 힌트는 이름에 있다. 바로 함수 단위에서 변경이 이뤄졌을 때 해당 함수가 리렌더링 시에 동작하도록 설정할 수 있는 것이 해당 Hook을 사용하는 이유이다.

숫자를 증가시키는 state 변경과, 숫자를 감소시키는 state 변경의 상황은 이제 수월하다. useState를 onChange에 따라서 변경시켜주면 되기 때문이다. 위의 예제는 하위컴포넌트로 만든 하위컴포넌트에서 초기화 버튼을 눌렀을 때 동작하는 함수에 대해서 설정했다고 하자.

// UseCallBack 컴포넌트
function UseCallBack() {
  const [count, setCount] = useState(0);
  const initCount = () => {
    setCount(0);
  };

	return (
  ...
   <Box1 initCount={initCount}/>
  ...)
}
export default UseCallBack;


// Box1 컴포넌트
function Box1({ initCount }) {
	console.log("Box1이 렌더링되었습니다.");
	const onInitButtonClickHandler = () => {
    initCount();
  };
    return (
    <div style={boxStyle}>
      <button onClick={onInitButtonClickHandler}>초기화</button>
    </div>
  );
}

export default React.memo(Box1)

export default React.memo(Box1)의 컴포넌트 단위를 React.memo 했더라도, 해당 하위 컴포넌트가 리렌더링되며 동작하는 것을 볼 수 있다. 이는 상위 컴포넌트에서 전달하는 props 때문이다.

화면이 리렌더링 되는 조건에는 props 가 변경되더라도를 포함한다. 즉 하위컴포넌트는 변경되지 않더라도, const [count, setCount] = useState(0);를 통해서 count가 변경되며 이때, 상위컴포넌트 전체가 리렌더링 되면서, const initCount의 메모리값도 변경되기 때문에 즉 하위컴포넌트가 실행되는 것이다. 이러한 경우를 제어하고자 사용하는 것이 useCallBack HOOK이다.

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

위와 같이 설정하면, initCount의 주소값은 컴포넌트가 처음 실행되었을 때의 값을 유지하고 있게 되는 것이고, 하위컴포넌트(Box1)는 해당 동작이 실행되었을 때에만 동작을 수행하게 되는 것이다.

버튼을 통해서 state의 값을 변경하더라도, const initCount는 동작하지 않을 것이고, 그 결과 props로 전달될 하위컴포넌트(Box1)는 할 일이 없기에 처음에 렌더링된 상태에서 기다리고 있을 것이다. 그러다가 함수가 호출되면 그때에서야 실행될 것이다. 즉 당시의 상황을 기억하고 있다가 상태를 변경한다는 것이다. 그것을 어떻게 알 수 있냐면 아래와 같이 실행해보자.

  const initCount = useCallback(() => {
    console.log(`${count}에서 0으로 변경되었습니다`)
    setCount(0);
  },[]);

count를 6으로 만든 상태에서 초기화를 눌려보자.

6에서 0으로 변경된 것을 기대했는데 그러지 못한 것은, initCount가 처음 렌더링 되었을 때 기억하고 있는 count(state)의 값이 0이기 때문이다. 만약 해당 로직이 바르게 동작하고 싶을 때는 [의존성 배열]에 [count]를 넣어주면 될 것이다. 즉 count가 변경될 때마다 initCount는 리렌더링 되며, Box1을 리렌더링 시킬 것이고, 함수를 사용할 준비를 계속 대기하며 있을 것이다.

이런 식으로 렌더링 최적화의 주제들 다뤄보았다. 개념은 이해했지만, 해당 과정이 실제 개발환경에서 어떻게 동작하는지는 또 다른 문제이기 때문에 그때가 되면 다시 이 자리로 돌아와야 할 것 같다.

Editor. EDWIN
date. 23/03/04

profile
신학전공자의 개발자 도전기!!

0개의 댓글