1. 불필요한 렌더링을 멈춰라: React.memo로 성능 최적화하기
1.1 문제점
(1) 구조 요약
App
├─ A Component
│ └─ Message
└─ B Component
└─ List
└─ ListItem
(2) 문제 상황
- A 컴포넌트의 상태(
message)가 변경 - 그런데 B 컴포넌트까지 함께 리렌더링됨
1.2 해결 방법: React.memo
const Message = React.memo(({ message }) => {
return <p>{message}</p>;
});
const ListItem = React.memo(({ post }) => {
return (
<li key={post.id}>
<p>{post.title}</p>
</li>
);
});
const List = React.memo(({ posts }) => {
return (
<>
{posts.map(post => (
<ListItem key={post.id} post={post} />
))}
</>
);
});
2. React의 비교전략: 얕은 비교(Shallow Compare)
2.1 React는 얕은 비교를 기본으로 한다
이전 참조 === 다음 참조 ?
- 같으면 → 변경 없음
- 다르면 → 변경 있음
2.2 깊은 비교(Deep Compare)는 뭔데?
객체 내부의 값까지 전부 순회하면서 비교하는 방식
JSON.stringify(obj1) === JSON.stringify(obj2); // true
_.isEqual(obj1, obj2); // true
문제점
- 느림 (O(n))
- 순환 참조 위험
- 비교 기준이 애매함
- 렌더링 최적화보다 비용이 커질 수 있음
얕은 비교는 React가 빠르고 예측 가능하게 상태 변화와 렌더링 여부를 판단하기 위해 선택한 기본 전략이다.
3. 함수 props와 useCallback 이해하기
const B = ({ message, posts }) => {
console.log('B component is Rendering');
const testFunction = () => {};
return (
<div>
<h1>B Component</h1>
<Message message={message} />
<List posts={posts} testFunction={testFunction} />
</div>
);
};
const List = React.memo(({ posts, testFunction }) => {
console.log('List component is Rendering');
return (
<ul>
{posts.map(post => (
<ListItem key={post.id} post={post} />
))}
</ul>
);
});
3.1 왜 그냥 <List posts={posts} />인데 리렌더링이 되지?
❌ 내가 막혔던 생각
“List에 posts만 넘기는데, message 입력이 왜 List 리렌더링이랑 상관있지?”
✅ 정확한 이해
- message 입력 → 부모(B) 리렌더링
- 부모가 리렌더링되면 → 안에 있는 코드 전부 다시 실행
- 그 안에 있던 함수는 → 다시 만들어짐
const testFunction = () => {};
📌 List가 리렌더링된 이유는 posts가 아니라 함수였다
3.2 posts는 값인데, 왜 함수처럼 취급되는 것 같지?
❌ 헷갈린 지점
“message는 값이라 치고, posts도 뭔가 함수처럼 취급되는 느낌인데?”
✅ 정확한 이해
message→ 문자열 (primitive)posts→ 배열/객체 (reference)- 함수가 아님
posts.map(...) // 배열이니까 가능한 것
📌 문제는 posts가 아니라, props로 같이 내려간 함수
3.3 props로 내려줬다는 걸 React는 어떻게 알아?
❌ 헷갈린 지점
“React가 내부적으로 뭔가 추론하는 거 아냐?”
✅ 정확한 이해
JSX는 이렇게 바뀜:
<List posts={posts} />
⬇
React.createElement(List, { posts: posts })
3.4 B 안의 List랑 아래 정의된 List는 같은 거야?
❌ 헷갈린 지점
“위에 B가 있고 아래에 List가 있으니까 위계가 있는 거 아닌가?”
✅ 정확한 이해
- 둘 다 컴포넌트 정의
- 파일 위치는 위계와 무관
- 위계는 렌더링 시점에만 생김
const B = () => <List />;
B
└── List (실행 시점에만 생기는 관계)
3.5 정의는 B 아니야? List는 왜 정의처럼 말해?
❌ 헷갈린 지점
“정의가 둘인 것처럼 말해서 헷갈림”
✅ 정확한 이해
const B = (...) => {}→ 컴포넌트 정의const List = React.memo(...)→ memo 옵션이 붙은 컴포넌트 정의
📌 둘 다 정의
📌 List는 “렌더링 규칙”이 추가된 정의
3.6 “한 글자 입력할 때마다 왜 계속 렌더링돼?”
❌ 헷갈린 지점
“input 하나 치는데 왜 난리가 나지?”
✅ 정확한 이해
input 입력
→ setMessage
→ B 리렌더
→ 함수 새로 생성
→ List props 변경
→ List 리렌더
3.7 useCallback은 정확히 뭘 막는 거야?
❌ 헷갈린 지점
“useCallback이 렌더링을 막는다?”
✅ 정확한 이해
useCallback은 함수의 ‘참조’를 고정한다
const testFunction = useCallback(() => {}, []);
- 새 함수 생성 ❌
- 같은 함수 재사용 ✅
그래서,
prevProps.testFunction === nextProps.testFunction // true
3.8 “input이 message라는 걸 React가 어떻게 알아?”
❌ 헷갈린 지점
“React가 연결해주는 거 아냐?”
✅ 정확한 이해
<input
value={message}
onChange={e => setMessage(e.target.value)}
/>
📌 개발자가 직접 연결