
2023.02.10
useState, useEffect, useRef, useContext 등 많은 훅을 배우면서 렌더링에 대해 자주 들었다. 리액트에서 리렌더링이 빈번하게 일어난다는것은 비용이 늘어난다는 뜻이기에 전혀 좋은게 아니다. 따라서 이런 불필요한 렌더링을 발생하지 않게하는것이 최적화(Optimization) 이다.
리-렌더링의 발생 조건 중 부모 컴포넌트가 리렌더링 되면 자식컴포넌트는 모두 리렌더링 된다 라는 조건에서의 최적화 작업이 React.memo 이다.
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;
Box1.jsx
import React from "react";
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#91c49f",
color: "white",
// 가운데 정렬 3종세트
display: "flex",
justifyContent: "center",
alignItems: "center",
};
function Box1() {
console.log("Box1이 렌더링되었습니다.");
return <div style={boxStyle}>Box1</div>;
}
export default Box1;
Box2.jsx
import React from "react";
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#4e93ed",
color: "white",
// 가운데 정렬 3종세트
display: "flex",
justifyContent: "center",
alignItems: "center",
};
function Box2() {
console.log("Box2가 렌더링되었습니다.");
return <div style={boxStyle}>Box2</div>;
}
export default Box2;
Box3.jsx
import React from "react";
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#c491be",
color: "white",
// 가운데 정렬 3종세트
display: "flex",
justifyContent: "center",
alignItems: "center",
};
function Box3() {
console.log("Box3가 렌더링되었습니다.");
return <div style={boxStyle}>Box3</div>;
}
export default Box3;
이러한 코드에서 plus 버튼 또는 minus 버튼을 누른 순간 모든 하위 컴포넌트가 리렌더링 되게된다.
간단히 React.memo를 이용해서 컴포넌트를 메모리에 저장해두고 필요할 때 갖다 쓰면 된다. 이렇게 하면 부모 컴포넌트의 state의 변경으로 인해 props가 변경이 일어나지 않는 한 컴포넌트는 리렌더링 되지 않는다. 이것을 컴포넌트 memoization 이라고 한다.
Box1.jsx, Box2.jsx, Box3.jsx 모두 동일
export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);
React.memo는 컴포넌트를 메모이제이션 했다면, useCallback은 인자로 들어오는 함수 자체를 기억(메모이제이션)한다.
Box1이 만일, count를 초기화 해 주는 코드라고 가정해보자.
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이 렌더링되었습니다.");
const onInitButtonClickHandler = () => {
initCount();
};
return (
<div style={boxStyle}>
<button onClick={onInitButtonClickHandler}>초기화</button>
</div>
);
}
...
+ 버튼이나, - 버튼을 누를 때 그리고 초기화 버튼을 누를 때 모두

App 컴포넌트와 Box1 컴포넌트가 리렌더링 되는 것을 볼 수 있다.
함수형 컴포넌트를 사용하기 때문에 App.jsx 가 리렌더링 되면서 코드가 다시 만들어지기 때문이다. 자바스크립트에서는 함수도 객체의 한 종류이다. 따라서 모양은 같더라도 다시 만들어지면 그 주솟값이 달라지고 이에 따라 하위 컴포넌트인 Box1.jsx는 props가 변경됐다고 인식하는 것이다.
그렇기 때문에,
const onInitButtonClickHandler = () => {
initCount();
};
이 함수를 메모리 공간에 저장해두고, 특정 조건이 아닌 경우엔 변경되지 않도록 제어한다.
useCallback hook의 적용
App.jsx
// 변경 전
const initCount = () => {
setCount(0);
};
// 변경 후
const initCount = useCallback(() => {
setCount(0);
}, []);
App.jsx
// count를 초기화해주는 함수
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, []);
...
count를 올린후 초기화 할때 콘솔을 찍게되면 "0에서 0으로 변경되었습니다." 라고 나오게된다.
이런 현상이 발생하는 이유는, useCallback이 count가 0일 때의 시점을 기준으로 메모리에 함수를 저장했기 때문이다. 이 때문에 dependency array가 필요하다.
App.jsx
// count를 초기화해주는 함수
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, [count]); // 이부분이 dependency array
memo는 memoization을 뜻한다. 기억한다는 의미인데, 맨 처음 해당 값을 반환할 때 그 값을 특별한 곳(메모리)에 저장한다는 뜻이다. 이렇게 사용하는 이유는 필요할 때 마다 다시 함수를 호출해서 계산하는게 아니라 이미 저장한 값을 단순히 꺼내와서 쓸 수 있게 하기 위해서다. 보통 이러한 기법을 캐싱을 한다. 라고 표현한다.
// as-is
const value = 반환할_함수();
// to-be
const value = useMemo(()=> {
return 반환할_함수()
}, [dependencyArray]);
dependency Array의 값이 변경 될 때만 반환할_함수()가 호출된다.
그 외의 경우에는 memoization 해놨던 값을 가져오기만 한다.
HeavyComponent 안에서는 const value = heavyWork() 를 통해서 value값을 세팅해주고 있다. 만약 heavyWork가 엄청나게 무거운 작업이라면 다른 state가 바뀔 때 마다 계속해서 호출이 될테지만, useMemo()로 감싸주게 되면 그럴 걱정이 없다.
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;
components > HeavyComponent.jsx
import React, { useState, useMemo } from "react";
function HeavyButton() {
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 HeavyButton;
아래와 같은 코드일때
import React, { useEffect, 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;
useEffect hook을 이용해서 me의 정보가 바뀌었을 때만 발동되게끔 dependency array를 넣어놨는데, 엉뚱하게도 count를 증가하는button을 눌러보면 계속 log가 찍히는 것을 볼 수가 있다. 이는 불변성과 관련이 있는데,
위 예제에서 버튼이 선택돼서 uselessCount state가 바뀌게 되면
→ 리렌더링이 된다
→ 컴포넌트 함수가 새로 호출한다
→ me 객체도 다시 할당한다(이 때, 다른 메모리 주소값을 할당)
→ useEffect의 dependency array에 의해 me 객체가바뀌었는지 확인해봐야 하는데
→ 이전 것과 모양은 같은데 주소가 다르다
→ 리액트 입장에서는 me가 바뀌었구나 인식하고 useEffect 내부 로직이 호출된다.
const me = useMemo(() => {
return {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
}, [isAlive]);
이런식으로 useMemo()를 사용해주면 해결된다.
다만 useMemo()는 별도의 메모리 확보를 많이 하게되므로 필요할때만 사용하는것이 좋다.