학습 공간 개념 · 판단 · 실험

Frontend Performance Lab

React

메모이제이션과 렌더 비용 진단

메모이제이션은 측정된 반복 계산과 재렌더 비용을 줄이는 최적화 도구이며 동작 정확성의 전제가 아닙니다.

이럴 때 필요해요

비교 비용보다 절약 이득이 큰 지점을 프로파일링으로 찾고 오래된 클로저를 만드는 잘못된 최적화를 피합니다.

어떻게 동작하나요?

검색창에 글자를 하나 입력할 때마다 아래의 수천 개짜리 목록이 매번 다시 계산되어 타이핑이 버벅인다면 어떻게 해야 할까요? React.memo는 부모가 다시 렌더링될 때 자식에게 전달된 props가 이전과 같으면 자식의 렌더링 계산을 건너뛸 기회를 줍니다(클래스 컴포넌트에서는 PureComponent와 shouldComponentUpdate가 비슷한 역할을 합니다). 단, 자식 자신의 state나 구독 중인 Context가 바뀌면 memo와 상관없이 다시 렌더링됩니다.

이때 useMemo는 무거운 함수의 '계산 결과값'을, useCallback은 자식에게 넘기는 '함수 참조 자체'를 의존성 배열 기준으로 재사용합니다. 하지만 부모가 매 렌더마다 새로운 객체 리터럴이나 배열을 만들어 넘기면 얕은 비교가 매번 실패해 메모이제이션 효과가 사라집니다. 또한 React는 필요할 때 메모이제이션 캐시를 비울 수 있으므로, 캐시가 없어져도 기능이 정상 동작하도록 순수한 계산에만 써야 합니다.

따라서 모든 코드에 습관적으로 memo와 useCallback을 붙이기보다, 먼저 React Profiler와 브라우저 Performance 탭으로 실제 느린 구간이 무거운 JS 계산인지, 불필요한 하위 트리 렌더인지, 과도한 DOM·레이아웃 비용인지 확인합니다. 상태를 실제로 쓰는 컴포넌트 가까이로 내리고 큰 목록을 가상화(Windowing)해 작업량 자체를 줄인 뒤, 반복 비용이 남는 곳에 메모이제이션을 적용하는 순서가 효과적입니다.

코드 예제

동작을 설명하는 읽기 전용 예제입니다.

example.jsx
import { memo, useMemo } from "react";

const List = memo(function List({ items }) {
  return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
});

export default function SearchResults({ items, query }) {
  const visible = useMemo(
    () => items.filter(item => item.name.includes(query)),
    [items, query]
  );
  return <List items={visible} />;
}
jsx · 읽기 전용13줄

Hooks는 부모 컴포넌트 안에서 호출하고 memo 컴포넌트는 모듈 범위에 선언합니다. items와 query가 같으면 필터 결과 참조를 재사용해 List의 props 비교에 활용할 수 있습니다. items를 매번 새로 만들면 이 전제가 달라집니다. 성능 이득은 프로파일로 확인해야 합니다.

자주 헷갈리는 포인트

  • 함수를 한 번만 만들겠다며 의존성 배열을 빈 배열([])로 고정해 옛날 props나 state를 읽는 버그를 만들지 않습니다.
  • React.memo가 하위 렌더링을 100% 절대 차단하거나 붙이기만 하면 무조건 페이지가 빨라진다고 단정하지 않습니다.

내 말로 설명해 보기

useCallback으로 이벤트 핸들러 함수를 고정했는데도 자식 컴포넌트의 리렌더링 비용이 줄어들지 않았다면 어떤 원인을 의심해 볼 수 있나요?

생각할 단서 함수 참조 하나 외에 자식의 다른 props와 state도 살펴보세요.

예시 답변 확인

설명에 들어갈 핵심

  • 함께 전달하는 다른 props가 매번 새로 만들어지면 memo 비교를 통과하지 못할 수 있습니다.
  • 자식 컴포넌트 내부의 state나 Context 변화도 리렌더링을 유발합니다.
  • 최적화 필요성과 실제 효과는 React Profiler 측정으로 판단합니다.

예시 답변

useCallback은 함수 참조만 재사용합니다. 다른 객체 props를 매번 새로 만들거나 자식의 state/Context가 바뀌면 자식은 다시 렌더될 수 있습니다. memo로 감쌌는지와 실제 병목이 자식 렌더인지도 확인해야 합니다.

문장을 그대로 외우기보다 예제의 값과 실행 흐름을 떠올리며 설명해 보세요.

실전 시나리오에 적용해 보기

함께 읽으면 좋은 개념

관련 면접 질문 5개

PureComponent는 무엇을 비교하나요?

PureComponent는 클래스 컴포넌트에서 이전 props·state와 다음 props·state를 얕게 비교(shallow comparison)해, 변경이 없으면 재렌더를 건너뛰도록 돕는 컴포넌트입니다. 최상위 프로퍼티의 참조나 원시값만 비교하기 때문에 중첩된 객체 내부를 직접 수정(mutation)하면 변경을 감지하지 못하고, 반대로 부모가 매번 새 객체나 인라인 함수를 만들어 넘기면 비교 최적화가 무력화됩니다. 따라서 불변성 유지와 실제 렌더 비용 확인이 선행되어야 효과를 볼 수 있습니다.

꼬리질문과 참고자료 →

shouldComponentUpdate는 어떤 용도로 사용하나요?

클래스 컴포넌트의 shouldComponentUpdate는 다음 props와 state를 받아 이번 업데이트에서 render 호출을 건너뛸지 결정하는 성능 최적화용 수명주기 메서드입니다. 기본 동작은 항상 true를 반환해 다시 그리는 것이며, 특정 프로퍼티만 비교해 불필요한 반복 렌더 비용을 줄이고 싶을 때 제한적으로 구현합니다. 하지만 비교 로직에서 함수 props나 상태 조건을 하나라도 빠뜨리면 화면이 갱신되지 않는 버그가 생기므로, 프로파일링으로 병목을 확인한 뒤 신중히 적용하거나 PureComponent를 우선 검토합니다.

꼬리질문과 참고자료 →

메모이제이션은 언제 이득이 있나요?

메모이제이션은 동일한 입력에 대한 계산 결과나 컴포넌트 출력, 함수 참조를 재사용해 반복 실행 비용을 줄이는 최적화 기법입니다. 모든 곳에 습관적으로 적용하기보다, 입력값이 자주 바뀌지 않으면서 재계산 비용이나 하위 트리 재렌더 비용이 의존성 비교 및 캐시 유지 비용보다 뚜렷하게 클 때 실질적인 이득이 있습니다. 따라서 먼저 React Profiler 등으로 실제 병목 지점을 확인한 뒤 적용하고, 캐시가 비워지더라도 기능이 올바르게 동작하도록 순수한 계산에만 사용해야 합니다.

꼬리질문과 참고자료 →
나머지 질문 2개

공식 참고 자료

← React