Jeonghwan's Blog
순수 컴포넌트, 꼭 필요한 순간만 똑똑하게 리렌더링하기

순수 컴포넌트, 꼭 필요한 순간만 똑똑하게 리렌더링하기

React를 개발하다 보면 이런 경험 한 번쯤 해보시지 않으셨나요?

버튼 하나 눌렀을 뿐인데 화면에 있는 컴포넌트가 전부 다시 렌더링 된다?! 🤯

사실 화면에 눈에 띄는 변화가 없어도, 부모가 리렌더링되면 자식도 덩달아 리렌더링되는 경우가 많아요. 이럴 때 성능을 지켜주는 것이 바로 순수 컴포넌트(Pure Component) 예요. 이름은 단순하지만 원리를 제대로 이해하고 쓰면 앱 퍼포먼스를 크게 개선할 수 있어요.

순수 컴포넌트란?

순수 컴포넌트(Pure Component)는 동일한 stateprops가 주어졌을 때 항상 동일한 UI를 렌더링하는 컴포넌트예요.

  • stateprops가 바뀌면 → 리렌더링
  • stateprops가 그대로면 → 그냥 재활용

즉, "입력이 변하지 않으면 다시 그리지 않는 컴포넌트" 에요.

// UserProfile.tsx
// 순수 컴포넌트 ✅
import { memo } from 'react';
 
interface UserPropfileProps {
  name: string;
  age: number;
}
 
const UserProfile = memo(
  ({ name, age }: UserPropfileProps) => {
    return (
      <div>
        <p>이름: {name}</p>
        <p>나이: {age}</p>
      </div>
    );
  }
);

어떻게 이게 가능할까?

React의 순수 컴포넌트는 이전 렌더의 stateprops를 현재 값과 얕게 비교해요. 얕은 비교는 1단계만 비교한다는 뜻이에요.

  • 원시 타입(문자열/숫자/불리언): 값 자체 비교 → 값이 같으면 동일하다고 간주
  • 참조 타입(객체/배열/함수): “내용”이 아니라 참조(주소)만 비교 → 참조가 같아야 동일
'Park' === 'Park'  // true
32 === 32          // true
 
const userA = { 
  name: "Jeonghwan" 
};
 
const userB = { 
  name: "Jeonghwan" 
};
 
userA === userB    // false (내용 같아도 서로 다른 객체 참조)

원시 타입 props는 얕은 비교로 충분히 최적화되고, 객체/배열/함수 props는 참조가 바뀌면 변한 것으로 간주돼요.

코드 예시

Before: 불필요한 리렌더링 발생

// UserProfile.tsx
// 순수 컴포넌트 ❌
import { memo } from 'react';
 
interface UserPropfileProps {
  name: string;
  age: number;
}
 
export const UserProfile = ({ name, age }: UserPropfileProps) => {
  return (
    <div>
      <p>이름: {name}</p>
      <p>나이: {age}</p>
    </div>
  );
)
// MyPage.tsx
import { useState } from 'react';
import { UserPropfile } from './UserProfile'; 
 
function MyPage() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <button type="button" onClick={() => setCount((prev) => prev + 1)}>+</button>
      <UserProfile name="Jeonghwan" age={32} /> {/* 순수 컴포넌트 ❌ */}
    </div>
  );
}

count만 변해도 UserProfile이 계속 리렌더링 돼요.

After: React.memo로 최적화하여 순수 컴포넌트로 선언

// UserProfile.tsx
// 순수 컴포넌트 ✅
import { memo } from 'react';
 
interface UserPropfileProps {
  name: string;
  age: number;
}
 
const UserProfile = memo(
  ({ name, age }: UserPropfileProps) => {
    return (
      <div>
        <p>이름: {name}</p>
        <p>나이: {age}</p>
      </div>
    );
  }
);
// MyPage.tsx
import { useState } from 'react';
import { UserPropfile } from './UserProfile'; 
 
function MyPage() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <button type="button" onClick={() => setCount((prev) => prev + 1)}>+</button>
      <UserProfile name="Jeonghwan" age={32} /> {/* 순수 컴포넌트 ✅ */}
    </div>
  );
}

name, age가 원시 타입이므로 값이 같으면 count가 변하더라도 리렌더링을 건너뛰어요.

참조 타입 props를 넘기면 의미가 없을까?

다음과 같이 원시 타입이 아닌 참조 타입 props를 순수 컴포넌트에 넘기면 의미가 없을까요?

// UserProfile.tsx
// 순수 컴포넌트 ✅
import { memo } from 'react';
 
interface UserPropfileProps {
  hobbies: string[];
}
 
const UserProfile = memo(
  ({ name, age }: UserPropfileProps) => {
    return <div>{hobbies.join(', ')}</div>;
  }
);

무조건 의미 없는 것은 아니고, “어떻게 전달하느냐” 에 달려 있어요.

매 렌더마다 새 참조를 전달하는 경우(최적화 실패❌)

// MyPage.tsx
import { useState } from 'react';
import { UserPropfile } from './UserProfile'; 
 
function MyPage() {
  const [count, setCount] = useState(0);
  const hobbies = ['축구', '독서'];
 
  return (
    <div>
      <button type="button" onClick={() => setCount((prev) => prev + 1)}>+</button>
      <UserProfile hobbies={hobbies} /> {/* 순수 컴포넌트 ✅ */}
    </div>
  );
}

hobbies 배열은 렌더 때마다 새 배열이라 참조가 매번 달라져요. 결국 React.memo가 “변경됨”으로 판단하여 리렌더링 발생해요.

물론 hobbies와 같이 변하지 않는 상수 데이터는 컴포넌트 함수 바깥에 선언하는 것이 가장 깔끔해요. 이렇게 하면 매 렌더마다 새로 생성되는 문제를 방지할 수 있어요. 다만 위 코드는 순수 컴포넌트 활용을 설명하기 위한 예시일 뿐이에요.

참조 고정(안정화)해 전달 (최적화 성공✅)

// MyPage.tsx
import { useState, useMemo } from 'react';
import { UserPropfile } from './UserProfile'; 
 
function MyPage() {
  const [count, setCount] = useState(0);
  const hobbies = useMemo(() => ['축구', '독서'], []);
 
  return (
    <div>
      <button type="button" onClick={() => setCount((prev) => prev + 1)}>+</button>
      <UserProfile hobbies={hobbies} /> {/* 순수 컴포넌트 ✅ */}
    </div>
  );
}

useMemouseCallback을 활용하면 참조를 안정화시킬 수 있어서 React.memo 최적화가 효과적으로 작동해요.

순수 컴포넌트는 항상 좋은 것일까?

순수 컴포넌트가 항상 좋은 건 아니에요. React.memoPureComponent는 props가 바뀌지 않으면 리렌더링을 막아주지만, 그 자체로도 이전 props와 새로운 props를 비교하는 비용이 들어가요. 그래서 아주 가벼운 컴포넌트나, 변화가 잦은 컴포넌트라면 굳이 쓰지 않는 게 더 효율적일 때도 있어요.

특히 props에 배열이나 객체, 함수를 매번 새로 만들어 전달하면 참조가 계속 바뀌어서 결국 매번 리렌더링이 발생하기 때문에 memo를 써도 큰 의미가 없어요. 이를 해결하려면 useMemouseCallback으로 참조를 고정해줘야 하는데, 이마저도 과하면 코드만 복잡해질 수 있어요.

따라서 순수 컴포넌트는 무조건 적용하는 게 아니라, 렌더링 비용이 크거나 반복적으로 사용되는데 실제 변화는 드문 컴포넌트에 한정해서 쓰는 게 좋아요. 한마디로, “언제나”가 아니라 “알맞을 때만” 쓰는 게 성능 최적화의 정석이에요.

언제 쓰면 좋은가? (체크리스트)

  • 렌더 비용이 크거나, 자주 쓰이는 리스트 아이템/테이블 행 ✅
  • props가 잘 변하지 않는 정적 UI (카드, 아바타, 배지 등) ✅
  • 부모 컴포넌트가 자주 리렌더링되지만, 자식 컴포넌트는 값이 그대로인 경우 ✅

마무리

순수 컴포넌트는 React에서 성능 최적화를 시작할 때 가장 먼저 떠올릴 수 있는 방법이에요. 하지만 무조건 순수 컴포넌트로 만드는 것이 좋은 게 아니라, 렌더링 비용이 크고 실제로는 자주 변하지 않는 컴포넌트에 선택적으로 적용해야 진짜 효과를 볼 수 있어요. 결국 핵심은 "언제 적용하는 것이 효과적인지" 판단할 수 있는 안목이라고 생각해요.

이 글을 통해 순수 컴포넌트의 동작 원리와 한계를 이해하고, 여러분의 프로젝트에 상황에 맞게 최적화를 적용해 보세요.

참고 자료