PJT_1. TicTacToe Application_4

React에서 렌더링 문제는 대부분 데이터가 아니라 참조를 잘못 다룬 문제다


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)}
/>

📌 개발자가 직접 연결