React를 활용하다 보면 간혹 불필요한 렌더링이 중복으로 동작 되는 경우를 발견할 수 있다.
아직까지 큰 프로젝트를 접한 경험은 없어 크게 문제로 여기지는 않았지만, 큰 퍼포먼스가 발생하는 웹 페이지를 다룰 경우 렌더링이 두세번 불필요하게 작동된다면, 브라우저 연산 과정에 속도 저하가 일어날 수 있다.
이러한 렌더링 제어를 할 수 있는 대표적 React Hook인 useMemo와 useCallback에 대해 알아보자.
import { useCallback, useState } from 'react';
import './App.css';
import Button_Plus from './Button_Plus';
import Button_Minus from './Button_Minus';
function App() {
const [countPlus, setCountPlus] = useState(0)
const [countMinus, setCountMinus] = useState(0)
const handlePlus = () => {
console.log("플러스 클릭")
return [countPlus, countPlus + 1, countPlus + 2]
}
const handleMinus = () => {
console.log("마이너스 클릭")
return [countMinus, countMinus - 1, countMinus - 2]
}
return (
<div className="App">
<button onClick={() => setCountPlus(countPlus + 1)}>+</button>
<button onClick={() => setCountMinus(countMinus - 1)}>-</button>
<Button_Plus handlePlus={handlePlus} />
<Button_Minus handleMinus={handleMinus} />
</div>
);
}
export default App;
import { useEffect, useState } from "react"
function Button_Plus({handlePlus}) {
const [plus, setPlus] = useState([])
useEffect(() => {
setPlus(handlePlus)
console.log("플러스 자식 컴포넌트 렌더링")
}, [handlePlus])
return (
<div style={{display: 'flex', justifyContent: 'center'}}>
{plus.map((num, idx) => (
<div key={idx}>
{num}{plus.length - 1 !== idx ? ", " : null}
</div>))}
</div>
)
}
export default Button_Plus
import { useEffect, useState } from "react"
function Button_Minus({handleMinus}) {
const [minus, setMinus] = useState([])
useEffect(() => {
setMinus(handleMinus)
console.log("마이너스 자식 컴포넌트 렌더링")
}, [handleMinus])
return (
<div style={{display: 'flex', justifyContent: 'center'}}>
{minus.map((num, idx) => (
<div key={idx}>
{num}{minus.length - 1 !== idx ? ", " : null}
</div>))}
</div>
)
}
export default Button_Minus
위와 같은 코드를 구성하여 button을 하나라도 클릭할 경우 마이너스 자식 컴포넌트 렌더링
, 플러스 자식 컴포넌트 렌더링
가 아래와 같이 일어난다.
플러스 버튼을 누를 경우 setCountPlus
로 state가 변경되어 Button_Plus
컴포넌트의 props가 뿌려져야 할텐데, 다른 함수인 handleMinus
또한 영향을 받아, 렌더링이 모두 일어나는 현상을 확인할 수 있다.
먼저 리렌더링의 발생 조건은 아래처럼 구성되어 있다.
즉, 위 코드는 만일 +에 대한 button을 onClick할 경우 state가 변경되어 어느 하나의 state가 변경되면 전체 부모 컴포넌트가 다시 리렌더링이 발생하게 되어, handlePlus
handleMinus
가 새로 생성된다.
handle
함수와 count
가 생성되어 렌더링 된다.handlePlus
함수가 작동하여 state count
가 변경된다.count
의 변경으로 인해, 부모 컴포넌트는 리렌더링이 되게 되고, 변경된 props를결국 button
을 누르면 두번의 렌더링이 발생하며, 그 이유는 컴포넌트가 렌더링될 때마다 해당 함수들을 새로 생성하여 결국 불필요한 렌더링이 일어난 것이다.
불필요한 렌더링을 방지하기 위해 React Hook인 useCallback과 useMemo가 필요하게 된다.
언급한 React Hook을 사용하기에 앞서 Memoization에 대해 알고 넘어가야 하는 점이 있다.
Memoization이란 수행한 연산의 결과값을 별도의 공간에 저장하고 동일한 값이 들어오게 되면, 별도의 공간을 재활용하는 알고리즘 기법을 말한다.
그러나 많은 메모리가 쓰이게 될 수 있으므로 메모리 관리에 있어 주의하여야 한다.
메모이제이션된 값을 반환한다.
useMemo(() => 실행문, [종속배열])
종속배열에 변화가 일어날 경우, 실행문이 작동된다.
다시 말해 종속 배열에 의존하여 useMemo가 실행된다고 보면 좋다.
const handlePlus = useMemo(() => {
console.log("플러스 클릭")
return [countPlus, countPlus + 1, countPlus + 2]
}, [countPlus])
const handleMinus = useMemo(() => {
console.log("마이너스 클릭")
return [countMinus, countMinus - 1, countMinus - 2]
}, [countMinus])
종속배열은 각각 countPlus
, countMinus
으로 묶여 있으므로 각 상태값이 변화가 일어날 때에 useMemo의 실행문이 작동되며, 위와 같은 방식을 활용하면 불필요한 렌더링을 방지할 수 있게 된다.
또한, 값을 반환한다고 언급하였는데, Button_Plus
컴포넌트의 props를 콘솔로그 찍어보면 아래와 같이 값이 출력된다.
메모이제이션된 함수를 반환한다.
useCallback(() => 실행문, [종속배열])
const handlePlus = useCallback(() => {
console.log("플러스 클릭")
return [countPlus, countPlus + 1, countPlus + 2]
}, [countPlus])
const handleMinus = useCallback(() => {
console.log("마이너스 클릭")
return [countMinus, countMinus - 1, countMinus - 2]
}, [countMinus])
코드 작성 방법은 useMemo와 큰 차이가 없다.
그러나 해당 함수를 props
에 넘겨 콘솔로그를 찍어보면 아래와 같이 함수가 출력된다.
useMemo와 useCallback을 통해 변화하지 않은 요소에 불필요 렌더링을 하지 않고, 메모이제이션 방법을 활용하여 이전의 Virtual DOM을 재사용 한다.
그렇다면 둘의 차이점은 무엇일까?
useMemo는 해당 함수의 연산량이 많을때 이전 출력 결과값을 메모이제이션 방법을 통해 재사용이 필요할 경우, useCallback은 함수가 재생성 되는것을 방지하기 위한 경우로 나뉜다.