Profiler부터 memo까지 제대로 사용해보자!

2026. 4. 27. 06:23·React

React 렌더링 최적화 실전 - Profiler부터 memo까지 제대로 사용해보자!

React에서 성능 최적화를 이야기할 때 가장 많이 나오는 키워드는 useMemo, useCallback, React.memo입니다.

하지만 이들을 어떻게 올바르게 적용할 수 있을까요?

중요한 것은 어디가 느린지 먼저 확인하고, 그 원인을 분석한 뒤 필요한 부분에만 최적화를 적용하는 것입니다.

 

이 글에서는 React DevTools Profiler와 memo를 사용하여 불필요한 렌더링을 찾아 최적화하는 방법에 대해서 알아보겠습니다!


React에서의 렌더링의 조건

React useState 완전 이해하기에서 state에 관련된 렌더링 조건에 대해서만 다뤘습니다.

 

React 컴포넌트는 다음 네 가지 상황에서 렌더링됩니다.

  • state가 변경될 때
  • props가 변경될 때
  • 부모 컴포넌트가 렌더링될 때
  • context 값이 변경될 때

또한 중요한 점은, 렌더링은 단순히 함수가 다시 실행되는 것이지 곧바로 DOM이 변경되는 것은 아니라는 점입니다.

React는 이전 결과와 비교하여 실제 변경이 필요한 부분만 DOM에 반영합니다.


렌더링이 문제되는 순간

모든 렌더링이 문제되는 것은 아닙니다. 다음과 같은 경우에만 성능 문제가 발생합니다.

  • 렌더링 비용이 큰 컴포넌트가 포함된 경우
  • 리스트, 차트, 복잡한 UI가 함께 렌더링되는 경우
  • 불필요하게 넓은 범위의 state가 존재하는 경우

예를 들어, 입력창 하나 때문에 페이지 전체가 다시 렌더링되면서 차트나 대형 리스트까지 같이 렌더링된다면 이는 비효율적인 구조가 됩니다.


React DevTools Profiler 활용법

React DevTools의 Profiler는 렌더링 성능을 분석하기 위한 도구입니다.

어떤 컴포넌트가 렌더링되었는지, 그리고 그 렌더링에 얼마나 시간이 걸렸는지를 확인할 수 있습니다.

 

기본적인 사용 방법은 다음과 같습니다.

 

먼저 설치를 진행합니다. 설치링크

 

Profiler 탭에서 Record 버튼을 누릅니다.

 

사용자가 실제로 수행하는 인터랙션을 재현합니다.

 

Record를 중지하고 결과를 확인합니다.

 

Profiler에서는 다음 정보를 확인할 수 있습니다.

  • 어떤 컴포넌트가 렌더링되었는지
  • 각 컴포넌트의 렌더링 시간
  • 렌더링이 발생한 원인

Profiler에서 가장 중요하게 봐야 할 부분은 두 가지입니다.

  • 첫째는 어떤 컴포넌트가 렌더링되었는지입니다.
  • 둘째는 그 렌더링이 얼마나 비용이 큰지입니다.

Flamegraph에서는 컴포넌트의 렌더링 시간을 시각적으로 확인할 수 있으며, 넓고 진한 블록일수록 비용이 큰 컴포넌트입니다.

 

Ranked 탭에서는 렌더링 시간이 긴 컴포넌트를 순위로 확인할 수 있어 병목을 빠르게 찾는 데 유용합니다.

 

또한 특정 컴포넌트를 클릭하면 해당 컴포넌트가 왜 렌더링되었는지도 확인할 수 있습니다.
props 변경인지, state 변경인지, 부모 렌더링 때문인지 파악하는 것이 핵심입니다.


실제로 Profiler를 사용해보자!

제가 진행했던 '마음모음' 프로젝트의 2차 인증 비밀번호 입력화면입니다.

 

다음은 PIN.tsx 페이지의 코드 일부이며, 컴포넌트 상단에서 입력한 비밀번호에 대한 state를 관리하기 때문에,

비밀번호가 입력되면 페이지 전체가 재렌더링 되는 상태였습니다.

const [password, setPassword] = useState(['', '', '', '']);
export default function PIN() {
  const [password, setPassword] = useState(['', '', '', '']);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  
  ...

  const handleSubmit = () => {
    verifyPassword({
      data: { secondaryPassword: password.join('') },
    });
  };

  return (
    <SubPageLayout title="2차 비밀번호 입력하기" backTo={ROUTE.LOGIN}>
      <div className="flex w-full flex-col items-center">
        <div className="mt-6 flex h-auto flex-col items-center gap-4 px-6">
          <div className="h-51">
            <img src={lockGif} alt="" className="h-auto w-51 object-cover" />
          </div>
        </div>
        <h1 className="text-title-2-semibold mt-5 text-center whitespace-pre-line text-gray-800">
          소중한 마음 기록을{'\n'}보호하고 있어요
        </h1>
        <h1 className="text-body-3-regular mt-1 mb-6 text-gray-400">현재 비밀번호를 입력해주세요</h1>
        <InputOTP
          value={password}
          onChange={setPassword}
          isError={!!errorMessage}
          onErrorReset={() => setErrorMessage(null)}
        />
        {errorMessage && <h1 className="text-caption-1-regular mt-3 text-red-500">{errorMessage}</h1>}
      </div>

      <Button
        className="fixed right-0 bottom-0 left-0 mx-6 mb-6 flex justify-center gap-3"
        variant="primary"
        appearance="filled"
        size="lg"
        onClick={handleSubmit}
        disabled={!password.every((v) => v) || isPending}
        data-submit-btn
      >
        <p className="text-body-2-semibold">다음</p>
      </Button>
    </SubPageLayout>
  );
}

 

 

실제로 PIN 내부에 있는 Input에 값을 입력할경우 페이지 전체가 리렌더링 되는 모습을 볼 수 있습니다.
SubPageLayout 내부의 모든 컴포넌트가 렌더링 되는 모습입니다.

export default function PIN() {
  
  ...

  const [formErrorMessage, setFormErrorMessage] = useState<string | null>(null);

  const handleSubmit = useCallback(
    (password: string[]) => {
      verifyPassword({
        data: { secondaryPassword: password.join('') },
      });
    },
    [verifyPassword]
  );

  return (
    <SubPageLayout title="2차 비밀번호 입력하기" backTo={ROUTE.LOGIN}>
      <div className="flex w-full flex-col items-center">
        <div className="mt-6 flex h-auto flex-col items-center gap-4 px-6">
          <div className="h-51">
            <img src={lockGif} alt="" className="h-auto w-51 object-cover" />
          </div>
        </div>
        <h1 className="text-title-2-semibold mt-5 text-center whitespace-pre-line text-gray-800">
          소중한 마음 기록을{'\n'}보호하고 있어요
        </h1>
        <h1 className="text-body-3-regular mt-1 mb-6 text-gray-400">현재 비밀번호를 입력해주세요</h1>
        <PinForm
          onSubmit={handleSubmit}
          errorMessage={formErrorMessage}
          onErrorReset={() => setFormErrorMessage(null)}
          isPending={isPending}
        />
      </div>
    </SubPageLayout>
  );
}

type PinFormProps = {
  onSubmit: (password: string[]) => void;
  errorMessage: string | null;
  onErrorReset: () => void;
  isPending: boolean;
};

function PinForm({ onSubmit, errorMessage, onErrorReset, isPending }: PinFormProps) {
  const [password, setPassword] = useState(['', '', '', '']);
  const isPasswordComplete = password.every(Boolean);

  return (
    <>
      <InputOTP value={password} onChange={setPassword} isError={!!errorMessage} onErrorReset={onErrorReset} />

      {errorMessage && <h1 className="text-caption-1-regular mt-3 text-red-500">{errorMessage}</h1>}

      <Button
        className="fixed right-0 bottom-0 left-0 mx-6 mb-6 flex justify-center gap-3"
        variant="primary"
        appearance="filled"
        size="lg"
        onClick={() => onSubmit(password)}
        disabled={!isPasswordComplete || isPending}
        data-submit-btn
      >
        <p className="text-body-2-semibold">다음</p>
      </Button>
    </>
  );
}

 

따라서 PinForm 컴포넌트를 새로 만들고, 그 안에서 state를 관리하기로 하였다.

 

이제 PIN을 입력하더라도 PinForm 내부만 다시 재렌더링이 발생하는 모습을 볼 수 있습니다.

 

물론 기존 구조에서, 이미지나 텍스트도 함께 렌더링되지만 실제 비용은 거의 발생하지 않는다는 것( 4ms )입니다.

React는 동일한 JSX 결과에 대해서는 DOM을 다시 생성하지 않으며, 이미지 역시 동일한 src를 유지하는 경우 재요청이 발생하지 않습니다.

 

따라서 극적인 최적화가 이뤄지지는 않을 것이며, 반드시 필요한 최적화라고도 말할 순 없습니다.

 

하지만 지금은 단순한 이미지와 텍스트지만, 여기에 다음과 같은 요소가 추가된다면 상황이 달라집니다.

  • 대형 리스트
  • 차트
  • 애니메이션
  • 복잡한 계산 로직

이 경우 입력값 하나 때문에 모든 요소가 다시 렌더링되는 구조는 비효율적이 됩니다.

무거운 계산이 필요한 컴포넌트가 함께 있을 경우 이러한 최적화는 필요할 것입니다.

 

Profiler를 어떻게 사용하는지 실제 코드에 적용하여 실습해볼 수 있었습니다.


이미지와 렌더링

렌더링 최적화를 고민할 때 자주 나오는 질문 중 하나는 이미지나 GIF가 다시 렌더링되면 비용이 큰 것이 아닌가 하는 점입니다.

React에서 렌더링은 함수 실행을 의미하며, 동일한 JSX 결과가 생성되면 실제 DOM은 변경되지 않습니다.
또한 이미지의 src가 동일하다면 브라우저 캐시를 활용하기 때문에 네트워크 요청이 다시 발생하지 않습니다.

따라서 단순한 이미지나 텍스트는 렌더링 비용 측면에서 큰 문제가 되지 않는 경우가 많습니다.
진짜로 신경 써야 할 대상은 리스트, 차트, 복잡한 계산과 같은 무거운 컴포넌트입니다.


useMemo, useCallback, React.memo - 언제 쓰고, 왜 써야 하는가

React에서 성능 최적화를 고민하다 보면 useMemo, useCallback, React.memo라는 세 가지 도구를 자주 접하게 됩니다. 하지만 이들을 단순히 “렌더링을 줄이는 기술”로 이해하고 사용하는 경우가 많습니다.

 

실제로는 각각의 역할이 명확하게 다르며,

잘못 사용하면 오히려 코드 복잡도만 증가하고 성능 개선 효과는 거의 없는 경우도 많습니다.

밑에서는 세 가지 도구가 각각 무엇을 하는지, 그리고 어떤 상황에서 사용하는 것이 적절한지를 실제 예시와 함께 설명합니다.


useMemo - 값을 기억하는 Hook

useMemo는 특정 계산 결과를 기억하고, 의존성이 변경되지 않는 한 이전 값을 재사용하는 Hook입니다.

 

즉, 동일한 계산을 반복하지 않도록 만들어주는 역할을 합니다.

 

React는 컴포넌트가 렌더링될 때마다 함수 내부 코드가 다시 실행됩니다.

이때 데이터 가공이나 필터링과 같은 연산이 포함되어 있다면, 렌더링이 반복될수록 불필요한 계산이 계속 발생하게 됩니다.

 

useMemo는 이러한 문제를 해결하기 위해 존재합니다.

예를 들어 리스트를 필터링하는 코드가 있다고 가정해보겠습니다.

const filteredList = items.filter(item => item.name.includes(keyword));
 

이 코드는 렌더링이 발생할 때마다 실행됩니다. 만약 items의 크기가 크다면 이 연산은 성능에 영향을 줄 수 있습니다. 이를 useMemo로 감싸면 다음과 같이 개선할 수 있습니다.

 
const filteredList = useMemo(() => {
  return items.filter(item => item.name.includes(keyword));
}, [items, keyword]);
 

이제 items나 keyword가 변경될 때만 필터링이 다시 실행되며, 그렇지 않은 경우에는 이전 결과를 그대로 사용합니다.

또한 useMemo는 단순히 계산 최적화뿐만 아니라 객체나 배열의 참조를 유지하는 용도로도 활용됩니다.

 

React에서는 객체와 배열이 매번 새로 생성되면 값이 같더라도 다른 것으로 판단되기 때문에, 이를 방지하기 위해 useMemo를 사용할 수 있습니다.


useCallback - 함수를 기억하는 Hook

useCallback은 함수 자체를 기억하는 Hook입니다. React에서는 컴포넌트가 렌더링될 때마다 함수도 함께 새로 생성됩니다.

대부분의 경우 이는 문제가 되지 않지만, 해당 함수가 자식 컴포넌트로 전달되는 경우에는 상황이 달라집니다.

예를 들어 다음과 같은 코드가 있다고 가정해보겠습니다.

 
<Child onClick={() => doSomething()} />
 

이 경우 렌더링이 발생할 때마다 새로운 함수가 생성되며, 자식 컴포넌트 입장에서는 props가 변경된 것으로 판단할 수 있습니다. 특히 자식 컴포넌트가 React.memo로 최적화되어 있다면, 이로 인해 불필요한 렌더링이 발생할 수 있습니다.

이를 해결하기 위해 useCallback을 사용할 수 있습니다.

 
const handleClick = useCallback(() => {
  doSomething();
}, []);

<Child onClick={handleClick} />
 

이제 handleClick 함수는 동일한 참조를 유지하게 되며, 자식 컴포넌트의 불필요한 렌더링을 방지할 수 있습니다.

다만 중요한 점은, useCallback은 모든 함수에 적용해야 하는 것이 아니라 props로 전달되는 함수이며, 해당 컴포넌트가 memoization되어 있는 경우에만 의미가 있다는 점입니다.


React.memo - 컴포넌트를 기억하는 기능

React.memo는 컴포넌트를 감싸서, props가 변경되지 않은 경우 렌더링을 건너뛰도록 하는 기능입니다. 이는 함수형 컴포넌트에서 사용할 수 있는 최적화 방법입니다.

const Child = memo(function Child({ value }) {
  return <div>{value}</div>;
});

 

이렇게 작성하면 value가 변경되지 않는 한 Child 컴포넌트는 다시 렌더링되지 않습니다.

React.memo는 특히 리스트 아이템처럼 동일한 구조의 컴포넌트가 반복되는 경우나, 렌더링 비용이 큰 컴포넌트에 적용할 때 효과적입니다.

예를 들어 리스트가 다음과 같이 구성되어 있다고 가정해보겠습니다.

 
const ListItem = memo(({ item }) => {
  return <li>{item.name}</li>;
});
 

이 경우 부모 컴포넌트가 렌더링되더라도 item이 변경되지 않는 한 각 ListItem은 다시 렌더링되지 않습니다.


언제 사용해야 하는가

세 가지 도구를 사용할 때 가장 중요한 기준은 “필요할 때만 사용한다”는 것입니다.

 

useMemo는 계산 결과를 캐싱하기 위한 Hook입니다.

filter, sort, map과 같이 비용이 큰 연산이 반복될 때 사용합니다.

또한 객체나 배열을 props로 전달할 때 참조를 안정화하기 위해 사용할 수 있습니다.

 

useCallback은 함수 참조를 유지하기 위한 Hook입니다.

함수를 props로 전달할 때, 특히 React.memo와 함께 사용할 때 의미가 있습니다.
불필요하게 함수가 새로 생성되는 것을 방지하여 자식 컴포넌트의 리렌더링을 줄입니다.

 

React.memo는 props가 변경되지 않았을 경우 렌더링을 건너뛰는 기능입니다.
렌더링 비용이 큰 컴포넌트나, 리스트 아이템처럼 반복적으로 렌더링되는 컴포넌트에 적용하는 것이 효과적입니다.

중요한 점은, 이 세 가지 도구는 모두 “렌더링을 줄이기 위한 수단”이 아니라 이미 확인된 병목을 해결하기 위한 도구라는 것입니다.


마무리

React 렌더링 최적화의 핵심은 단순히 렌더링을 줄이는 것이 아니라, 렌더링의 범위와 비용을 통제하는 것입니다.

 

모든 컴포넌트에 memo를 적용하는 것이 아니라, Profiler를 통해 병목을 찾고 그 부분만 정확하게 최적화하는 것이 중요합니다.

결국 좋은 최적화는 다음과 같이 정리할 수 있습니다.

 

렌더링을 막는 것이 아니라, 불필요한 렌더링과 비용이 큰 렌더링을 구분하고 제어하는 것이 중요합니다.

 

오늘도 읽어주셔서 감사합니다!

'React' 카테고리의 다른 글

폴더 구조와 아키텍처에 대해서 알아보자!  (0) 2026.05.17
에러 핸들링 & 안정성 - 프론트엔드 에러 처리  (0) 2026.05.04
TanStack Query 에 대해서 알아보기!  (0) 2026.04.04
상태 관리 전략 비교 - Context API, Zustand, Redux Toolkit 중 무엇을 선택해야 할까?  (0) 2026.03.23
React useState 완전 이해하기  (0) 2026.03.21
'React' 카테고리의 다른 글
  • 폴더 구조와 아키텍처에 대해서 알아보자!
  • 에러 핸들링 & 안정성 - 프론트엔드 에러 처리
  • TanStack Query 에 대해서 알아보기!
  • 상태 관리 전략 비교 - Context API, Zustand, Redux Toolkit 중 무엇을 선택해야 할까?
수달군
수달군
  • 수달군
    수달 코딩 공장
    수달군
  • 전체
    오늘
    어제
    • 분류 전체보기 (21)
      • React (10)
      • Next.js (7)
      • TypeScript 딥 다이브! (1)
      • 웹 기초 이론 (0)
      • 코딩 테스트 준비 (1)
        • Python 기본 (1)
      • AI 도구들 (0)
      • 프로젝트 회고 (0)
      • 자료구조 (0)
      • 일상 (0)
      • 해외 여행 (0)
      • 국내 여행 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    입력
    입력받기
    coding
    코딩
    입력값
    it
    input
    python
    파이썬
    파이썬 초보
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
수달군
Profiler부터 memo까지 제대로 사용해보자!
상단으로

티스토리툴바