
1. 리액트의 재랜더링에 관하여
요즘 웹최적화에 약간 관심이 생기면서(자사서비스 개발로 이직한지 1년이 넘어가는데, 아직도 마인드는 대충 대충...) 리액트에서는 상태(state)나 프롭스(props)가 변경될 때 컴포넌트가 다시 렌더링된다. 다만 불필요한 재랜더링이 발생하면 성능 저하로 이어질 수 있다. 이게 앱이 작을 때는 별로 티가 안나지만, 점점 규모가 커질수록 퍼포먼스가 뚝뚝 떨어지는 게 보인다. 그러니, 초반에는 별로 신경쓰지 않고 개발하더라도 어느 시점에는 최적화가 필요한 시기가 오니 미리미리 익혀두는 게 좋다고 생각한다.
다만 무조건 재랜더링 방지 전략을 쓰는게 능사는 아니다. 왜냐하면 이건 공짜 점심이 아니고, 어디까지나 메모리 리소스를 쓰는 행위이기 때문. 선택과 집중을 잘하자.
🔥 비교 정리
최적화 기법
|
사용 목적
|
언제 사용하는가?
|
React.memo
|
컴포넌트 재랜더링 방지
|
프롭스가 변경되지 않을 때만 렌더링하고 싶을 때
|
useCallback
|
함수 재생성 방지
|
자식 컴포넌트에 함수를 프롭스로 넘길 때
|
useMemo
|
연산량이 많은 값 최적화
|
값이 변경될 때만 연산을 수행하고 싶을 때
|
useRef
|
렌더링 없이 값 유지
|
값은 유지하되 재랜더링은 하지 않고 싶을 때
|
Context 최적화
|
Context 값 변경 최소화
|
useContext 사용 시 불필요한 렌더링을 방지하고 싶을 때
|
2. 리액트 재랜더링의 주요 원인
- 상태(State) 변경: useState의 값이 변경되면 해당 컴포넌트와 하위 컴포넌트가 다시 렌더링됨.
- 부모 컴포넌트의 재랜더링: 부모 컴포넌트가 변경되면 자식 컴포넌트도 자동으로 재랜더링됨.
- 새로운 프롭스(Props) 전달: 자식 컴포넌트가 이전과 다른 프롭스를 받으면 다시 렌더링됨.
- 컨텍스트(Context) 변경: useContext를 사용 중일 때, 컨텍스트 값이 변경되면 해당 값을 사용하는 모든 컴포넌트가 다시 렌더링됨.
- 객체 및 함수의 참조 변화: useMemo나 useCallback을 사용하지 않으면 불필요한 객체 생성으로 인해 재랜더링이 발생할 수 있음.
3. 최적화 방법
1) React.memo : 컴포넌트 메모화
React.memo를 사용하면 프롭스가 변경되지 않는 한 컴포넌트가 다시 렌더링되지 않도록 할 수 있다.
아래의 코드에서 Child 컴포넌트는 count가 변경될 때만 다시 렌더링된다. memo를 쓰지 않으면, text가 변경되도 다시 렌더링이 되니 골때리는 결과가 나오게 된다.
import React from "react";
// 자식 컴포넌트
const Child = React.memo(({ count }) => {
console.log("Child 렌더링");
return <div>카운트: {count}</div>;
});
// 부모 컴포넌트
const Parent = () => {
const [count, setCount] = React.useState(0);
const [text, setText] = React.useState("");
return (
<div>
<Child count={count} />
<button onClick={() => setCount(count + 1)}>증가</button>
<input value={text} onChange={(e) => setText(e.target.value)} />
</div>
);
};
2) useCallback : 함수메모화 (함수 재생성 방지)
부모 컴포넌트가 다시 렌더링될 때, 함수가 새롭게 생성되면 이를 프롭스로 받는 자식 컴포넌트도 재랜더링된다.
이를 방지하려면 useCallback을 사용한다. 개인적으로 가장 자주 쓰게 되는 메모화이지 않을까 싶다.
// 부모 컴포넌트
const Parent = () => {
const [count, setCount] = React.useState(0);
const increment = React.useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return <Child onIncrement={increment} />;
};
// 자식 컴포넌트
const Child = React.memo(({ onIncrement }) => {
console.log("Child 렌더링");
return <button onClick={onIncrement}>증가</button>;
});
3) useMemo: 변수메모화 (값 계산 최적화)
복잡한 계산이 필요한 경우, useMemo를 사용하여 불필요한 연산을 방지할 수 있다.
useMemo를 사용하면 num이 변경될 때만 연산이 수행된다.
const ExpensiveComponent = ({ num }) => {
const expensiveCalculation = React.useMemo(() => {
console.log("복잡한 연산 수행...");
return num * 2;
}, [num]);
return <div>결과: {expensiveCalculation}</div>;
};
4) useRef: 상태 변경 없이 값 유지
useRef를 사용하면 상태를 변경하지 않고도 값을 유지할 수 있어 불필요한 재랜더링을 방지할 수 있다.
아래 코드에서 useRef를 사용하면 renderCount 값이 변경되어도 컴포넌트가 다시 렌더링되지 않는다.
되짚어 보면, 만약 useRef가 없다면 renderCount값이 변경되면 Component가 가진 상태값이 변하므로, Component는 재랜더링되고 말 것이다. 이걸 막는 것이다.
const Component = () => {
const renderCount = React.useRef(0);
renderCount.current += 1;
return <div>렌더링 횟수: {renderCount.current}</div>;
};
5) Context 최적화
Context 값이 변경될 때, 해당 값을 구독하는 모든 컴포넌트가 다시 렌더링된다.
이를 막으려면 context를 여러 개로 분리하거나 useMemo를 사용해야 한다.
useMemo를 사용하면 UserContext.Provider의 값이 불필요하게 변경되는 것을 방지할 수 있다.
const UserContext = React.createContext();
const Parent = () => {
const [user, setUser] = React.useState({ name: "Alice", age: 25 });
const value = React.useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
<Child />
</UserContext.Provider>
);
};