리액트 컴포넌트의 리렌더링 조건으로는 크게 두가지가 있다.
1) props or state 변경
2) 부모 컴포넌트가 리렌더링
1번의 컴포넌트에서 렌더링해야 할 데이터 값이 변경되었다면 당연히 리렌더링 되어야 하는게 맞지만, 2번의 경우 해당 컴포넌트에선 같은 결과를 렌더링할 때도 리렌더링 해야하는 경우가 생긴다.
클래스 컴포넌트에서는 이를 shouldComponentUpdate()로 설정할 수 있다. 현재 props 또는 state값과 다음에 렌더링해야할 props, state 값을 직접 비교해서 리렌더링 여부를 결정하는 메소드이다. 함수형 컴포넌트에는 React.memo를 사용해서 이를 관리할 수 있다.
React.memo(Component, compFunc);
다음과 같이 사용한다. compFunc에는 비교함수를 작성한다. 결과값이 true면 리렌더링하고, false면 이전 렌더링 결과를 그대로 출력한다(리렌더링하지않음). 비교함수를 작성하지 않으면 pure component처럼 전체 state, props를 얕은비교 후 변경값이 있을 때만 출력한다.
예제 1)
App.js
import React, { useState } from "react"
import Child from "./Components/Child"
function App() {
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)
return (
<div className="App">
<button onClick={() => setCount1(count1 + 1)}>버튼1</button>
<button onClick={() => setCount2(count2 + 1)}>버튼2</button>
<Child id={1} count={count1} />
<Child id={2} count={count2} />
</div>
)
}
export default App
Child.js
import React from 'react'
const Child = ({id, count}) => {
console.log(id + " 리렌더링...")
return (
<div>
[ 카운터 {id} ] : {count}
</div>
)
}
export default Child
그냥 버튼 누르면 카운트 출력하는 컴포넌트이다.
버튼 1을 누르면 자식 컴포넌트인 Child컴포넌트가 둘 다 리렌더링 된다.
import React, {memo} from 'react'
const Child = ({id, count}) => {
console.log(id + " 리렌더링...")
return (
<div>
[ 카운터 {id} ] : {count}
</div>
)
}
export default memo(Child)
memo로 감싼 경우 버튼1 클릭시 카운터2의 컴포넌트가 리렌더링 되지 않음.
예제 2) props에 함수를 넘겨줄 때
//App.js
const getCube = (n) => n * n * n;
//Child.js
import React, {memo} from 'react'
const Child = ({id, count, getCube}) => {
console.log(id + " 리렌더링...")
return (
<div>
[ 카운터 {id} ] : {getCube(count)}
</div>
)
}
export default memo(Child)
count의 세제곱을 반환하는 getCube메서드를 만들고 Child 컴포넌트를 넘겨준다
memo를 썼음에도 버튼1을 눌렸을 때 카운터 1, 2가 모두 리렌더링 된다.
그 이유는 부모 컴포넌트인 App이 리렌더링 할 때 정의한 메서드인 getCube의 값도 새로 만들어진다. 리액트 memo는 얕은비교만 수행하기 때문에 함수의 내용물보다는 함수자체의 레퍼런스값만으로 판단한다. 새로운 함수는 이전과 같은 기능을 수행한다 하더라도 이미 삭제된 이전 함수와는 다른 레퍼런스 값을 가지고 있기 때문에 react.memo는 다른값이라고 인식한다. 이를 해결하기위해선 comp함수를 지정하거나 useCallback을 사용하면 된다.
1. comp함수 사용
//Child.js
import React, { memo } from "react"
const Child = ({ id, count, getCube }) => {
console.log(id + " 리렌더링...")
return (
<div>
[ 카운터 {id} ] : {getCube(count)}
</div>
)
}
const comp = (prev, next) => {
return prev.count === next.count
}
export default memo(Child, comp)
사실상 id값도 변경될 일 없는 값이기 때문에 count값만 이전과 다른지 체크한다. 다음에 올 count값이 다르다면 comp는 true를 리턴하고 렌더함수가 실행된다.
2. useCallback 사용
const getCube = useCallback((n) => n * n * n, [])
useCallback은 함수를 메모이제이션해서 처음 만들어 놓은 값을 그대로 쓰도록 도와주는 함수이다. 두번째 인자인 deps 배열에 넣은 값이 변경되었을 경우에만 새로만들어지고 그 외엔 이전 값을 그대로 사용한다. 이는 props로 넘겨주는 함수값이 이전과 같다고 React.memo에서 비교하기 때문에 함수때문에 리렌더링되는 일을 방지한다.
예제 3) Children을 사용할 때
//App.js
import React, { useState, useCallback } from "react"
import Parrent from "./Components/Parrent"
import Child from "./Components/Child"
function App() {
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)
const [count3, setCount3] = useState(0)
const getCube = useCallback((n) => n * n * n, [])
return (
<div className="App">
<button onClick={() => setCount1(count1 + 1)}>버튼1</button>
<button onClick={() => setCount2(count2 + 1)}>버튼2</button>
<button onClick={() => setCount3(count3 + 1)}>버튼3</button>
<Parrent>
<Child id={1} count={count1} getCube={getCube} />
<Child id={2} count={count2} getCube={getCube} />
</Parrent>
</div>
)
}
export default App
//Parent.js
import React, {memo} from "react"
const Parrent = ({ children }) => {
console.log('Parrent 리렌더링..')
return (
<div style={{ margin: 20, padding: 20, backgroundColor: "lavender" }}>
{children}
</div>
)
}
export default memo(Parrent)
코딩하다가 겪은 상황인데 자식 컴포넌트들을 묶어서 단순히 스타일값을 넣어주고 싶어서 만든 Parrent 컴포넌트이다.
위의 경우 Child 컴포넌트는 모두 React.memo로 감싸져있고 각각 count1과 count2 값이 변하는 버튼1과 버튼2를 클릭했을 때만 리렌더링 된다. 임의로 추가한 버튼3은 count3을 증가시키는데, 이 때 자식 컴포넌트의 값(child1, child2)이 변하지 않으므로 Parrent 컴포넌트도 리렌더링 될 필요가 없다. 하지만 Children 배열은 부모(App.js)가 리렌더링 될 때 마다 새로운 배열에다 담아주는듯 하다. 위의 함수와 비슷한 상황. 이 때 Parrent 컴포넌트의 리렌더링을 방지하려면 어떻게 해야하나?
이는 memo가 얕은 복사를 하기 때문에 children의 내용물이 아닌 레퍼런스값만 가지고 비교를 하기 때문이라서 깊은 비교를 하면 된다. 간단한 방법으론 두 값을 문자열로 변환 후 단순 비교하는 방법이 있다. 당연히 성능적으로는 좋지 못할 것이다.
이를 도와주는 라이브러리가 있다.
www.npmjs.com/package/react-fast-compare
react-fast-compare
Fastest deep equal comparison for React. Great for React.memo & shouldComponentUpdate. Also really fast general-purpose deep comparison.
www.npmjs.com
memo에 쓰일 동등비교함수를 제공한다. 깊은 비교를 위한 라이브러리인데 내부엔 레퍼런스값이 같은가? (같으면 return true) -> 틀리다면 contstructor가 다른가(다르면 return false) -> length, key 등등 두 데이터를 비교하는 도중 결과가 나오면 탈출하는식으로 짜서 문자열로 변환해서 비교하는 것 보다 훨씬 빠르다.
import React, {memo} from "react"
import isEqual from "react-fast-compare"
const Parrent = ({ children }) => {
console.log('Parrent 리렌더링..')
return (
<div style={{ margin: 20, padding: 20, backgroundColor: "lavender" }}>
{children}
</div>
)
}
export default memo(Parrent, isEqual)
children 내부 컴포넌트의 변화가 없다면 리렌더링되지않음.
리액트 memo로 리렌더링을 조작해서 성능을 개선 할 수 있다.
하지만 props의 변경이 절대적으로 많은 경우는 그냥 props 비교를 안하는게 차라리 성능상으로 더 나을 수 있다고 한다.
(예를들면 스톱위치의 시간을 밀리초단위로 출력하는 카운트 컴포넌트)
근데 대부분의 경우에는 좋을듯? 자주써야겠다.
'프로그래밍' 카테고리의 다른 글
useRef 사용용도 (0) | 2020.12.22 |
---|---|
리액트 커스텀 훅 (0) | 2020.12.14 |
리액트 에러 경계 (0) | 2020.12.08 |
리액트 네이티브 커스텀 폰트 적용 (0) | 2020.11.02 |
webp 움짤 압축은 용량을 얼마나 줄여주나 (0) | 2020.09.19 |