Rerendering이 발생하는 조건
1. 컴포넌트에서 state가 바뀌었을 때
2. 컴포넌트가 내려받은 props가 변경되었을 때
3. 부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트는 모두
리액트에서 리렌더링이 빈번하게, 자주 일어난다는 것은 비용이 발생한다는 것이므로 최적화(Optimizaion)를 통해 불필요한 리렌더링이 발생하지 않도록 해야 한다.
⭐️최적화하는 대표적인 방법⭐️
- memo(React.memo) : 컴포넌트를 캐싱
- useCallback : 함수를 캐싱
- useMemo : 값을 캐싱 (함수가 리턴하는 값)
👉 부모 컴포넌트가 리렌더링될 때 자식 컴포넌트가 리렌더링되는 것을 건너뛸 수 있다.
👉 props가 변경되지 않은 경우 이 컴포넌트를 리렌더링하는 것을 건너뛸 수 있다.const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
// App.jsx
import React, { useState } from "react";
import Box1 from "./components/Box1";
import Box2 from "./components/Box2";
import Box3 from "./components/Box3";
const boxesStyle = {
display: "flex",
marginTop: "10px",
};
function App() {
console.log("App 컴포넌트가 렌더링되었습니다!");
const [count, setCount] = useState(0);
// 1을 증가시키는 함수
const onPlusButtonClickHandler = () => {
setCount(count + 1);
};
// 1을 감소시키는 함수
const onMinusButtonClickHandler = () => {
setCount(count - 1);
};
return (
<>
<h3>카운트 예제입니다!</h3>
<p>현재 카운트 : {count}</p>
<button onClick={onPlusButtonClickHandler}>+</button>
<button onClick={onMinusButtonClickHandler}>-</button>
<div style={boxesStyle}>
<Box1 />
<Box2 />
<Box3 />
</div>
</>
);
}
export default App;
Box 컴포넌트에는 각각 "Box1이 렌더링 되었습니다." 를 콘솔에 출력하는 코드가 있다.
+버튼, -버튼을 클릭할 때 count라는 state가 변경되어 부모 컴포넌트 (App)가 리렌더링 되고, 자식 컴포넌트(Box1, Box2, Box3) 도 같이 리렌더링 된다.
(콘솔에 아래와 같은 로그가 반복적으로 찍힌다.)
그러나 자식 컴포넌트(Box1, Box2, Box3) 입장에서는 바뀐 것이 없는데 리렌더링 되는 것이 불필요하다. 그러므로 React.memo
를 통해 불필요한 리렌더링을 막을 수 있다.
// 변경 전
export default Box1; // Box1.jsx
export default Box2; // Box2.jsx
export default Box3; // Box3.jsx
// 변경 후
export default React.memo(Box1); // Box1.jsx
export default React.memo(Box2); // Box2.jsx
export default React.memo(Box3); // Box3.jsx
이렇게 하면 첫 렌더링 이외에는 +버튼, -버튼을 클릭하여 App.jsx의 state가 변경되더라도 자식 컴포넌트인 Box1, Box2, Box3 은 리렌더링이 되지 않는다.
(콘솔에 "App 컴포넌트가 렌더링되었습니다!" 만 반복적으로 찍힌다.)
function Btn({ text, changeValue }) {
console.log(text, "was rendered");
return (
<button
onClick={changeValue}
style={{
backgroundColor: "tomato",
color: "white",
padding: "10px 20px",
border: 0,
borderRadius: 10,
}}
>
{text}
</button>
);
}
function App() {
const [value, setValue] = useState("Save Changes");
const changeValue = () => setValue("Revert Changes");
return (
<div>
<Btn text={value} changeValue={changeValue} />
<Btn text="Continue" />
</div>
);
}
첫 번째 Btn은 리렌더링 되어야 한다. setValue가 실행되어서 props가 변경되었으니까. 하지만 두 번째 Btn은 변경 사항이 없어서 리렌더링 할 필요가 없다.
그러나 부모 컴포넌트 (App) 에서 state를 변경하고 있으므로 첫 번째 Btn 두 번째 Btn 모두 리렌더링 된다.
이 때 우리는 React.memo
를 통해 만약 props 가 변경되지 않는다면 이 컴포넌트가 리렌더링 하는 것을 막을 수 있다.
const MemorizedBtn = React.memo(Btn);
이렇게!
...
const MemorizedBtn = React.memo(Btn); // 메모
function App() {
const [value, setValue] = React.useState("Save Changes");
const changeValue = () => setValue("Revert Changes");
return (
<div>
<MemorizedBtn text={value} changeValue={changeValue} />
<MemorizedBtn text="Continue" />
</div>
);
}
이제 첫 번째 Btn을 클릭해도 두 번째 Btn이 리렌더링 되지 않는다!
👉 useCallback은 리렌더링 사이에 함수 정의를 캐시할 수 있는 React Hook이다.
const cachedFn = useCallback(fn, [의존성 배열])
// App.jsx
...
// count를 초기화해주는 함수
const initCount = () => {
setCount(0);
};
return (
<>
<h3>카운트 예제입니다!</h3>
<p>현재 카운트 : {count}</p>
<button onClick={onPlusButtonClickHandler}>+</button>
<button onClick={onMinusButtonClickHandler}>-</button>
<div style={boxesStyle}>
<Box1 initCount={initCount} />
<Box2 />
<Box3 />
</div>
</>
);
}
...
...
// Box1.jsx
function Box1({ initCount }) {
console.log("Box1이 렌더링되었습니다.");
return (
<div style={boxStyle}>
<button onClick={initCount}>초기화</button>
</div>
);
}
export default React.memo(Box1);
+버튼, -버튼, 초기화버튼을 누를 때 모두 위와 같은 로그가 콘솔에 찍힌다.
React.memo를 통해 Box1을 메모이제이션 했음에도 불구하고 Box1이 리렌더링 된다.
그 이유는 App 컴포넌트가 리렌더링 될 때 initCount
함수도 다시 저장하게 되는데, 이 때 내용물은 같더라도 다시 만들어지면 그 주솟값이 달라지게 되어 자식 컴포넌트인 Box1은 props인 initCount
가 변경되었다고 인식하는 것이다.
(함수도 객체의 한 종류이므로 주솟값만 저장 -> 불변성 part 참고!)
이 때 initCount
함수를 캐싱하기 위해 useCallback
Hook을 사용하면 된다.
// 변경 전
const initCount = () => {
setCount(0);
};
// 변경 후
const initCount = useCallback(() => {
setCount(0);
}, []);
이렇게 하면 App 컴포넌트가 리렌더링 되어도 Box1 컴포넌트는 리렌더링 되지 않는다.
👉 useMemo는 리렌더링 사이에 계산 결과를 캐시할 수 있는 React Hook이다.
👉 함수가 리턴하는 값을 캐싱한다.const cachedValue = useMemo(calculateValue, [의존성 배열])
동일한 값을 반환하는 함수를 계속 호출할 때 생기는 불필요한 렌더링을 줄이기 위해 함수가 맨 처음 값을 반환할 때 그 값을 메모리에 저장한다.
이렇게 하면 매번 함수를 호출해서 계산하는 것이 아니라 필요할 때마다 이미 저장한 값을 꺼내와서 쓸 수 있다. 이를 캐싱
이라고 표현한다.
// App.jsx
import "./App.css";
import HeavyComponent from "./components/HeavyComponent";
function App() {
const navStyleObj = {
backgroundColor: "yellow",
marginBottom: "30px",
};
const footerStyleObj = {
backgroundColor: "green",
marginTop: "30px",
};
return (
<>
<nav style={navStyleObj}>네비게이션 바</nav>
<HeavyComponent />
<footer style={footerStyleObj}>푸터 영역이에요</footer>
</>
);
}
export default App;
// HeavyComponent.jsx
import React, { useState, useMemo } from "react";
function HeavyComponent() {
const [count, setCount] = useState(0);
const heavyWork = () => {
for (let i = 0; i < 1000000000; i++) {}
return 100; // 무거운 작업
};
// CASE 1 : useMemo를 사용하지 않았을 때
// const value = heavyWork();
// CASE 2 : useMemo를 사용했을 때
const value = useMemo(() => heavyWork(), []);
return (
<>
<p>나는 {value}을 가져오는 엄청 무거운 작업을 하는 컴포넌트야!</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
누르면 아래 count가 올라가요!
</button>
<br />
{count}
</>
);
}
export default HeavyComponent;
HeavyComponent 안에서는 const value = heavyWork()
를 통해 value 를 세팅하고 있다. count라는 state가 변경될 때마다 heavyComponent가 리렌더링되어 heavyWork라는 함수도 계속해서 호출이 된다. 그리고 heavyWork() 가 매우 무거운 작업이라 함수 호출할 때마다 성능이 저하된다. 하지만 useMemo()
로 감싸주게 되면 리렌더링 할 때마다 함수를 다시 호출하지 않는다.
const value = useMemo(() => heavyWork(), []);
=> 성능이 향상된다!
useMemo를 남발하게 되면, 별도의 메모리를 너무 많이 확보하게 되어 오히려 성능이 악화될 수 있다. 🙂