React.memo: 불필요한 재렌더링을 방지하여 성능을 최적화하기
React.memo는 컴포넌트의 불필요한 재렌더링을 방지하여 성능을 최적화하는 강력한 도구입니다.
하지만 모든 컴포넌트에 무조건 적용하기보다는 특정 조건에 맞게 사용하는 것이 중요합니다.
React.memo란?
React.memo는 고차 컴포넌트(Higher-Order Component)로, 컴포넌트를 메모이제이션하여 props가 변경되지 않은 경우 해당 컴포넌트를 재렌더링하지 않습니다.
즉, 부모 컴포넌트가 재렌더링되더라도 자식 컴포넌트의 props가 변경되지 않았다면 기존 렌더링 결과를 재사용합니다.
기본적으로 props는 얕은 비교(shallow comparison)를 통해 변경 여부를 판단합니다.
const MemoizedComponent = React.memo(MyComponent);
React.memo를 사용하기 좋은 경우
1. 부모의 재렌더링에 영향을 받는 자식 컴포넌트
부모 컴포넌트가 자주 재렌더링되지만, 자식 컴포넌트의 props가 변경되지 않는 경우 React.memo를 사용하여 불필요한 렌더링을 방지할 수 있습니다.
const Button = React.memo(({ onClick, label }: { onClick: () => void; label: string }) => {
console.log("Button rendered!");
return <button onClick={onClick}>{label}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<Button onClick={() => console.log("Clicked")} label="Click me" />
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
- 문제 없이 작동: Parent가 재렌더링되더라도 Button의 props가 변경되지 않으므로 다시 렌더링되지 않습니다.
2. 렌더링 비용이 큰 컴포넌트
컴포넌트가 복잡한 렌더링 로직을 포함하거나 대량의 데이터를 처리하는 경우, 불필요한 렌더링을 방지하여 성능을 개선할 수 있습니다.
const ExpensiveComponent = React.memo(({ items }: { items: number[] }) => {
console.log("ExpensiveComponent rendered!");
// 대규모 데이터를 처리하는 로직
const sum = items.reduce((acc, item) => acc + item, 0);
return (
<div>
<p>Sum of items: {sum}</p>
</div>
);
});
function Parent() {
const [count, setCount] = useState(0);
const items = [1, 2, 3, 4, 5]; // 변하지 않는 데이터
return (
<div>
<ExpensiveComponent items={items} />
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<p>Count: {count}</p>
</div>
);
}
- Parent 컴포넌트의 count가 변경되더라도 ExpensiveComponent는 props(items)가 동일하기 때문에 재렌더링되지 않습니다.
- items 배열이 변하지 않는 데이터이므로, React.memo로 최적화가 가능합니다.
3. props 변경 여부가 단순한 경우
React.memo는 기본적으로 얕은 비교를 수행하므로, props가 기본 타입(string, number 등)일 때 사용하기 적합합니다.
const SimpleComponent = React.memo(({ value }: { value: string }) => {
console.log("SimpleComponent rendered!");
return <p>Value: {value}</p>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<SimpleComponent value="Hello, world!" />
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<p>Count: {count}</p>
</div>
);
}
- Parent가 재렌더링되더라도 SimpleComponent의 props(value)가 변경되지 않으므로 재렌더링되지 않습니다.
- value는 문자열 타입으로, React.memo의 얕은 비교에 적합합니다.
React.memo를 사용하지 않아도 되는 경우
1. 간단하고 렌더링 비용이 낮은 컴포넌트
컴포넌트가 매우 간단하고 렌더링 비용이 낮다면 React.memo를 사용하는 것이 오히려 성능에 부정적인 영향을 미칠 수 있습니다. React.memo의 비교 작업 자체가 비용을 발생시키기 때문입니다.
const SimpleComponent = React.memo(() => {
console.log("Rendered!");
return <div>Simple Component</div>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<SimpleComponent />
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
문제점
- SimpleComponent는 재렌더링 비용이 거의 없으므로 React.memo를 사용하는 것이 오히려 불필요한 비교 작업을 추가해 성능을 저하시킬 수 있습니다.
해결책
React.memo를 제거하여 간소화합니다.
const SimpleComponent = () => {
console.log("Rendered!");
return <div>Simple Component</div>;
};
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<SimpleComponent />
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
2. props가 항상 변경되는 경우
부모 컴포넌트에서 props로 매번 새로운 객체나 함수를 생성해 전달한다면, React.memo를 사용하더라도 매번 재렌더링이 발생합니다. 이런 상황에서는 React.memo의 효과가 없습니다.
const Child = React.memo(({ onClick }: { onClick: () => void }) => {
console.log("Child rendered!");
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = () => console.log("Clicked");
return <Child onClick={handleClick} />;
}
문제점
- handleClick 함수는 Parent가 렌더링될 때마다 새로 생성되므로, Child는 항상 재렌더링됩니다.
해결책
useCallback을 사용해 함수 참조를 고정합니다.
const Child = React.memo(({ onClick }: { onClick: () => void }) => {
console.log("Child rendered!");
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => console.log("Clicked"), []);
return <Child onClick={handleClick} />;
}
3. props가 복잡한 객체나 배열인 경우
React.memo는 기본적으로 얕은 비교만 수행하므로, 중첩된 객체나 배열을 props로 전달하면 항상 변경된 것으로 간주될 가능성이 있습니다.
이로 인해 불필요한 재렌더링이 발생할 수 있습니다.
const ComplexChild = React.memo(({ data }: { data: { value: number } }) => {
console.log("ComplexChild rendered!");
return <div>Value: {data.value}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
const data = { value: count };
return (
<div>
<ComplexChild data={data} />
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
문제점
- 매 렌더링마다 data 객체가 새로 생성되므로, ComplexChild는 항상 재렌더링됩니다.
해결책 1: useMemo로 참조값 고정
const ComplexChild = React.memo(({ data }: { data: { value: number } }) => {
console.log("ComplexChild rendered!");
return <div>Value: {data.value}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
const data = useMemo(() => ({ value: count }), [count]);
return (
<div>
<ComplexChild data={data} />
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
해결책 2: Custom Comparison Function
const areEqual = (prevProps: { data: { value: number } }, nextProps: { data: { value: number } }) => {
return prevProps.data.value === nextProps.data.value;
};
const ComplexChild = React.memo(({ data }: { data: { value: number } }) => {
console.log("ComplexChild rendered!");
return <div>Value: {data.value}</div>;
}, areEqual);
function Parent() {
const [count, setCount] = useState(0);
const data = { value: count };
return (
<div>
<ComplexChild data={data} />
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}'개발 공부 > React' 카테고리의 다른 글
| React.FC (0) | 2025.01.30 |
|---|---|
| React.memo (3) - useMemo (0) | 2025.01.27 |
| React.memo (1) (1) | 2025.01.22 |
| NextAuth.js 요약 (1) | 2025.01.19 |
| NextAuth.js 사용자 인증 로직 (3) NextAuth 동작 방식 (0) | 2025.01.19 |