리렌더링, 아마 React를 공부하거나 써본 사람이라면 한 번쯤 들어봤을 단어일 것이다. React에서의 리렌더링은 어떤 변화가 생겼을 때 컴포넌트 함수가 다시 실행되는 것을 뜻한다. 화면을 다시 그린다고도 표현한다.
그런데 리렌더링에 대해 검색해보면 항상 이런 말들이 따라온다.
“불필요한 리렌더링을 막아야 합니다.”
“최적화를 통해 렌더링을 줄이세요.”
그런 말을 듣다 보면 이런 생각이 든다.
불필요한 리렌더링은 뭔데? 필요한 리렌더링은 또 뭐고? 그 기준은 누가 정하지?
그리고 렌더링이 그렇게까지 성능에 치명적인 건가?
나도 처음엔 리렌더링만 생기면 뭔가 큰일 나는 줄 알았다. 하지만 React가 렌더링을 처리하는 방식을 조금 더 들여다보다 보니, 꼭 그렇게까지 걱정할 일은 아니라는 걸 알게 됐다.
그래서 이 글에서는 내가 리렌더링을 어떻게 이해하게 되었는지, 그리고 언제, 왜 리렌더링을 줄이려는 고민을 해야 하는지를 정리해보려고 한다.
React 공식 문서를 보면 React의 렌더링 과정은 이렇게 흘러간다.
그리고 이 사이사이에 React와 브라우저가 처리하는 작업들이 더 있다.
단계 | 설명 | 주체 |
---|---|---|
1. 상태 변경 트리거 | setState , useReducer 등으로 상태 변경 | 개발자 |
2. 렌더(Render) | 컴포넌트 함수 재실행 → Virtual DOM 생성 | React |
3. 가상 DOM 비교(Diff) | 이전과 현재 Virtual DOM 비교 | React |
4. 커밋(Commit) | 실제 DOM을 업데이트 | React |
5. 페인트(Paint) | 브라우저가 화면을 다시 그림 | 브라우저 |
여기서 중요한 점은, 실제 DOM을 바꾸는 건 오직 커밋 단계에서만 발생한다는 것이다.
즉, 컴포넌트가 리렌더링(render)됐다고 해서 곧바로 DOM이 바뀌는 건 아니다.
"Virtual DOM에서 바뀐 부분만 실제 DOM에 반영하는 과정"
이 단계에서 브라우저에 진짜 변화가 생긴다.
그러니까 상태가 바뀌어도 가상 DOM이 이전과 동일하다면, React는 커밋하지 않고 넘어간다.
많은 사람들은 "리렌더링을 줄여야 한다"라고 말하지만, 사실 리렌더링(render) 자체는 단순히 컴포넌트 함수의 재실행일 뿐이다.
그래서 생각한 것 만큼 비용이 크지 않다. 반면에 커밋(commit)은 실제 DOM을 바꾸는 작업이라 더 무겁다.
여기서 나는 한가지 의문이 들었다
렌더는 어차피 Virtual DOM 만들기고, 커밋이 진짜 DOM 바꾸는 건데,
그럼 커밋만 줄이면 되는 거 아닌가?
나는 이렇게 생각했는데, 알고 보니 중요한 전제가 있었다.
커밋은 렌더 없이는 일어날 수 없다.
렌더링을 통해 새로운 Virtual DOM이 생성되어야 React가 그것을 이전과 비교(diff)하고 커밋 여부를 판단할 수 있다.
즉, 렌더는 커밋의 선행 조건이다.
그래서 렌더가 가볍다고 해도, 불필요한 렌더가 많아지면 결국 커밋도 많아질 수 있다.
결국 리렌더링을 줄인다는 건 곧 커밋 비용을 줄이기 위한 출발점이기 때문에 리렌더링을 줄이자고 말하는 것이었다.
리렌더링은 무조건 줄여야 하는 것도, 무조건 방치해도 되는 것도 아니다.
중요한 건 줄여야 할 리렌더링’이 정말 필요한가?를 판단하는 기준이다.
내가 직접 겪었을 때 리렌더링을 줄여야 하는 상황은 이런 경우인 것 같다
- 관련 없는 컴포넌트까지 같이 리렌더링될 때
→ props가 바뀌지 않았는데도 계속 함수가 새로 생성되거나, context 전파 등으로 리렌더링 발생
- 사용자가 체감할 정도의 깜박임이나 끊김이 있을 때
→ 애니메이션, 입력 반응, 레이아웃 재배치 등에서 렉이 느껴짐
- 렌더 횟수가 지나치게 많다고 느껴질 때
→ 코드 구조상 단순한 인터랙션인데도 너무 많은 컴포넌트가 반응함
하지만 이런 상황이 의심되면, 막연히 useCallback, memo부터 붙이지 말고 먼저 측정부터 해보자.
왜냐하면, 실제로 렌더링이 자주 발생한다고 해도 그게 정말 병목으로 이어지는지, 혹은 사용자 경험에 영향을 줄 정도인지는 눈으로 직접 확인하기 전에는 알 수 없기 때문이다.
React는 공식 개발자 도구를 통해 리렌더링 여부를 시각적으로 확인할 수 있다.
컴포넌트 테두리가 초록색/파란색으로 깜빡이면 렌더링이 일어난 것이다.
DevTools의 Profiler 탭에서는 더 정밀한 측정이 가능하다.
Profiler
탭에서 Record 시작특히 렌더링 시간이 긴 컴포넌트가 보이면, 거기부터 리팩터링 또는 최적화 대상이 될 수 있다.
위 이미지는 나는 아이템 의 수량을 늘리는 + 버튼을 클릭한 후 React Profiler의 Flamegraph 탭에서 캡처한 결과다.
왼쪽은 UI 화면, 오른쪽은 실제 어떤 컴포넌트가 얼마나 렌더링되었는지를 보여주는 시각화 트리다.
이 Flamegraph는 시간의 흐름에 따라 렌더링이 어떻게 발생했는지를 보여준다.
아래 이미지 순서는 1 → 2 → 3 → 4로 커밋이 순차적으로 일어난 것이다.
이 Flamegraph에서 우리가 주목할 수 있는 포인트는 다음과 같다:
이런 경우, 렌더링 자체가 심각한 병목은 아니지만 리스트나 아이템 수가 많아질 경우 큰 비용으로 이어질 수 있다.
이런 상황에서 다음과 같은 것을 확인해볼 수 있다.
- ProductListContainer가 매번 리렌더링되는 이유가 뭘까?
→ 부모 컴포넌트의 props나 context가 자주 바뀌고 있을 가능성
- 리스트 아이템 컴포넌트는 memo 처리되어 있나?
→ React.memo()로 감싸서 개별 아이템 리렌더링 최소화 가능
- 부모의 함수 props가 매번 새로 만들어지는 건 아닐까?
→ useCallback()으로 불필요한 재생성 방지 가능
그럴 필요는 없다고 생각한다.
Flamegraph나 DevTools를 통해 렌더링 상황을 확인해본다고 해서 모든 리렌더링을 막고, 모든 컴포넌트를 memo로 감싸야 한다는 뜻은 아니다.
오히려 그 반대다. 렌더링 시간이 길다거나 너무 자주 불필요한 컴포넌트가 같이 리렌더링된다거나 사용자가 체감할 정도의 지연이 있다거나, 이런 확실한 문제 상황이 보일 때만 최적화를 고민하면 된다.
렌더링 시간 0.1ms 차이로 React.memo 붙이는 건 대부분 의미 없다. 성능보다 코드 가독성이 더 중요할 수도 있다.
리렌더링은 React의 자연스러운 동작이다.
그걸 무조건 줄여야 할 대상이라고 보기보다는, 진짜 비용이 크고, 실제 문제가 보일 때만 조심스럽게 다루는 게 맞다고 생각한다.
DevTools나 Profiler 같은 도구를 활용해서 실제로 얼마나 렌더링이 일어나고, 어떤 컴포넌트가 반복적으로 비용을 유발하는지 확인하는 것만으로도 불필요한 최적화를 피할 수 있다.
React.memo, useCallback, useMemo 같은 도구는 분명 강력하지만, "언제 써야 하는지"보다도 "언제 안 써도 되는지"를 판단할 수 있는 능력이 더 중요하다고 느꼈다.
나는 예전엔 렌더링이 발생하면 "왜 발생했지? 리렌더링 줄여야 하나...?"를 먼저 떠올렸는데, 지금은 "이게 문제인가?"를 먼저 고민해봐야한다고 생각한다.
React의 리렌더링을 무조건 막는 게 목적이 아니라, 문제가 보일 때만, 그 원인을 정확히 파악하고 필요한 만큼만 최적화하는 것이 그게 진짜 최적화를 ‘잘하는 것’이라는 생각이 들었다.