React가 렌더링하는 방식
"렌더링"은 React에서 가장 자주 사용되면서도 가장 덜 살펴지는 용어 중 하나입니다. 상태 업데이트와 화면의 픽셀 변화 사이에서 실제로 일어나는 일을 이해하면 많은 것이 명확해집니다. 컴포넌트가 언제 실행되는지, 대부분의 리렌더링이 실제로는 비용이 거의 없는 이유, 그리고 성능 최적화가 정말 필요한 부분이 어디인지 알 수 있습니다. 이 장에서 이러한 개념을 정립할 것입니다. 다음 두 장인 메모이제이션과 코드 분할에서는 이를 실제로 적용해봅니다.
렌더링은 함수 호출입니다
React가 컴포넌트를 "렌더링"할 때, 컴포넌트 함수를 호출합니다.
function Counter() {
const [count, setCount] = useState(0)
console.log('Counter 렌더링됨') // React가 이 함수를 호출할 때마다 실행됩니다
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}이것이 전부입니다. 함수가 위에서 아래로 실행되고, 함수 내의 모든 계산이 다시 실행되며, UI가 지금 어떻게 보여야 하는지 설명하는 JSX를 반환합니다. 여기까지는 브라우저의 DOM과 무관합니다. 렌더링은 설명을 생성할 뿐, 페이지를 변경하지 않습니다.
이러한 구분이 중요한 이유는 두 작업의 비용이 엄청나게 다르기 때문입니다. JavaScript 함수를 호출하고 설명을 구성하는 것은 빠릅니다. 실제 DOM을 변경해서 브라우저에서 레이아웃 및 그리기 작업을 트리거하는 것이 비용이 많이 드는 부분입니다. React의 전체 설계는 저렴한 작업은 자유롭게 하고, 비싼 작업은 최대한 적게 하는 원칙에 기반합니다.
세 가지 단계
하나의 "리렌더링"은 세 가지 단계를 거칩니다. 보통 상태 업데이트가 트리거지만, 초기 마운트, 부모 컴포넌트의 렌더링, 새로운 context 값도 동일한 메커니즘을 실행합니다.
- 렌더링. React는 상태가 변경된 컴포넌트 함수를 호출한 후, 그것이 반환하는 모든 컴포넌트를 재귀적으로 호출합니다. 결과는 새로운 가상 DOM입니다. 즉, UI의 해당 부분에 대한 가볍고 메모리 상의 설명입니다.
- 재조정. React는 diffing 알고리즘을 실행하여 새로운 가상 DOM을 이전 가상 DOM과 비교하고, 정확히 무엇이 변경되었는지 파악합니다.
- 커밋. 차이점, 오직 차이점만 실제 DOM에 적용됩니다. 이것이 사용자가 실제로 볼 수 있는 부분입니다.
이 과정을 설명하기에 좋은 비유가 있습니다. 건축가가 건물을 개선할 때, 먼저 청사진을 다시 그립니다(렌더링). 그 다음 새 청사진이 실제로 필요한 변경 사항을 나열합니다(재조정). 그리고 나서야 시공팀이 건물에 손을 대게 됩니다(커밋). 청사진을 다시 그리는 것은 빠릅니다. 시간과 비용이 드는 것은 시공 단계이므로, 팀은 목록에서 허용하는 최소한의 작업만 수행합니다.
렌더링과 재조정은 빠르게 유지되고, 커밋은 diff가 찾은 것으로만 제한되기 때문에 React는 기본적으로 빠르게 느껴집니다. 무언가가 느려지면 Profiler가 어느 단계에서 문제가 발생했는지 알려주므로, 추측할 필요가 없습니다.
Counter 버튼을 클릭할 때마다 세 단계가 모두 실행됩니다. 로그가 출력되고, React가 결과를 비교하며, 커밋이 버튼의 텍스트 노드만 업데이트하고 그 주변의 다른 것은 건드리지 않습니다.
렌더링은 재귀적이고, 그것은 보통 괜찮습니다
컴포넌트가 렌더링될 때, React는 그것이 반환하는 모든 것을 렌더링하고, 그 컴포넌트들이 반환하는 모든 것을 렌더링하며, 각 가지를 끝까지 내려간 후 다음 가지로 이동합니다. 따라서 부모의 상태 변화는 기본적으로 그것이 변경된 상태를 props로 받는지 여부와 관계없이 모든 자식을 리렌더링합니다.
단계를 다시 분리하면 낭비처럼 보이지 않습니다. 자식 렌더링은 설명을 생성하는 함수 호출입니다. 재조정 중에 React는 그들 중 어느 것도 변경된 속성을 찾지 못하므로, 커밋은 그 부분 트리에 대해 DOM 작업을 할 필요가 없습니다. 사용자가 볼 수 있는 DOM 작업은 실제로 변경된 것으로만 범위가 제한됩니다. 아무것도 커밋하지 않는 부분 트리를 리렌더링하는 것은 정상이고 건강한 경우이며, 대다수의 컴포넌트에서는 측정하기에도 너무 저렴합니다.
두 가지 상황에서 문제가 됩니다. 컴포넌트가 렌더링 중에 비용이 많이 드는 작업을 수행하거나, 부분 트리가 너무 커서 저렴한 렌더링도 누적되는 경우입니다. 이것들이 정확히 메모이제이션이 다루는 경우들입니다. 증거가 있기 전에 그것에 손을 대려는 충동을 참으세요. 이제 측정 방법을 알아봅시다.
최적화하기 전에 측정하세요
React는 이미 빠르고, 재조정은 이미 DOM 업데이트를 최소한으로 유지합니다. 이 과정의 조언은 명확합니다. 기술을 적용하기 전에 성능 문제를 측정할 수 있어야 하고, 모든 최적화를 그것이 복잡하게 만드는 코드의 가독성과 비교해야 합니다. 유지보수할 수 없는 영리함도 비용입니다.
두 가지 도구가 대부분의 측정 필요를 충당합니다.
- 브라우저의 Performance 탭. 세션을 기록하고, 느린 상호작용을 수행한 후, 중단하고, 무엇이 실행되었는지와 얼마나 오래 걸렸는지 검사합니다. CPU 스로틀링(4배 또는 6배 슬로우다운을 시도해보세요)과 네트워크 스로틀링을 사용하면 빠른 개발 머신에서 사용자가 실제로 경험하는 느린 기기와 연결을 모방할 수 있습니다. 노트북에서는 보이지 않는 문제들이 즉시 나타납니다.
- React DevTools Profiler. React 팀의 브라우저 확장 프로그램으로, 자체 Profiler 탭에 있습니다. 같은 기록-작동-중단 흐름이지만, 출력이 React 관점입니다. 어떤 컴포넌트가 렌더링되었는지, 왜 렌더링되었는지, 각각 얼마나 오래 걸렸는지, 커밋이 얼마나 오래 걸렸는지 보여줍니다. React 앱을 진단하기 위해서는 보통 더 직접적인 도구입니다.
StrictMode: 개발을 위한 안전장치
StrictMode는 앱(또는 어떤 부분 트리) 주위에 감싸서 개발 전용 확인 사항들을 켤 수 있는 컴포넌트입니다. Setup이 main.jsx에서 <App />을 감싸는 방식입니다.
개발 중에 StrictMode는 의도적으로 컴포넌트 함수를 두 번 호출하며, React가 순수하기를 기대하는 다른 함수들도 마찬가지입니다. useState와 useReducer 초기화 함수, setter에 전달하는 업데이터, reducer, useMemo 계산식입니다. 각 effect를 여러 번 실행하고, 정리한 후 다시 설정하며, ref 콜백을 다시 연결합니다. 또한 더 이상 사용되지 않는 API에 대해서도 경고합니다. 프로덕션 빌드에서는 아무것도 하지 않으므로, 사용자가 보는 앱을 절대 느리게 하거나 이중으로 렌더링하지 않습니다.
두 번 실행하는 것이 폭탄처럼 들리지만, 그것이 잡아내는 것을 보면 이해가 됩니다. 컴포넌트 함수는 순수해야 합니다. 같은 입력이면 같은 출력, 렌더링 중에 부작용이 없어야 합니다. 두 번 렌더링하는 것이 한 번 렌더링하는 것과 다른 결과를 생성하면, 컴포넌트가 버그를 숨기고 있던 것입니다.
이 과정은 렌더링 중에 자신 밖에서 정의된 배열에 push를 호출하는 컴포넌트로 이를 보여줍니다. 페이지는 괜찮아 보이지만 상태가 변경되고 함수가 다시 실행되면 중복이 나타납니다. StrictMode 아래에서는 중복이 첫 로드에서 즉시 나타나고, 해결책(배열을 수정하기 전에 복사하거나, 작업을 렌더링 밖으로 이동)이 따라옵니다.
순수성은 업데이터도 포함합니다
setCount(c => c + 1)의 업데이터도 같은 순수성 요구사항을 충족해야 하며, 그 함수도 두 번 실행되기 때문입니다.
effect 재실행 테스트는 다른 쪽에서 같은 계약을 테스트합니다. Effects는 정리 규칙을 다루고, StrictMode에서 정리가 없으면 어떻게 되는지 보여줍니다. 지워지지 않은 setInterval은 2씩 증가하게 하고, 마운트될 때마다 타이머를 누수시킵니다.
개발에서 더 많은 버그를 보는 것이 처음에는 거꾸로 느껴지지만, 이것이 전체 포인트입니다. StrictMode가 드러내는 모든 버그는 이미 존재했던 것이며, 그렇지 않았으면 프로덕션에서 자신을 드러낼 때까지 기다렸을 것입니다.
버전 참고
effect 동작은 React 18에서 도입되었습니다. 마운트 시 StrictMode는 각 effect의 설정을 실행한 후 정리를 실행하고, 다시 설정을 합니다. 더 오래된 글들은 StrictMode를 컴포넌트 이중 렌더링으로만 설명합니다. 이 과정은 래퍼를 React.StrictMode로 씁니다. 명명된 import를 사용하면 <StrictMode>입니다.
개발할 때 앱을 StrictMode로 감싸세요. 개발에서 의도적으로 두 번 실행해서 조기에 버그를 찾는 데 도움을 주고, 완성된 앱에서는 자동으로 꺼집니다.
다음: 메모이제이션, 참조 동등성이 React가 건너뛸 수 있는 것을 결정하는 곳입니다.

