[TIL] 2022-06-01 / 53일차 (React - Hook 3)

미음제

·

2022. 6. 1. 19:25

https://ko.reactjs.org/

 

오늘 배운 내용

 

useMemo

 

 

Hooks API Reference – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

useMemo는 최적화를 위해 사용하는 Hook이다. 컴포넌트가 리 렌더링 된다는 것은 Function 컴포넌트를 호출하는 것과 동일하다. 만약 리 렌더링이 여러 번 일어난다는 것은 함수가 매번 다시 실행되고 내부의 선언된 변수와 함수가 그때마다 다시 선언되고 실행된다는 의미다.

 

그렇다면 리 렌더링(re-rendering)은 언제 발생하는가?

 

  • 함수 컴포넌트는 자신의 state가 변경될 때 re-rendering 한다.
  • 부모 컴포넌트로부터 받는 props가 변경될 때 re-rendering 한다.
  • 부모 컴포넌트의 state가 변경될 때 re-rendering 한다.

연산 속도가 느리고, 컴포넌트가 비대한 경우 re-rendering이 자주 발생하면 많은 성능 소비로 이어지게 된다.

 

이때, 두 번 연산을 하지 않기 위해 useMemo를 사용하고, 최적화를 위해 사용하는 Hook이라고 하는 이유이다.

memo는 memoization에서 유래

 

import { useState } from 'react';

function sum(n) {
  console.log('sum function start');
  let result = 0;
  for (let i = 0; i<=n; i++) {
    result += i;
  }
  console.log('sum function end');

  return result;
};

const ShowResult = ({ tmpStr }) => {
  console.log('ShowResult');
  const result = sum(100);
  return (
    <span>
      Sum : {result}, {tmpStr}
    </span>
  )
};

function App() {
  const [tmpStr, setTmpStr] = useState('State : ');

  return (
    <div>
      {tmpStr}<br />
      <ShowResult tmpStr={tmpStr}/>
      <button onClick={() => setTmpStr(tmpStr + '!')}>
        click here
      </button>
    </div>
  );
}

export default App;

 

부모 컴포넌트인 App 컴포넌트에서 ShowResult 컴포넌트와 버튼을 추가해주었다. 버튼을 클릭하게 되면, 부모 컴포넌트의 tmpStr state에 '!'를 추가하도록 설정되어 있다.

 

하위 컴포넌트인 ShowResult 컴포넌트에서는 부모 컴포넌트의 tmpStr을 prop으로 받아온다.

 

ShowResult 컴포넌트 내부에서는 부모 컴포넌트의 tmpStr과 sum 함수의 결과로 result를 사용한다. 부모 컴포넌트의 prop tmpStr이 변경될 때마다 re-rendering 되고 sum 함수도 재 실행되는 것을 확인할 수 있다.

 

 

(원래 한 번씩 출력되어야 하는데, 왜 두 번 찍히는지는 모르겠다.. 두 번의 결과를 한 번으로 감안하고 보아야 한다..)

 

버튼을 클릭하게 되면 부모 컴포넌트의 state가 변경되고 이에 따라 자식 컴포넌트도 re-rendering 되면서 sum 함수도 재 실행된다. 그러나 sum 함수의 합계는 변함이 없는데 부모 컴포넌트의 prop이 변경되었다는 점에서 sum 함수도 매번 재 실행되는 것이다. result는 똑같은데 매번 계산을 해야 한다니 매우 불합리하다.

 

const ShowResult = ({ tmpStr }) => {
  console.log('ShowResult');
  // const result = sum(100);
  const n = 100;
  const result = useMemo(() => sum(n), [n]);
  return (
    <span>
      Sum : {result}, {tmpStr}
    </span>
  )
};

 

showResult 컴포넌트에서 result 값을 useMemo로 계산된 값으로 받는다. 

 

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

 

생성 함수와 의존성 값을 배열로 넘겨주게 된다. 배열 의존성의 값이 변경된 경우에만 함수를 다시 실행한다. 그렇지 않으면 한번 계산을 하고, 그 값을 메모이제이션한다.

 

 

그 결과로 전과 동일하게 버튼을 두 번 클릭했더라도 sum 함수가 재실행되지 않는 것을 확인할 수 있다. n의 값이 변경이 되면 새로운 합계를 구해야 하니 그때만 실행을 하고, 그렇지 않은 경우는 초기 렌더링 되기 전에 계산해둔 값을 기억해두고 값을 반환하게 되는 것이다.

 

React.memo()

 

 

React 최상위 API – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

다시 글의 서두에 작성된 리 렌더링 조건을 보자.

 

그렇다면 리 렌더링(re-rendering)은 언제 발생하는가?

 

  • 함수 컴포넌트는 자신의 state가 변경될 때 re-rendering 한다.
  • 부모 컴포넌트로부터 받는 props가 변경될 때 re-rendering 한다.
  • 부모 컴포넌트의 state가 변경될 때 re-rendering 한다.

 

부모 컴포넌트로부터 받은 props가 변경되면 re-rendering 되는 것은 당연하다. 그러나 전혀 관련 없는 부모 컴포넌트의 state가 변경되었을 때 발생하는 re-rendering도 매우 불합리해 보인다.

 

부모 컴포넌트의 state와 관련 없이 동작하게 할 수 있는 것이 React.memo()이다.

 

import { useState } from 'react';

const Box = () => {
  console.log('Rendering Box Component');
  const style = {
    width: 100,
    height: 100,
    backgroundColor: 'skyblue'
  };
  return (
    <div>
      <div style={style} />
    </div>
  );
}

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>+</button>
      <Box />
    </div>
  );
}

export default App;

 

App 컴포넌트에서 count state가 존재하고, 버튼을 클릭하면 count state를 변경하는 구조이다. 그리고 하위 컴포넌트 Box 컴포넌트는 부모로부터 아무런 prop을 받지 않는다.

 

최초 렌더링

 

버튼 클릭 후

 

prop으로 넘겨받은 값이 없음에도 하위 컴포넌트 Box 컴포넌트가 두 번 re-rendering 되는 것을 확인할 수 있다.

 

import React, { useState } from 'react';

const Box = React.memo(() => {
  console.log('Rendering Box Component');
  const style = {
    width: 100,
    height: 100,
    backgroundColor: 'skyblue'
  };
  return (
    <div>
      <div style={style} />
    </div>
  );
})

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>+</button>
      <Box />
    </div>
  );
}

export default App;

 

React.memo()를 통해 하위 컴포넌트가 렌더링 되는 함수를 감싸주었다. 그리고 동작을 그 전과 동일하게 버튼을 두 번 클릭하면 다음과 같은 결과가 나온다.

 

버튼 클릭 후

 

두 번 버튼을 클릭했음에도 하위 컴포넌트가 re-rendering 되지 않았다.

 

const MyComponent = React.memo(function MyComponent(props) {
  /* props를 사용하여 렌더링 */
});

 

컴포넌트가 동일한 prop으로 (현재는 prop을 받고 있지 않지만) 같은 결과를 렌더링 한다면 React.memo를 통해 감싸주고 최초 렌더링을 메모이제이션하고 re-rendering 되는 것을 방지한다.

 

React.memo()는 props의 얕은 비교만을 수행하기 때문에 정확한 비교를 하고 싶다면 커스텀 함수를 통해 prevProps와 nextProps를 비교해주어야 한다.

 

function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);

 

useMemo Hook과 React.memo()

 

요약하자면, useMemo의 경우 하위 컴포넌트에서 계산된 결과를 유지하기 위해 사용할 수 있고, React.memo()의 경우 컴포넌트의 결과를 유지하기 위해 사용할 수 있다.

 

  • 특정 값(계산된 결과)의 최적화 : useMemo Hook
  • 특정 컴포넌트의 최적화 : React.memo()

 

useCallback

 

 

Hooks API Reference – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

앞서 React.memo()를 통해 컴포넌트를 반환하는 함수를 감싸주면, 직관된 prop의 변화가 있을 경우에만 re-rendering 된다고 했다. 그렇다면 다음 예제를 살펴보자.

 

import React, { useState } from 'react';

const Checkbox = React.memo(({ label, on, onChange}) => {
  console.log(label, on);
  return (
    <label>
      {label}
      <input type="checkBox" defaultChecked={on} onChange={onChange}/>
    </label>
  )
});

function App() {
  const [catOn, setCatOn] = useState(false);
  const [hong3On, setHong3On] = useState(false);
  const [hongsamOn, setHongsamOn] = useState(false);

  const catChange = (e) => setCatOn(e.target.checked);
  const hong3Change = (e) => setHong3On(e.target.checked);
  const hongsamChange = (e) => setHongsamOn(e.target.checked);
  
  return (
    <div>
      <Checkbox label="cat" on={catOn} onChange={catChange}/>
      <Checkbox label="hong3" on={hong3On} onChange={hong3Change}/>
      <Checkbox label="hongsam" on={hongsamOn} onChange={hongsamChange}/>
    </div>
  );
}

export default App;

 

최초 렌더링

 

App 컴포넌트에서는 하위 컴포넌트로 Checkbox 컴포넌트 3개를 사용하고 있다. 각각의 Checkbox 컴포넌트는 재사용하여 서로 다른 컴포넌트들이다.

 

각각의 컴포넌트에 맞는 state가 존재하고, 각각의 컴포넌트의 변화가 있는 경우(checkbox의 상태, 즉 checked 되었는지 여부) 동작하는 함수들도 각각 정의되어 있다.

 

그리고 하위 컴포넌트 Checkbox의 경우 prop의 변경이 없는 경우 re-rendering 되지 않도록 React.memo()로 감싸져 있고, 렌더링 되었을 때 어떤 컴포넌트인지, 상태 값은 어떤지 확인하기 위한 console.log()가 있다.

 

앞서 언급한 대로 React.memo()를 통해 감싸져 있기 때문에, 하나의 컴포넌트의 prop이 변경되면 다른 컴포넌트는 re-rendering 되지 않아야 한다. 그러나 다음 결과를 보면 그렇지 않다는 것을 알 수 있다.

 

cat 컴포넌트 check 후

 

cat 컴포넌트(편의상 cat 컴포넌트)의 checkbox를 check 하면 App 컴포넌트의 catOn state가 true로 변경이 되고 해당 state가 cat 컴포넌트의 prop으로 전달된다. cat 컴포넌트의 prop만 변경되었으니 해당 컴포넌트만 re-rendering 되는 것이고 나머지 컴포넌트들은 re-rendering 되지 않아야 하지만 모두 re-rendering 되었다.

 

하위 컴포넌트와는 별개로 onChange 함수가 재정의 되면서 새롭게 렌더링 된 것이다. 즉, 부모 컴포넌트에서 state가 변경되었으니 re-rendering 되는 것이고, 그때 onChange 함수가 재정의 되면서 하위 컴포넌트들이 모두 re-rendering 된 것이다.

 

이를 막기 위해 useCallback을 활용할 수 있다.

 

import React, { useState, useCallback } from 'react';

function App() {
  const [catOn, setCatOn] = useState(false);
  const [hong3On, setHong3On] = useState(false);
  const [hongsamOn, setHongsamOn] = useState(false);

  const catChange = useCallback((e) => setCatOn(e.target.checked), []);
  const hong3Change = useCallback((e) => setHong3On(e.target.checked), []);
  const hongsamChange = useCallback((e) => setHongsamOn(e.target.checked), []);
  
  return (
    <div>
      <Checkbox label="cat" on={catOn} onChange={catChange}/>
      <Checkbox label="hong3" on={hong3On} onChange={hong3Change}/>
      <Checkbox label="hongsam" on={hongsamOn} onChange={hongsamChange}/>
    </div>
  );
}

export default App;

 

이런 식으로 onCahnge 함수에 대한 함수부 정의를 useCallback을 통해 감싸주고 두 번째 인자로 빈 배열을 넘겨주게 된다.

그리고 이전과 동일하게 cat 컴포넌트를 check 하게 되면, prop이 변경된 cat 컴포넌트만 re-rendering 되고 나머지 컴포넌트들은 re-rendering 되지 않는다.

 

useCallback 사용 후

 

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

 

useCallback의 사용법은 useMemo와 동일하다.

 

useCallback(fn, deps)은 useMemo(() => fn, deps)와 같습니다.

 

useCallback은 함수를 메모이제이션 한다고 할 수 있다. 렌더링 될 때마다 함수를 재정의하지 않고 싶은 경우 의존성으로 빈 배열을 넘겨주면 된다.

 

함수 내부에서 사용되는 값의 변경에 따라 함수를 재 정의해야 한다면, 배열 인자로 해당 값을 넘겨주면 된다.

 

정리해보자면, 빈 배열로 의존성을 넣지 않은 경우 최초 mounted 되었을 때만 함수를 정의하고 나머지는 재정의하지 않는다는 개념이고, 의존성을 배열의 인자로 넘기게 된 경우, 최초 mounted 되었을 때의 함수를 메모이제이션 해두고 넘겨받은 인자의 변경이 있을 경우에만 함수를 재정의 한다는 것이다.

 

 


 

오늘의 회고

 

 

하면 할수록 더 알아야 할 내용들이 점점 많아진다. 당연한 소리겠지만 정말 그렇다. 처음 강의를 들을 때 그럭저럭 끄덕끄덕 했다. 그리고 관련 문서들을 참고하면서 끄덕끄덕했다. 다시 최근 강의들을 들으며 사용할 땐 "왜 그렇지?"라는 의문에 다시 첫 기초부터 찾고 사용 예제들을 찾아본다. 이해한다. 그리고 다시 최적화에 대한 개념을 도입하면 "왜 그렇지?"라는 의문과 함께 이해가 가지 않기 시작한다.

 

오늘 작성하는 글까지 강의 내용 중 다루었던 Hook에 대한 정리는 끝이다. 기본적인 Hook에 대한 설명을 했던 강의 내용을 기준으로 글을 작성하면서 정리한 바로는 이해가 된다. 최근 강의에서 custom Hook이나 component를 추상화하며 작성하고 해당 내용에서 약간의 최적화를 도입할 때면 대체 Hook들이 어떻게 작용하길래 저런 방식이 되는지 이해하기 어렵다.

 

다양한 문서에서 Hook 차이를 기술하고 어떤 경우에 사용해야 하는지, 어떤 경우에 사용을 지양해야 하는지 설명하고 있지만 우선 얕은 내용의 이해만 정리해두었다. 최적화 과정에서 사용되는 Hook에 대한 내용들을 더 공부하고 명확한 차이라던가 정확한 사용 시기를 더 공부해야 한다.

참고

 

 

React – 사용자 인터페이스를 만들기 위한 JavaScript 라이브러리

A JavaScript library for building user interfaces

ko.reactjs.org

 

강의 : 프로그래머스 데브코스 프론트엔드 2기

반응형