안녕하세요!
이번 시간에는 React 렌더링 최적화 방법에 대해 공유하고자 합니다.
먼저 React 리랜더링 되는 이유는 다음과 같습니다.
- 컴포넌트의 state가 변경되었을 때
- 컴포넌트가 상속받은 props가 변경되었을 때
- 부모 컴포넌트가 리렌더링이 된 경우 자식 컴포넌트는 모두 리렌더링
이 과정에서 불필요한 렌더링이 발생되게 됩니다.
만약 비싼 계산 로직이 있는 자식 컴포넌트가 100개가 있다면, 각 자식 컴포넌트가 리렌더링될 때마다 계산이 반복되므로 성능에 큰 부담을 주게 됩니다. 특히, 부모 컴포넌트의 상태나 props 변경으로 인해 자식 컴포넌트가 모두 리렌더링될 경우, 전체 애플리케이션의 성능이 현저히 저하될 수 있습니다.
코드로 살펴봅시다.
import React, { useState, useMemo, useCallback } from 'react';
import Child from './Child';
import Child2 from './Child2';
import Child3 from './Child3';
function App() {
const [parentAge, setParentAge] = useState(0);
const [childAge, setChildAge] = useState(0);
const childName = {
lastName: '홍',
firstName: '범선',
}
const tellMe = () => {
console.log('길동아 사랑해 💕')
};
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
const incrementChildAge = () => {
setChildAge(childAge + 1);
}
console.log("👨👩👧👦 부모 컴포넌트가 렌더링 되었어요");
return (
<div style={{ border: '2px solid navy', padding: '10px' }}>
<h1>👨👩👧👦 부모</h1>
<p>age: {parentAge}살</p>
<button onClick={incrementParentAge}>부모 나이 증가</button>
<button onClick={incrementChildAge}>자녀 나이 증가</button>
<Child name={"홍길동"} age={childAge}/>
<Child2 name={childName} />
<Child3 name={"홍길동"} tellMe={tellMe}/>
</div>
);
}
export default App;
부모 컴포넌트입니다.
부모 컴포넌트의 state 변수를 props로 전달하고 있습니다.
// 아이1
const Child = ({name, age}) => {
console.log("👼 자녀 컴포넌트가 렌더링 되었어요");
return (
<div style={{ border: '2px solid powderblue', padding:'10px' }}>
<h3>👼 자녀</h3>
<p> name : {name}</p>
<p>age: {age}살</p>
</div>
)
}
export default Child;
// 아이2
const Child2 = ({name}) => {
console.log("👩👦 자녀2 컴포넌트가 렌더링 되었어요");
return (
<div style={{ border: '2px solid powderblue', padding:'10px' }}>
<h3>👩👦 자녀</h3>
<p> 성 : {name.lastName}</p>
<p>이름: {name.firstName}</p>
</div>
)
}
export default Child2;
// 아이3
const Child3 = ({name, tellMe}) => {
console.log("👨👦 자녀3 컴포넌트가 렌더링 되었어요");
return (
<div style={{ border: '2px solid powderblue', padding:'10px' }}>
<h3>👨👦 자녀</h3>
<p> 이름 : {name}</p>
<button onClick={tellMe}>엄마 나 사랑해?</button>
</div>
)
}
export default Child3;
자식 컴포넌트들입니다.
부모 컴포넌트에서 전달받은 props를 화면에 렌더링 하는 컴포넌트들입니다.
부모 컴포넌트의 나이를 변경시킬 '부모 나이 증가' 버튼을 클릭하겠습니다.
그러면 incrementParentAge 함수가 실행될 것입니다.
다음은 실행 후 렌더링 결과입니다.
부모의 나이 증가 버튼을 클릭했음에도 불구하고
자식 컴포넌트의 나이는 변경되지 않았는데도 렌더링이 발생하고 있습니다.
즉, 불필요한 렌더링이 이루어지고 있는 상황입니다. 이를 최적화해보겠습니다.
최적화는 React에서 제공하는 React.memo를 사용하여 불필요한 렌더링을 방지할 수 있습니다.
export default Child; // before
export default React.memo(Child); // after
export default Child2; //before
export default React.memo(Child2); // after
export default Child3; //before
export default React.memo(Child3); // after
다음과 같이 React.memo를 사용하여 컴포넌트를 메모이제이션할 수 있습니다.
React.memo는 props로 전달된 값을 비교하는데, 기존값과 같다면 리렌더링은 건너뛰고 이전 렌더링 결과값을 사용합니다.
반면 기존값과 다르다면 이때 리렌더링이 발생합니다.
바뀐 후 결과를 보겠습니다.
Child 컴포넌트는 메모리제이션이 성공적으로 작동했지만
Child2, Child3는 재랜더링이 발생합니다.
분명 props가 바뀐 것이 없는데도 재랜더링이 발생합니다.
그 이유는 App 컴포넌트가 리렌더링될 때, 함수나 객체가 새로운 참조(Reference)를 생성하기 때문입니다.
그렇기 때문에, React.memo에서 기존값과 비교할 때 변경된 것으로 간주하고 리렌더링을 트리거 합니다.
이 문제를 해결하기 위해서,
새로운 참조가 생기지 않도록 React에서는 memo, useCallback을 제공합니다.
// const childName = {
// lastName: '홍',
// firstName: '범선',
// }
// const tellMe = () => {
// console.log('길동아 사랑해 💕')
// };
const tellMe = useCallback(() => {
console.log('길동아 사랑해 💕')
}, [])
const childName = useMemo(() => {
return {
lastName: '홍',
firstName: '범선',
}
},[])
함수는 useCallback을 사용하여 새로 생성되는 것을 방지하고
객체는 useMemo를 사용하여 새로 생성되는 것을 방지합니다.
다시 실행해보겠습니다.
Child, Child2, Child3 컴포넌트 전부 메모리제이션이 된 것을 확인할 수 있습니다.
정리하자면 다음과 같습니다.
- 컴포넌트를 메모리제이션 하기 위해선 React.Memo를 사용한다.
- React.memo는 props가 변경되지 않으면 이전 값을 재사용하여 불필요한 리렌더링을 방지한다.
- 컴포넌트가 리렌더링될 때, 객체는 새로운 참조값이 생성된다.
- 함수는 useCallback을 사용하여 불필요한 재생성을 방지한다
- 객체는 useMemo를 사용하여 새로운 참조값이 생성되지 않도록 최적화한다.
이렇게 하여 리액트에서 렌더링 최적화 방법에 대해 알아보았습니다.
감사합니다.
'스터디 발표자료' 카테고리의 다른 글
코딩 테스트 서버 만들기 (0) | 2025.02.10 |
---|---|
OSI 7계층 Wireshark로 분석하기 (0) | 2025.02.10 |