React는 언제 리렌더링되는가?
방법을 알아보기 전에, React가 리렌더링을 수행하는 트리거를 명확히 이해해야 합니다.
-
State(상태) 변경: 컴포넌트의
state가 바뀌면 리렌더링됩니다. -
Props 변경: 부모로부터 전달받은
props가 바뀌면 리렌더링됩니다. -
부모 컴포넌트 리렌더링: 가장 빈번한 원인 중 하나로, 부모가 대대적으로 리렌더링되면 자식 컴포넌트도 기본적으로 함께 리렌더링됩니다.
핵심 최적화 기법 (Next.js & React)
① 컴포넌트 구조 분리
가장 비용이 적고 효과적인 방법입니다. 자주 변경되는 상태를 가진 부분을 별도의 하위 컴포넌트로 분리하여, 상태 변경의 영향 범위를 최소화합니다.
-
Bad (전체가 리렌더링됨)
// Input 입력할 때마다 대형 컴포넌트 전체가 리렌더링됨 export default function HeavyPage() { const [text, setText] = useState(""); return ( <div> <input value={text} onChange={(e) => setText(e.target.value)} /> <HeavyComponent /> {/* 무거운 컴포넌트가 계속 재실행됨 */} </div> ); } -
Good (상태 분리)
// 상태를 하위 컴포넌트에 가두기 function SearchInput() { const [text, setText] = useState(""); return <input value={text} onChange={(e) => setText(e.target.value)} />; } export default function HeavyPage() { return ( <div> <SearchInput /> <HeavyComponent /> {/* 이제 리렌더링되지 않음 */} </div> ); }
② React.memo를 통한 자식 컴포넌트 리렌더링 방지
부모 컴포넌트가 리렌더링되더라도, 자식 컴포넌트의 props가 변하지 않았다면 리렌더링을 건너뛰도록 설정합니다.
-
사용법
import { memo } from 'react'; const MyComponent = memo(function MyComponent({ value }) { return <div>{value}</div>; }); -
주의점:
memo는props를 얕은 비교(Shallow Comparison)합니다. 객체, 배열, 함수를props로 넘기면 매번 새로운 참조값이 생성되어memo가 작동하지 않습니다. 이때 필요한 것이 바로 아래의useMemo와useCallback입니다.
③ useMemo와 useCallback으로 참조 동일성 유지
-
useMemo: 컴포넌트가 리렌더링될 때마다 발생하는 복잡한 연산 결과값을 캐싱하거나, 자식에게 넘겨주는 객체/배열의 참조값을 유지할 때 사용합니다. -
useCallback: 컴포넌트가 리렌더링될 때마다 함수가 재생성되는 것을 방지하여, 자식 컴포넌트(memo적용된)에props로 전달될 때 불필요한 리렌더링을 막습니다.
import { useState, useMemo, useCallback } from 'react';
export default function SuperParent() {
const [count, setCount] = useState(0);
// 1. 객체 참조 유지
const heavyData = useMemo(() => ({ role: 'admin' }), []);
// 2. 함수 참조 유지
const handleClick = useCallback(() => {
console.log('클릭됨');
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>증가</button>
{/* memo 처리된 자식에게 주소값이 변하지 않는 props를 전달 */}
<OptimizedChild data={heavyData} onClick={handleClick} />
</div>
);
}
④ children props (컴포넌트 합성) 활용하기
상태를 가지고 있는 컴포넌트가 다른 무거운 컴포넌트를 감싸는 구조라면, children으로 넘겨 컴포넌트 구조상 리렌더링을 피할 수 있습니다.
// 컴포넌트가 리렌더링되어도 children으로 들어온 컴포넌트는 영향을 받지 않음
export default function WrapperComponent({ children }) {
const [theme, setTheme] = useState("light");
return (
<div className={theme}>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>테마 변경</button>
{children} {/* 부모 상태가 바뀌어도 감싸진 컴포넌트는 리렌더링 안 됨 */}
</div>
);
}
Next.js 환경에서의 추가 팁
Next.js 환경에서는 구조적으로 불필요한 리렌더링을 원천 차단할 수 있는 강력한 무기가 있습니다.
-
Server Component(서버 컴포넌트) 적극 활용: Next.js App Router에서 기본 컴포넌트는 서버 컴포넌트입니다. 서버 컴포넌트는 서버에서 한 번만 렌더링되어 HTML과 데이터 스냅샷으로 브라우저에 전달되므로, 클라이언트 측 리렌더링 개념 자체가 없습니다. 데이터 페칭이나 무거운 로직은 서버 컴포넌트에서 처리하세요.
-
"use client"의 경계 최소화: 상태 변경이나 이벤트 핸들러가 필요한 부분만 정확히"use client"파일로 떼어내어 클라이언트 컴포넌트로 만드세요. 인터랙션이 없는 정적 데이터 영역까지 클라이언트 컴포넌트에 포함시키면 성능 저하의 원인이 됩니다.