React 성능 모델: 렌더와 재사용
렌더 트리거, memo/useCallback/useMemo 역할, 배칭·스케줄링 관점에서 성능을 해석
React 렌더링을 성능 관점에서 구조적으로 이해하기
React 성능 문제의 대부분은 “느리다”가 아니라 왜 다시 렌더되는지 모른다에서 시작된다.
React는 기본적으로 빠르다. 문제가 되는 건 렌더링 모델을 오해한 상태에서 최적화를 남용할 때다.
이 글의 목표는 하나다.
React 렌더링을
“DOM을 얼마나 적게 바꾸느냐”가 아니라
“무엇을 다시 계산하고, 무엇을 재사용하는가” 관점에서 이해하는 것
렌더링 vs DOM 업데이트: 가장 흔한 오해
개념
React에서 “렌더링”은 컴포넌트 함수 실행이다.
렌더링 = 컴포넌트 함수 호출 → JSX 계산
DOM 업데이트 = 계산 결과를 실제 DOM에 반영
이 둘은 완전히 다른 단계다.
왜 중요한가
많은 사람들이 이렇게 생각한다.
“렌더링이 일어나면 DOM이 다시 그려진다”
이건 틀렸다.
- 렌더링은 JS 연산
- DOM 업데이트는 비교(diff) 후 필요한 부분만 반영
React는 항상:
- 렌더링(계산)
- 이전 결과와 비교
- 달라진 부분만 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 구조
실무에서의 올바른 접근 순서
- 렌더링 원인 파악
- 무엇이 바뀌어서 렌더됐는가
- 계산 비용 확인
- 렌더 중 무거운 연산이 있는가
- 구조 개선
- state/props 설계가 적절한가
- 그 다음에 memo / useMemo / useCallback
정리: React 성능의 본질
- 렌더링 ≠ DOM 업데이트
- React는 계산 → 비교 → 최소 반영 모델
- memo 계열은 “필요할 때만”
- batching과 scheduling은 기본 제공 최적화
- 성능 문제의 대부분은 구조 문제
한 문장으로 요약하면:
React 성능은 최적화 기술보다 렌더링 모델을 얼마나 정확히 이해하느냐에 달려 있다.
