이 글은 아래 출처의 글을 번역한 글입니다. 오역과 의역을 자주 사용합니다.
리액트 리렌더와 관련된 블로그 포스트를 준비하고 있었는데 우연히 작은 React 지식을 발견했고 여러분께 도움이 되리라 생각합니다.
If you give React the same element you gave it on the last render, it wont bother re-rendering that element.
— Kent C. Dodds 💿 (@kentcdodds) June 24, 2019
이 블로그 포스트를 읽은 후에 Brooks Lybrand는 이 트릭을 구현했고 아래는 그 결과 입니다.
A little before and after optimization on a react component.
— Brooks Lybrand (@BrooksLybrand) July 12, 2019
I didn't use any memoization to accomplish this, yet I was able to go from a 13.4ms to a 3.6ms render.
I also didn't do anything besides move code into an extra component, which ended up cutting out 27 lines of code. pic.twitter.com/xrUN0MUm5Y
흥미롭나요? 단순한 예제와 함께 살펴보고 여러분의 앱을 위한 실용적인 예제에 대하여 이야기 해봅시다
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?
요약하자면 성능이슈를 경험하셨다면 아래와 같이 시도해보세요
- 비용이 비싼 컴포넌트를 부모로 올려서 render가 적게 되게 해보세요
- 비용이 비싼 컴포넌트를 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
'Frontend > React' 카테고리의 다른 글
React 공식문서 주요개념 살펴보기 (0) | 2022.05.26 |
---|---|
React what is JSX? (번역글) 🤔 (0) | 2022.04.27 |
React The State Reducer Pattern with React Hooks (번역글) 🤔 (0) | 2022.04.24 |
React theStateReducerPattern (번역글) 🤔 (0) | 2022.04.24 |
React React-query InfiniteQuery 예제 ∞ (0) | 2022.04.23 |