React 무한 렌더링 지옥 탈출: useMemo와 useCallback 완벽 최적화 아키텍처
[Troubleshooting] React 무한 렌더링 지옥 탈출: useMemo와 useCallback 아키텍처
"상위 컴포넌트에서 버튼 하나만 눌렀을 뿐인데, 연관도 없는 하위 컴포넌트 수십 개가 동시에 리렌더링(Re-rendering)되면서 브라우저 화면이 심하게 버벅거립니다. React.memo를 씌워봐도 전혀 소용이 없습니다."
React 프론트엔드 생태계에서 서비스 규모가 커질수록 100% 마주하게 되는 '렌더링 퍼포먼스 붕괴' 현상입니다. 리액트는 상태(State)가 변하면 화면을 다시 그린다는 직관적인 철학을 가지고 있지만, 이 철학 이면에 숨겨진 자바스크립트의 메모리 참조(Reference) 메커니즘을 이해하지 못하면 애플리케이션은 순식간에 무거워집니다.
초보자들은 화면 깜빡임을 잡겠다고 모든 변수와 함수에 무작정 최적화 훅(Hook)을 도배하여 오히려 메모리를 낭비하는 안티 패턴에 빠집니다. 본 가이드에서는 리액트 렌더링 트리와 메모리 할당의 상관관계를 해체하고, 꼭 필요한 곳에만 렌더링 방어막을 치는 무결점 최적화 아키텍처를 단호하게 제시합니다.
📌 이 글의 핵심 포인트
- 참조의 덫: 객체와 함수는 렌더링될 때마다 새로운 메모리 주소를 할당받아 하위 컴포넌트를 강제 업데이트시키는 원리
- useMemo: 무거운 연산 결괏값이나 참조형 데이터(객체, 배열)의 주소값을 캐싱하여 불필요한 재생성 방어
- useCallback: 자식 컴포넌트에게 Props로 내려보내는 이벤트 핸들러 함수의 재생성을 막는 렌더링 차단벽
1단계: 핵심 원인 분석 - 자바스크립트의 얕은 비교(Shallow Compare)
하위 컴포넌트에 React.memo를 씌우면, 부모로부터 받는 Props가 변경될 때만 리렌더링이 일어납니다. 그런데 왜 계속 리렌더링이 발생할까요?
부모 컴포넌트가 렌더링될 때, 내부에 선언된 const data = { id: 1 } 같은 객체나 const handleClick = () => {} 같은 함수는 내용이 완벽히 똑같아도 '새로운 메모리 주소'를 발급받아 새로 생성됩니다. 리액트는 Props의 내용물이 아니라 '메모리 주소'가 달라졌는지만 확인(얕은 비교)하므로, "어? 주소가 바뀌었네? 화면 다시 그려야지!"라고 착각하여 불필요한 렌더링 폭탄을 터뜨리는 것입니다.
2단계: 완벽한 트러블슈팅 - 메모리 주소 고정하기
이 착각을 막으려면 부모 컴포넌트가 다시 실행되더라도, 특정한 객체나 함수의 메모리 주소가 변하지 않도록 리액트 엔진의 어딘가에 안전하게 보관(캐싱)해야 합니다.
import { useMemo, useCallback } from 'react';
export default function ParentComponent({ userId }) {
// ❌ 최악의 패턴: 렌더링될 때마다 새로운 배열이 생성되어 자식을 괴롭힘
// const filterOptions = ['active', 'pending'];
// ✅ 완벽한 패턴: useMemo를 사용하여 배열의 메모리 주소를 고정시킴
const filterOptions = useMemo(() => ['active', 'pending'], []);
// ❌ 최악의 패턴: 렌더링될 때마다 함수가 새로 생성됨
// const onUserClick = () => { console.log(userId); };
// ✅ 완벽한 패턴: useCallback을 사용하여 함수의 메모리 주소를 고정시킴
// 의존성 배열에 userId를 넣어, 해당 값이 바뀔 때만 함수를 재생성
const onUserClick = useCallback(() => {
console.log("Clicked User:", userId);
}, [userId]);
return <ChildComponent options={filterOptions} onClick={onUserClick} />;
}
3단계: 엔터프라이즈 아키텍트의 시선 - 과유불급
그렇다면 모든 변수와 함수를 다 캐싱해야 할까요? 절대 아닙니다. useMemo와 useCallback 자체도 메모리를 차지하고 연산을 수행하는 비용(Cost)입니다. 원시 타입(String, Number)이나, 자식 컴포넌트에 Props로 넘기지 않고 자기 자신 안에서만 쓰는 함수에는 절대 최적화 훅을 사용하지 마십시오. 최적화의 비용이 렌더링 비용보다 커지는 '배보다 배꼽이 큰' 상황을 경계해야 합니다.
💡 핵심 정리 및 마무리
프론트엔드 성능 최적화는 맹목적으로 코드를 추가하는 것이 아니라, 자바스크립트 엔진과 리액트 프레임워크가 데이터를 평가하는 엇갈린 시선을 동기화하는 작업입니다. 값(Value)과 참조(Reference)의 차이를 완벽히 통제하여, 유저가 마우스 휠을 굴릴 때 프레임 드랍 하나 없이 매끄럽게 동작하는 궁극의 웹 애플리케이션을 완성하시기 바랍니다.
댓글
댓글 쓰기