본문 바로가기

FrontEnd

리액트 성능 최적화 : React.memo

반응형

TLDR : React.memo는 불필요한 리렌더링을 막는다.  메모 컴포넌트에 Primitive Value만 props로 넘기도록 설계하자.

더 중요한 것은 리렌더링보다 느린 리렌더링을 막는 것이다.
느린 리렌더링은 useMemo를 통해 회적화한다.

 React 앱의 라이프사이클은 다음과 같다.

→  render → reconciliation → commit
         ↖                   ↙
              state change
  • render phase : React.createElement로 리액트 엘리먼트를 생성
  • recolcilation phase : 이전 엘리먼트와 새로운 엘리먼트를 비교함
  • commit phase : 변경된 부분만 dom에 반영

 

리액트는 매우 빠르지만, 때때로 업데이트에 대한 힌트를 제공해야 한다.

리액트 컴포넌트는 다음 이유 중 하나로 리렌더링됨

  • 상위 컴포넌트가 전달하는 props 변화
    • memo가 아닌 상태면 상위 컴포넌트의 리렌더링은 반드시 하위 컴포넌트의 리렌더링을 유발함.
    • memo인 경우는 equal 함수에 따라 다름.
  • 내부 상태 변화
    • setState, dispatch등은 자신과 하위 컴포넌트의 리렌더링 유발
  • consuming 중인 컨텍스트의 값 변화
    • 컨텍스트 내부에 상태가 있을 경우
  • 부모 컴포넌트의 리렌더링

문제는 부모 컴포넌트의 리렌더링임

내부 컴포넌트의 상태가 변하지 않았어도, 자식들은 반드시 업데이트 됨.

이를 막기 위한 힌트를 주는게 필요함.

최적화 전 UI
force rerender를 누르면, 부모 컴포넌트 리렌더링 때문에 모든 컴포넌트가 다시 그려졌다는 설명을 볼 수 있다.

이는 위가 바뀌면 아래가 다 바뀌는 결과를 초래할 것이다.

모든게 다 리렌더링 되는 컴포넌트

function CountButton({count, onClick}) {
  return <button onClick={onClick}>{count}</button>
}

function NameInput({name, onNameChange}) {
  return (
    <label>
      Name: <input value={name} onChange={e => onNameChange(e.target.value)} />
    </label>
  )
}

function Example() {
  const [name, setName] = React.useState('')
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <div>
        <CountButton count={count} onClick={increment} />
      </div>
      <div>
        <NameInput name={name} onNameChange={setName} />
      </div>
      {name ? <div>{`${name}'s favorite number is ${count}`}</div> : null}
    </div>
  )
}

위 예시도 마찬가지로, CountButton에서 onClick => state변화 => CountButton, NameInput 전부 리렌더링의 결과를 초래한다.

NameInput은 상태를 반영할 뿐이지 부모의 상태 변경과 자신의 상태와 관련된 정보는 알지 못한다.

 

따라서 아래와 같이 힌트를 준다.

이름이 있어야 Devtool에서 식별 가능하다. 익명 함수를 지양하자
function CountButton({count, onClick}) {
  return <button onClick={onClick}>{count}</button>
}

function NameInput({name, onNameChange}) {
  return (
    <label>
      Name: <input value={name} onChange={e => onNameChange(e.target.value)} />
    </label>
  )
}
// 이름이 있어야 Devtool에서 식별 가능하다. 익명 함수 쓰지 말자.
NameInput = React.memo(NameInput)

CountButtonuseCallback과 동시에 사용해야 함을 잊지 말자.

 

1. 커스텀 comparator function

list가 하이라이트되면, 모든 아이템이 다 리렌더링 된다.

리렌더링 원인 레코딩 옵션 켜기

DOM 업데이트가 필요한 유일한 ListItem은 1) 이전 강조 표시된 항목과 2) 새 강조 표시된 항목이다.

React.memo는 두번째 인자로 커스텀 비교 함수를 허용함.

props를 비교하고 구성 요소를 다시 렌더링할 필요가 없으면 true를 반환하고 필요한 경우 false를 반환할 수 있음

const ListItem = function ListItem({
  getItemProps,
  item,
  index,
  selectedItem,
  highlightedIndex,
  ...props
}) {
  const isSelected = selectedItem?.id === item.id
  const isHighlighted = highlightedIndex === index
  return (
    <li
      {...getItemProps({
        index,
        item,
        style: {
          fontWeight: isSelected ? 'bold' : 'normal',
          backgroundColor: isHighlighted ? 'lightgray' : 'inherit',
        },
        ...props,
      })}
    />
  )
}
ListItem = React.memo(ListItem, (prevProps, nextProps) => {
  // true means do NOT rerender
  // false means DO rerender

  // these ones are easy if any of these changed, we should re-render
  if (prevProps.getItemProps !== nextProps.getItemProps) return false
  if (prevProps.item !== nextProps.item) return false
  if (prevProps.index !== nextProps.index) return false
  if (prevProps.selectedItem !== nextProps.selectedItem) return false

  // this is trickier. We should only re-render if this list item:
  // 1. was highlighted before and now it's not
  // 2. was not highlighted before and now it is
  if (prevProps.highlightedIndex !== nextProps.highlightedIndex) {
    const wasPrevHighlighted = prevProps.highlightedIndex === prevProps.index
    const isNowHighlighted = nextProps.highlightedIndex === nextProps.index
    return wasPrevHighlighted === isNowHighlighted
  }
  return true
})

저렇게까지 비교 함수를 짜면서 최적화를 해야할까? props의 설계를 좀 더 생각할 필요가 있다.

위의 경우 isSelected와 isHighlited를 파라미터로 받으면 문제가 없을 것이다.

ListItem들이 리렌더링되지 않는 모습

2. primitive value만으로 Dom Update 비교

비교함수를 넣지 말고, 디폴트 비교(1단계 deep equal 비교)를 이용하자.

VAC 컴포넌트에 최대한 primitive value만 전달하자.

function Menu({
  items,
  getMenuProps,
  getItemProps,
  highlightedIndex,
  selectedItem,
}) {
  return (
    <ul {...getMenuProps()}>
      {items.map((item, index) => (
        <ListItem
          key={item.id}
          getItemProps={getItemProps}
          item={item}
          index={index}
          isSelected={selectedItem?.id === item.id}
          isHighlighted={highlightedIndex === index}
        >
          {item.name}
        </ListItem>
      ))}
    </ul>
  )
}
Menu = React.memo(Menu)

function ListItem({
  getItemProps,
  item,
  index,
  isHighlighted,
  isSelected,
  ...props
}) {
  return (
    <li
      {...getItemProps({
        index,
        item,
        style: {
          backgroundColor: isHighlighted ? 'lightgray' : 'inherit',
          fontWeight: isSelected ? 'bold' : 'normal',
        },
        ...props,
      })}
    />
  )
}
ListItem = React.memo(ListItem)

참고

Fix the slow render before you fix the re-render

 

Fix the slow render before you fix the re-render

How to start optimizing your React app renders

kentcdodds.com

 

반응형