본문 바로가기
Frontend/React

React One Simple trick to optimize React re-renders (번역글) 🤔

by 우보틀 2022. 4. 27.

이 글은 아래 출처의 글을 번역한 글입니다. 오역과 의역을 자주 사용합니다.

 

 

리액트 리렌더와 관련된 블로그 포스트를 준비하고 있었는데 우연히 작은 React 지식을 발견했고 여러분께 도움이 되리라 생각합니다.

 

 

이 블로그 포스트를 읽은 후에 Brooks Lybrand는 이 트릭을 구현했고 아래는 그 결과 입니다.

 

흥미롭나요? 단순한 예제와 함께 살펴보고 여러분의 앱을 위한 실용적인 예제에 대하여 이야기 해봅시다

 

An example

// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-g9mt5

import * as React from 'react'
import ReactDOM from 'react-dom'

function Logger(props) {
  console.log(`${props.label} rendered`)
  return null
}

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <button onClick={increment}>The count is {count}</button>
      <Logger label="counter" />
    </div>
  )
}

ReactDOM.render(<Counter />, document.getElementById('root'))

 

이것이 동작할때 "counter rendered"는 콘솔에 즉시 찍힙니다. 그리고 count가 증가될때마다 "counter rendered"는 콘솔에 출력될 겁니다. 버튼이 클릭되었고 상태가 변했기 때문에 발생합니다. React는 상태 변화에 근하여 새로운 React 요소를 가져와서 렌더 해야 하기 위해 알아야 합니다. 새로운 요소를 가져올때 렌더되고 DOM에 커밋 됩니다.

 

여기에 흥미로운 요소가 있습니다. `<Logger label="counter"/>`는 사실 렌더간에 절대 변하지 않습니다. 이것은 정적입니다 그러므로 추출될 수 있습니다.재미로 한번 시도해봅시다. (이걸 추천하지는 않습니다. 이따 실용적인 추천이 나올때까지 좀만 기다려주세요)

 

// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-o9e9f

import * as React from 'react'
import ReactDOM from 'react-dom'

function Logger(props) {
  console.log(`${props.label} rendered`)
  return null // what is returned here is irrelevant...
}

function Counter(props) {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <button onClick={increment}>The count is {count}</button>
      {props.logger}
    </div>
  )
}

ReactDOM.render(
  <Counter logger={<Logger label="counter" />} />,
  document.getElementById('root'),
)

 

변화를 눈치챘나요? 네 우리는 시작 로그를 가지게 됩니다. 그러나 버튼을 클릭했을때 새로운 로그는 얻지 못합니다

 

 

What's going on?

 

무엇이 이 차이를 유발했을까요? 리액트 요소와 관련이 있습니다. React 요소와 그들과 JSX의 관계를 빠르게 refresh 하기위해 "What is JSX?" 이 글을 잠시 멈추고 읽고 오는건 어떨까요? 

 

리액트가 counter 함수를 호출할 때, 아래와 같이 보이는 것을 가져옵니다.

 

const counterElement = {
  type: 'div'
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: increment, // this is the click handler function
          children: 'The count is 0'
        },
      }, {
        type: Logger,
        props: {
          label: 'counter',
        }
      }
    ]
  }
}

 

이들은 UI 묘사 객체로 불립니다. 그들은 React가 DOM 에서 생성해야 하는 UI를 묘사합니다. 버튼을 누르고 변화를 살펴봅시다

const counterElement = {
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: increment,
          children: 'The count is 1',
        ]
      }, 
      {
        type: Logger,
        props: {
          label: 'counter'
        }
      }
    ]
  }
}

 

우리가 호출을 아무리 많이 해도 button의 `onClick`과 `children` prop만 변화합니다. 그러나 이 모든 것은 완전히 새롭습니다. React가 사용되던 초기 이후에 여러분은 이러한 객체를 매 렌더마다 새로 생성하였었습니다. (운이 좋게도, 모바일 브라우저에서도 이 동작은 빠릅니다. 그래서 특별한 성능 문제는 발생하지 않았습니다.)

 

React 요소가 렌더간에 같은지 tree 일부분에서 검사하는 것이 훨씬 쉬울것입니다. 아래에는 렌더간에 변화하지 않는 것들이 있습니다.

const counterElement = {
  type: 'div',  // 변화 🔴
  props: {
    children: [
      {
        type: 'button',  // 변화 🔴
        props: {
          onClick: increment,
          children: 'The count is 1',
        },
      },
      {
        type: Logger,  // 변화 🔴
        props: {
          label: 'counter',  // 변화 🔴
        },
      },
    ],
  },
}

 

모든 요소의 type은 동일하고 Loger 요소의 `label` prop도 변화하지 않습니다. 그러나 props 객체는 렌더 간에 스스로 변화합니다. 이전 prop 객체와 객체의 property들이 같을지라도 말이빈다.

 

자 여기에 kicker가 있습니다. Logger props 객체가 변화되었기 때문에 React는 새로운 prop객체에 기반하여 새로운 JSX를 가져와야 하는지 확인하기 위해  Logger 함수를 재동작해야할 필요가 있습니다.  그런데 render간에 prop의 변화를 막을 수 있다면 어떨까요??

만약 prop이 바뀌지 않았다면 React는 JSX는 바뀌지 않을거고 함수의 재동작도 필요없다고 알수 있습니다. 이건 React에 다음과 같이 작성이 되어있고 React가 처음 시작할 때부터 그랬습니다.

 

그러나 문제는 우리가 새로운 React 요소를 생성할 때 마다 react는 새로운 `props` 객체를 만드는 것입니다. 그러면 어떻게 우리가 렌더 간에 props 객체가 변화하지 않았다라는 것을 보장할 수 있을까요?

이제 두번째 예제에서 Logger가 리렌더 되지 않는 것을 이해하고 가닥이 잡혔길 바랍니다.

 

Let's bring it back together

 

두번째 예제를 다시 보겠습니다.

// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-o9e9f

import * as React from 'react'
import ReactDOM from 'react-dom'

function Logger(props) {
  console.log(`${props.label} rendered`)
  return null // what is returned here is irrelevant...
}

function Counter(props) {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <button onClick={increment}>The count is {count}</button>
      {props.logger}
    </div>
  )
}

ReactDOM.render(
  <Counter logger={<Logger label="counter" />} />,
  document.getElementById('root'),
)

 

렌더 간에 같은 것들을 찾아보겠습니다.

 

Logger 요소는 완벽히 변하지 않기 때문에 React는 자동적으로 최적화를 제공할 수 있고 Logger 요소가 리렌더 되지 않아도 된다는 것을 알 수 있습니다. prop들을 개별적으로 검사하는 것을 제외하고 `React.memo`와 기본적으로 유사합니다. React는 prop 객체를 전체적으로 확인합니다.

 

So what does this mean for me?

요약하자면 성능이슈를 경험하셨다면 아래와 같이 시도해보세요

 

  1. 비용이 비싼 컴포넌트를 부모로 올려서 render가 적게 되게 해보세요
  2. 비용이 비싼 컴포넌트를 prop으로 전달합니다

 

코드에 거대한 데이밴드를 분이는 것과 같이  `React.memo`의 빈번한 쓰임 없이 퍼포먼스 문제를 해결할 수 있습니다.

 

Demo

 

React에서 느린 앱의 실제 데모를 만드는 것은 전체 앱을 구축해야 하기 때문에 까다로워서 저는 before/after를 확인해볼 수 있는 예시를 만들었습니다.

 

 

 

 

 첨언하고 싶은 것은 코드의 빠른 버전을 사용하는 것이 더 나을지라도 , 초기 렌더링 시에는 성능이 여전히 떨어집니다. 그리고 만약 다른 top-down 리렌더가 필요하다면 성능은 좋지 않을것입니다. 그것은 아마도 자체적인 장점에 따라 다루어져야 하는 성능 문제입니다. 또한 codesandbox는 React development 버전을 사용했다는 것을 기억해주세요 좋은 개발경험을 주지만 production 버전보다는 성능이 느립니다.

 

그리고 이는 앱의 상위 단계에서만 유용한 것이 아닙니다. 앱에서 필요한 어느곳에서라도 적용할 수 있습니다.  제가 이것에 관하여 좋아하는 것은 "구성에 있어 자연스럽고 최적화의 기회로 작용합니다." 그래서 거부감이 없었고 퍼포먼스 성능도 공짜로 얻었습니다. 그리고 이것이 React와 관련하여 제가 좋아하는 것입니다. React는 React앱이 기본적으로 빠르도록 작성되었고 React는 함정을 피할 수 있게 최적화 helper를 제공합니다.

 

 


 

출처 : https://kentcdodds.com/blog/optimize-react-re-renders

 

One simple trick to optimize React re-renders

Without using React.memo, PureComponent, or shouldComponentUpdate

kentcdodds.com