리렌더링의 발생 조건
1. 컴포넌트에서 state가 바뀌었을 때
2. 컴포넌트가 내려받은 props가 변경됐을 때
3. 부모 컴포넌트가 리렌더링 된 경우
리액트에서 리렌더링이 자주 일어나는 것은 그다지 좋은 현상이 아니기 때문에 비용(cost)을 줄여야 한다.
비용을 줄이기 위한 방법이 최적화이다.
대표적인 방법
1. memo: 컴포넌트를 캐싱(메모이제이션)
2. useCallback: 함수를 캐싱(메모이제이션)
3. useMemo: 값을 캐싱(함수가 return하는 값 또는 값 자체)
캐싱이란 메모리에 저장하는 것이다.
아래와 같은 부모 컴포넌트가 있을 때
import React, { useState } from "react";
import "./App.css";
import Box1 from "./components/Box1";
import Box2 from "./components/Box2";
import Box3 from "./components/Box3";
function App() {
console.log("App 컴포넌트가 렌더링");
const [count, setCount] = useState(0);
const increaseBtnHandler = () => {
setCount(count + 1);
};
const decreaseBtnHandler = () => {
setCount(count - 1);
};
return (
<>
<h3>카운트 예제</h3>
<p>현재 카운트 {count}</p>
<button onClick={increaseBtnHandler}>+</button>
<button onClick={decreaseBtnHandler}>-</button>
<div
style={{
display: "flex",
margin: "10px",
}}>
<Box1 />
<Box2 />
<Box3 />
</div>
</>
);
}
export default App;
버튼을 클릭하여 부모 컴포넌트가 리렌더링 될 때 자식 컴포넌트는 변경이 없어도 같이 리렌더링 된다. 이 문제를 해결하는 방법은 간단하다.
export default React.memo(Box1);
자식 컴포넌트를 export시 컴포넌트 이름을 React.memo()로 감싸는 것이다.
위의 부모 컴포넌트에 아래와 같이 count를 초기화하는 함수가 있고
const initCount = () => {
setCount(0);
};
아래와 같은 그 함수를 props로 받는 자식 컴포넌트가 있을 때
import React from "react";
const style = {
width: "100px",
height: "100px",
color: "#fff",
backgroundColor: "#01c49f",
};
function Box1({ initCount }) {
console.log("box1 렌더링");
return (
<div style={style}>
<button
onClick={() => {
initCount();
}}>
초기화
</button>
</div>
);
}
export default React.memo(Box1);
React.memo를 사용해도 부모 컴포넌트가 리렌더링 되면 리액트는 props로 받는 함수가 변경됐다고 판단하여 해당 props를 물려받는 자식 컴포넌트도 리렌더링된다.(부모 컴포넌트의 +나 -버튼을 눌렀을 때)
이 문제는 아래와 같이 해당 함수를 useCallback으로 감싸주면 되며 useCallback은 의존성 배열이 필요하다.
const initCount = useCallback(() => {
setCount(0);
}, []);
의존성 배열이 빈 배열이면 의존성 배열에 새로운 값이 추가되거나 변경되지 않으니 initCount 함수는 그대로 유지, 즉 props가 변경되지 않는 것이다.
하지만 initCount함수 내에서 현재 count값을 사용해야 한다면 의존성 배열에 count를 넣어줘야 한다.
왜냐하면 useCallback으로 감싸면 count값은 함수는 처음 저장된 시점, 즉 count값이 초기값 0인 시점 상태기 때문에 의존성 배열에 count를 넣지 않으면 initCount함수 내의 count값은 0이다.(snapshot을 유지하기 때문)
useMemo는 함수의 값, 즉 return하는 값을 캐싱하는 방법이다.
만약 아래와 같이 시간이 오래 걸리는(무거운) 컴포넌트가 있다고 하자.
import React, { useState } from "react";
function HeavyComponent() {
const [count, setCount] = useState(0);
const heavyWork = () => {
for (let i = 0; i < 1000000000; i++) {}
return 100;
};
const value = heavyWork();
console.log(value);
return (
<>
<p>Im HeavyComponent</p>
<p>{count}</p>
<button
onClick={() => {
setCount(count + 1);
}}>
count +
</button>
</>
);
}
export default HeavyComponent;
heavyWork함수의 값을 value에 저장해서 콘솔에 출력을 한다고 했을 때
버튼을 클릭 => state변경 => 리렌더링 이 과정에 딜레이가 생기는 것을 볼 수 있다.
이런 문제를 해결하기 위해 useMemo를 사용한다. 사용하는 방법은 아래와 같다.
const value = useMemo(() => heavyWork(), []);
useMemo로 감싸면 버튼을 클릭해도 딜레이가 생기지 않는다.
useMemo 예시 2
아래와 같은 코드가 있다면
import React, { useEffect, useMemo, useState } from "react";
function ObjectComponent() {
const [isAlive, setIsAlive] = useState(true);
const [uselessCount, setUselessCount] = useState(0);
const me = {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
useEffect(() => {
console.log("생존여부가 바뀔 때만 호출해주세요!");
}, [me]);
return (
<>
<div>
내 이름은 {me.name}이구, 나이는 {me.age}야!
</div>
<br />
<div>
<button
onClick={() => {
setIsAlive(!isAlive);
}}>
누르면 살았다가 죽었다가 해요
</button>
<br />
생존여부 : {me.isAlive}
</div>
<hr />
필요없는 숫자 영역이에요!
<br />
{uselessCount}
<br />
<button
onClick={() => {
setUselessCount(uselessCount + 1);
}}>
누르면 숫자가 올라가요
</button>
</>
);
}
export default ObjectComponent;
필요없는 숫자 영역의 버튼을 클릭했을 때 콘솔이 출력되는 것을 볼 수 있다.
uselessCount의 state가 변경이 되면서 함수 자체(컴포넌트)가 리렌더링이 되고,
리렌더링 과정에서 me라는 객체의 주소값이 바뀌기 때문에
리액트는 useEffect의 의존성 배열 안에 있는 me라는 객체가 바뀌었다고 판단하기 때문이다.
이 문제를 해결하기 위해서는 me라는 객체를 아래와 같이 useMemo로 감싸면 되고
const me = useMemo(() => {
return {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
}, [isAlive]);
마찬가지로 의존성 배열이 필요하다.
주의할 점은 useMemo, useCallback을 통해서 저장하는 값은 메모리에 임시적으로 저장되기 때문에 남발하면 메모리 성능을 저하시킨다.