Post

React 성능 모델: 렌더와 재사용

렌더 트리거, memo/useCallback/useMemo 역할, 배칭·스케줄링 관점에서 성능을 해석

React 성능 모델: 렌더와 재사용

React 렌더링을 성능 관점에서 구조적으로 이해하기

React 성능 문제의 대부분은 “느리다”가 아니라 왜 다시 렌더되는지 모른다에서 시작된다.
React는 기본적으로 빠르다. 문제가 되는 건 렌더링 모델을 오해한 상태에서 최적화를 남용할 때다.

이 글의 목표는 하나다.

React 렌더링을
“DOM을 얼마나 적게 바꾸느냐”가 아니라
“무엇을 다시 계산하고, 무엇을 재사용하는가” 관점에서 이해하는 것


렌더링 vs DOM 업데이트: 가장 흔한 오해

개념

React에서 “렌더링”은 컴포넌트 함수 실행이다.

렌더링 = 컴포넌트 함수 호출 → JSX 계산
DOM 업데이트 = 계산 결과를 실제 DOM에 반영

이 둘은 완전히 다른 단계다.

왜 중요한가

많은 사람들이 이렇게 생각한다.

“렌더링이 일어나면 DOM이 다시 그려진다”

이건 틀렸다.

  • 렌더링은 JS 연산
  • DOM 업데이트는 비교(diff) 후 필요한 부분만 반영

React는 항상:

  1. 렌더링(계산)
  2. 이전 결과와 비교
  3. 달라진 부분만 DOM에 반영

즉, 렌더링이 잦다고 해서 DOM이 잦게 바뀌는 건 아니다.

언제 문제가 되는가

  • “렌더가 많다 = 성능 문제”라고 오해
  • 실제 병목은 DOM이 아니라 렌더 중 무거운 계산
  • 불필요한 최적화(memo 남용)로 코드 복잡도만 증가

렌더링이 발생하는 진짜 트리거

React에서 렌더링이 발생하는 경우는 명확하다.

  • state 변경
  • props 변경
  • 부모 컴포넌트 렌더
  • context 값 변경

이 중 가장 흔한 오해는 이것이다.

“자식 컴포넌트는 자기 state가 바뀔 때만 렌더된다”

아니다. 부모가 렌더되면 자식도 기본적으로 다시 렌더된다.


memo: “렌더 결과 재사용” 도구

개념

memo는 컴포넌트의 렌더 결과(JSX) 를 재사용한다.

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

왜 중요한가

부모 렌더 → 자식 렌더는 기본 동작이다. memo는 이렇게 말한다.

“props가 이전과 같다면 이 컴포넌트는 다시 계산할 필요가 없다”

즉, memo는 렌더링 차단 도구다.

언제 문제가 되는가

1) props가 매번 새로 만들어지는 경우

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

이 경우:

  • 함수는 매 렌더마다 새 참조
  • memo는 무력화

2) 가벼운 컴포넌트에 memo 남용

  • 비교 비용 > 렌더 비용
  • 가독성 하락
  • 디버깅 난이도 상승

원칙

memo는 “렌더 비용이 충분히 큰 컴포넌트”에만


useCallback: 함수의 “정체성”을 고정

개념

useCallback은 함수를 캐싱한다.

1
2
3
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

왜 중요한가

JS에서 함수는 값(참조) 이다.

  • 렌더마다 새 함수 생성
  • props로 전달 시, 자식 입장에선 “값 변경”

useCallback은 이렇게 말한다.

“의존성이 안 바뀌면 같은 함수 참조를 계속 써라”

언제 문제가 되는가

  • 모든 함수에 무작정 useCallback
  • 의존성 관리가 더 복잡해짐
  • 실제 성능 개선은 미미

핵심 연결

  • useCallback은 memo를 살리기 위한 도구
  • 단독 사용은 의미 없음

useMemo: 값 계산의 재사용

개념

useMemo계산 결과를 캐싱한다.

1
2
3
const sortedItems = useMemo(() => {
  return items.slice().sort(compare);
}, [items]);

왜 중요한가

렌더링 중 무거운 계산이 있을 때:

  • 렌더마다 동일 계산 반복
  • JS 연산이 병목

useMemo는:

“의존성이 안 바뀌면 계산 결과를 재사용”

언제 문제가 되는가

  • 단순 계산에 useMemo
  • 의존성 관리 실수 → stale 값
  • 코드 복잡도 증가

원칙

useMemo는 “비싼 계산”에만


memo / useCallback / useMemo 관계 정리

도구캐싱 대상목적
memo컴포넌트 렌더 결과불필요한 렌더 차단
useCallback함수 참조memo/의존성 안정화
useMemo계산 결과렌더 중 연산 비용 절감

이 셋은 세트로 이해해야 한다. 하나만 쓰면 효과가 반감되거나 없다.


batching: “여러 상태 변경을 한 번에 처리”

개념

React는 여러 state 업데이트를 하나의 렌더로 묶는다.

1
2
setA(1);
setB(2);

→ 렌더 1번

왜 중요한가

batching 덕분에:

  • 이벤트 핸들러 안에서 여러 setState를 써도
  • 렌더링은 한 번만 발생

React 18부터는:

  • 비동기 코드에서도 batching이 기본 적용

언제 문제가 되는가

  • setState 직후 값을 바로 쓰려고 할 때
  • “바로 반영될 거라 착각”
1
2
setCount(c => c + 1);
console.log(count); // 이전 값

이건 버그가 아니라 의도된 스케줄링이다.


scheduling: “언제 렌더할 것인가”

개념

React는 모든 업데이트를 즉시 처리하지 않는다.

  • 긴급한 업데이트
  • 덜 중요한 업데이트

를 구분해 스케줄링한다.

왜 중요한가

이 덕분에:

  • 사용자 입력이 끊기지 않는다
  • 큰 렌더링 작업도 UI가 멈추지 않는다

Concurrent 기능, transition, suspense는 전부 이 스케줄링 모델 위에 있다.

언제 문제가 되는가

  • 렌더 타이밍을 “즉시”라고 가정
  • 동기적 사고로 비동기 UI를 제어하려 할 때

성능 최적화를 남용하면 안 되는 이유

1) 복잡도는 즉시 증가한다

  • 의존성 배열
  • memo 경계
  • 참조 안정성

→ 코드 이해 비용 급증

2) 잘못된 최적화는 버그를 만든다

  • stale closure
  • 업데이트 누락
  • 예상치 못한 렌더 차단

3) 대부분의 앱은 최적화가 필요 없다

  • React 기본 렌더링은 충분히 빠르다
  • 병목은 대개:

    • 무거운 계산
    • 잘못된 state 설계
    • 불필요한 re-render 구조

실무에서의 올바른 접근 순서

  1. 렌더링 원인 파악
    • 무엇이 바뀌어서 렌더됐는가
  2. 계산 비용 확인
    • 렌더 중 무거운 연산이 있는가
  3. 구조 개선
    • state/props 설계가 적절한가
  4. 그 다음에 memo / useMemo / useCallback

정리: React 성능의 본질

  • 렌더링 ≠ DOM 업데이트
  • React는 계산 → 비교 → 최소 반영 모델
  • memo 계열은 “필요할 때만”
  • batching과 scheduling은 기본 제공 최적화
  • 성능 문제의 대부분은 구조 문제

한 문장으로 요약하면:

React 성능은 최적화 기술보다 렌더링 모델을 얼마나 정확히 이해하느냐에 달려 있다.


This post is licensed under CC BY 4.0 by the author.