"부모 컴포넌트가 다시 렌더링되더라도, 자식 컴포넌트가 불필요하게 리렌더링되지 않도록" 하고, "Context, 리스트, 무거운 컴포넌트 등에서 메모화와 적절한 분리를 활용"하는 것이 리액트 성능 최적화의 핵심이다.
주로 불필요한 컴포넌트 리렌더링을 방지하고, 꼭 필요한 지점에서만 최적화를 적용하도록 유의하면서 코드 구조를 잘 잡아나가면 된다.
리액트가 화면을 업데이트하는 과정은 크게 3단계로 나뉜다.
1. 트리거
2. 렌더 단계
3. 커밋 단계
이 후에 브라우저가 도큐먼트 객체 모델(DOM) 변경 사항을 실제 화면에 그리는 ‘브라우저 렌더링(Browser Rendering)’이 일어난다.
렌더링, 리렌더링
- 렌더링(Rendering)은 컴포넌트 함수가 실행되어 Virtual DOM을 생성하는 과정
- 리렌더링(Re-rendering)은 컴포넌트가 다시 실행되는 과정으로, state, props, context 등이 변경될 때 발생
1. 상태(state)가 변경되는 경우
setState
등을 통해 상태가 변경되면, 해당 컴포넌트가 다시 렌더링된다.2. 부모 컴포넌트가 다시 렌더링되는 경우
React.memo
를 사용하면 props가 변경되지 않은 경우 자식이 다시 렌더링되지 않는다.3. 컨텍스트(context)가 업데이트된 경우
useContext
훅으로 컨텍스트를 사용하는 컴포넌트는 컨텍스트 값이 변경될 때 다시 렌더링된다.4. 커스텀 훅 내부에서 상태가 업데이트된 경우
이제 실제로 리렌더링을 방지하는 방법을 살펴보자.
// 큰 컴포넌트를 분리
function EventsTable() {
return (
<>
<TableHeader />
<TableRows />
</>
);
}
export const MemoizedRows = React.memo(TableRows);
function TableRows({ rows, prepareRow }) {
return rows.map(row => (
<TableRow row={prepareRow(row)} key={row.id} />
));
}
const renderSomething = () => { ... }
처럼 함수를 선언하면, 컴포넌트가 리렌더링될 때마다 새로운 함수가 생성되므로, 불필요한 연산이 발생할 수 있다.// BEFORE: JSX 내부에서 함수 선언 (렌더링될 때마다 새 함수가 생성됨)
const renderTableTitle = (title, totalRows) => (
<Flex>
<Heading>{title}</Heading>
{totalRows}
</Flex>
);
// AFTER: 별도 컴포넌트로 분리 (매 렌더링 시 새 함수가 생성되는 문제 방지)
function TableTitle({ title, totalRows }) {
return (
<Flex>
<Heading>{title}</Heading>
{totalRows}
</Flex>
);
}
// 안티패턴
const Parent = () => {
const ChildInside = () => <div>Something</div>; // 렌더될 때마다 새로 정의됨
return <ChildInside />;
};
Parent
가 렌더링될 때마다 ChildInside
가 새롭게 정의된다.리액트는 state가 변경되거나 부모 컴포넌트가 리렌더링될 때, 해당 컴포넌트를 다시 렌더링한다.
단, 부모의 리렌더링이 항상 자식의 리렌더링을 의미하는 것은 아니며, React.memo 등을 활용하면 불필요한 리렌더링을 방지할 수 있다.
무거운 부모 컴포넌트에서 불필요한 상태를 관리하지 않도록 한다.
모달 다이얼로그를 여닫는 로직
을 부모에서 관리하면 부모가 렌더링될 때마다 불필요하게 모달 상태도 영향을 받을 수 있다.// 상태를 자식 컴포넌트에서 관리하도록 변경
function ButtonWithDialog() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
{open && <Dialog />}
</>
);
}
// 부모
function Parent() {
return (
<>
<ButtonWithDialog />
<VerySlowComponent /> {/* 영향을 받지 않음 */}
</>
);
}
const SlowComponent = () => {
console.log("SlowComponent 렌더링!");
return <div>나는 느린 컴포넌트야</div>;
};
const Component = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
return (
<div onMouseMove={handleMouseMove}>
<p>마우스 위치: {position.x}, {position.y}</p>
<SlowComponent /> {/* ❗ 불필요한 리렌더링 발생 */}
</div>
);
};
const SlowComponent = () => {
console.log("SlowComponent 렌더링!");
return <div>나는 느린 컴포넌트야</div>;
};
const MouseTracker = ({ children }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
return (
<div onMouseMove={handleMouseMove}>
<p>마우스 위치: {position.x}, {position.y}</p>
{children} {/* children은 props이므로 상태 변경 시에도 리렌더링되지 않음 */}
</div>
);
};
const Component = () => {
return (
<MouseTracker>
<SlowComponent /> {/* 리렌더링되지 않음! */}
</MouseTracker>
);
};
children
으로 전달되므로 불필요한 리렌더링이 발생하지 않음React.memo
는 기본적으로 props의 얕은 비교를 수행한다.useMemo
또는 useCallback
을 사용하여 참조값을 고정하면, props가 변경되지 않는 한 불필요한 리렌더링을 방지할 수 있다.const MemoizedChild = React.memo(Child);
function Parent() {
//useMemo를 사용하여 객체 참조값 유지
const obj = useMemo(() => ({ someKey: 'someValue' }), []);
//useMemo 없이 props를 넘길 경우 (문제 발생 가능)
//새로운 객체가 매번 생성됨
//const obj = { someKey: 'someValue' };
return <MemoizedChild value={obj} />;
}
{items.map((item) => (
<Child key={item.id} item={item} />
))}
리스트 항목이 변경되지 않을 경우, 렌더링을 방지할 수 있다.
const MemoizedChild = React.memo(Child);
{items.map((item) => (
<MemoizedChild key={item.id} item={item} />
))}
Context Provider
의 value
가 변경될 때, 이를 구독하는 모든 컴포넌트가 리렌더링됨.useMemo
를 사용하여 value
객체의 참조값을 고정function SomeProvider({ children }) {
const [state, setState] = useState({});
const memoValue = useMemo(() => ({ state, setState }), [state]);
return (
<SomeContext.Provider value={memoValue}>
{children}
</SomeContext.Provider>
);
}
function AppProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserDataContext.Provider value={user}>
<UserApiContext.Provider value={setUser}>
{children}
</UserApiContext.Provider>
</UserDataContext.Provider>
);
}
useContext
를 쓰는 모든 컴포넌트가 리렌더링될 수 있다.React.memo
를 조합해 특정 값만 바뀔 때만 렌더링하도록 만들 수도 있다.use-context-selector
같은 라이브러리를 사용할 수도 있다.React.memo
로 감싸서 props 변동이 없으면 건너뛰도록 한다.useMemo
나 useCallback
을 사용해 참조값을 고정한다.children
혹은 별도 props로 무거운 컴포넌트를 받아두면, 상태 변경 시에도 무거운 컴포넌트가 호출되지 않는다.