useMemo, useCallback, React.memo를 사용하여 React 애플리케이션의 성능을 최적화하는 포괄적인 가이드입니다. 불필요한 재렌더링을 방지하고 사용자 경험을 개선하는 방법을 알아보세요.
React 성능 최적화: useMemo, useCallback, React.memo 마스터하기
사용자 인터페이스를 구축하기 위한 인기 있는 JavaScript 라이브러리인 React는 컴포넌트 기반 아키텍처와 선언적 스타일로 유명합니다. 그러나 애플리케이션의 복잡성이 증가함에 따라 성능이 문제가 될 수 있습니다. 컴포넌트의 불필요한 재렌더링은 성능 저하와 사용자 경험 저하로 이어질 수 있습니다. 다행히 React는 useMemo
, useCallback
, React.memo
를 포함하여 성능을 최적화하는 여러 도구를 제공합니다. 이 가이드에서는 이러한 기술을 자세히 살펴보고, 고성능 React 애플리케이션을 구축하는 데 도움이 되는 실용적인 예제와 실행 가능한 통찰력을 제공합니다.
React 재렌더링 이해
최적화 기술을 자세히 살펴보기 전에 React에서 재렌더링이 발생하는 이유를 이해하는 것이 중요합니다. 컴포넌트의 상태 또는 props가 변경되면 React는 해당 컴포넌트와 잠재적으로 하위 컴포넌트의 재렌더링을 트리거합니다. React는 가상 DOM을 사용하여 실제 DOM을 효율적으로 업데이트하지만 과도한 재렌더링은 특히 복잡한 애플리케이션에서 여전히 성능에 영향을 미칠 수 있습니다. 제품 가격이 자주 업데이트되는 글로벌 전자 상거래 플랫폼을 상상해 보세요. 최적화가 없으면 작은 가격 변경조차도 전체 제품 목록에서 재렌더링을 트리거하여 사용자 검색에 영향을 줄 수 있습니다.
컴포넌트가 재렌더링되는 이유
- 상태 변경:
useState
또는useReducer
를 사용하여 컴포넌트의 상태가 업데이트되면 React는 컴포넌트를 재렌더링합니다. - Prop 변경: 컴포넌트가 상위 컴포넌트에서 새 props를 받으면 재렌더링됩니다.
- 상위 재렌더링: 상위 컴포넌트가 재렌더링되면 해당 자식 컴포넌트도 props가 변경되었는지 여부에 관계없이 기본적으로 재렌더링됩니다.
- 컨텍스트 변경: React 컨텍스트를 사용하는 컴포넌트는 컨텍스트 값이 변경될 때 재렌더링됩니다.
성능 최적화의 목표는 불필요한 재렌더링을 방지하여 컴포넌트가 실제로 데이터가 변경된 경우에만 업데이트되도록 하는 것입니다. 주식 시장 분석을 위한 실시간 데이터 시각화를 포함하는 시나리오를 생각해 보세요. 차트 컴포넌트가 사소한 데이터 업데이트마다 불필요하게 재렌더링되면 애플리케이션이 응답하지 않게 됩니다. 재렌더링을 최적화하면 원활하고 반응성이 뛰어난 사용자 경험을 보장할 수 있습니다.
useMemo 소개: 비용이 많이 드는 계산 메모이제이션
useMemo
는 계산 결과를 메모이제이션하는 React 훅입니다. 메모이제이션은 비용이 많이 드는 함수 호출 결과를 저장하고 동일한 입력이 다시 발생할 때 해당 결과를 재사용하는 최적화 기술입니다. 이렇게 하면 함수를 불필요하게 다시 실행할 필요가 없습니다.
useMemo 사용 시기
- 비용이 많이 드는 계산: 컴포넌트가 props 또는 상태를 기반으로 계산 집약적인 계산을 수행해야 할 때.
- 참조 동등성: 재렌더링할지 여부를 결정하기 위해 참조 동등성에 의존하는 자식 컴포넌트에 값을 prop으로 전달할 때.
useMemo 작동 방식
useMemo
는 두 개의 인수를 사용합니다.
- 계산을 수행하는 함수.
- 종속성 배열.
함수는 배열의 종속성 중 하나가 변경될 때만 실행됩니다. 그렇지 않으면 useMemo
는 이전에 메모이제이션된 값을 반환합니다.
예: 피보나치 수열 계산
피보나치 수열은 계산 집약적인 계산의 고전적인 예입니다. useMemo
를 사용하여 n번째 피보나치 수를 계산하는 컴포넌트를 만들어 보겠습니다.
import React, { useState, useMemo } from 'react';
function Fibonacci({ n }) {
const fibonacciNumber = useMemo(() => {
console.log('Calculating Fibonacci...'); // 계산이 실행될 때를 보여줍니다.
function calculateFibonacci(num) {
if (num <= 1) {
return num;
}
return calculateFibonacci(num - 1) + calculateFibonacci(num - 2);
}
return calculateFibonacci(n);
}, [n]);
return Fibonacci({n}) = {fibonacciNumber}
;
}
function App() {
const [number, setNumber] = useState(5);
return (
setNumber(parseInt(e.target.value))
/>
);
}
export default App;
이 예제에서 calculateFibonacci
함수는 n
prop이 변경될 때만 실행됩니다. useMemo
가 없으면 Fibonacci
컴포넌트가 재렌더링될 때마다 함수가 실행되어 n
이 동일하게 유지되더라도 실행됩니다. 이 계산이 글로벌 금융 대시보드에서 발생한다고 상상해 보세요. 시장의 모든 틱이 전체 재계산을 유발하여 상당한 지연이 발생합니다. useMemo
는 이를 방지합니다.
useCallback 소개: 함수 메모이제이션
useCallback
은 함수를 메모이제이션하는 또 다른 React 훅입니다. 매번 새로운 함수 인스턴스가 생성되는 것을 방지하므로 콜백을 자식 컴포넌트에 props로 전달할 때 특히 유용합니다.
useCallback 사용 시기
- 콜백을 Prop으로 전달:
React.memo
또는shouldComponentUpdate
를 사용하여 재렌더링을 최적화하는 자식 컴포넌트에 함수를 prop으로 전달할 때. - 이벤트 핸들러: 자식 컴포넌트의 불필요한 재렌더링을 방지하기 위해 컴포넌트 내에서 이벤트 핸들러 함수를 정의할 때.
useCallback 작동 방식
useCallback
은 두 개의 인수를 사용합니다.
- 메모이제이션할 함수.
- 종속성 배열.
함수는 배열의 종속성 중 하나가 변경될 때만 다시 생성됩니다. 그렇지 않으면 useCallback
은 동일한 함수 인스턴스를 반환합니다.
예: 버튼 클릭 처리
콜백 함수를 트리거하는 버튼이 있는 컴포넌트를 만들어 보겠습니다. useCallback
을 사용하여 콜백 함수를 메모이제이션합니다.
import React, { useState, useCallback } from 'react';
function Button({ onClick, children }) {
console.log('Button re-rendered'); // 버튼이 재렌더링될 때를 보여줍니다.
return ;
}
const MemoizedButton = React.memo(Button);
function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Button clicked');
setCount((prevCount) => prevCount + 1);
}, []); // 빈 종속성 배열은 함수가 한 번만 생성됨을 의미합니다.
return (
Count: {count}
Increment
);
}
export default App;
이 예제에서 handleClick
함수는 종속성 배열이 비어 있으므로 한 번만 생성됩니다. count
상태 변경으로 인해 App
컴포넌트가 재렌더링될 때 handleClick
함수는 동일하게 유지됩니다. React.memo
로 래핑된 MemoizedButton
컴포넌트는 props가 변경된 경우에만 재렌더링됩니다. onClick
prop (handleClick
)이 동일하게 유지되므로 Button
컴포넌트는 불필요하게 재렌더링되지 않습니다. 대화형 지도 애플리케이션을 상상해 보세요. 사용자가 상호 작용할 때마다 수십 개의 버튼 컴포넌트가 영향을 받을 수 있습니다. useCallback
이 없으면 이러한 버튼이 불필요하게 재렌더링되어 지연된 경험을 생성합니다. useCallback
을 사용하면 더 부드러운 상호 작용이 보장됩니다.
React.memo 소개: 컴포넌트 메모이제이션
React.memo
는 함수형 컴포넌트를 메모이제이션하는 고차 컴포넌트(HOC)입니다. props가 변경되지 않은 경우 컴포넌트가 재렌더링되는 것을 방지합니다. 이는 클래스 컴포넌트의 PureComponent
와 유사합니다.
React.memo 사용 시기
- 순수 컴포넌트: 컴포넌트의 출력이 props에만 의존하고 자체 상태가 없는 경우.
- 비용이 많이 드는 렌더링: 컴포넌트의 렌더링 프로세스가 계산 집약적인 경우.
- 빈번한 재렌더링: 컴포넌트의 props가 변경되지 않았는데도 컴포넌트가 자주 재렌더링되는 경우.
React.memo 작동 방식
React.memo
는 함수형 컴포넌트를 래핑하고 이전 및 다음 props를 얕게 비교합니다. props가 동일하면 컴포넌트는 재렌더링되지 않습니다.
예: 사용자 프로필 표시
사용자 프로필을 표시하는 컴포넌트를 만들어 보겠습니다. 사용자의 데이터가 변경되지 않은 경우 불필요한 재렌더링을 방지하기 위해 React.memo
를 사용합니다.
import React from 'react';
function UserProfile({ user }) {
console.log('UserProfile re-rendered'); // 컴포넌트가 재렌더링될 때를 보여줍니다.
return (
Name: {user.name}
Email: {user.email}
);
}
const MemoizedUserProfile = React.memo(UserProfile, (prevProps, nextProps) => {
// 사용자 지정 비교 함수 (선택 사항)
return prevProps.user.id === nextProps.user.id; // 사용자 ID가 변경된 경우에만 재렌더링
});
function App() {
const [user, setUser] = React.useState({
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
});
const updateUser = () => {
setUser({ ...user, name: 'Jane Doe' }); // 이름 변경
};
return (
);
}
export default App;
이 예제에서 MemoizedUserProfile
컴포넌트는 user.id
prop이 변경된 경우에만 재렌더링됩니다. user
객체의 다른 속성(예: 이름 또는 이메일)이 변경되더라도 ID가 다르면 컴포넌트가 재렌더링되지 않습니다. `React.memo` 내의 이 사용자 지정 비교 함수를 사용하면 컴포넌트가 재렌더링되는 시기를 세밀하게 제어할 수 있습니다. 소셜 미디어 플랫폼에서 지속적으로 업데이트되는 사용자 프로필을 생각해 보세요. `React.memo`가 없으면 사용자의 상태 또는 프로필 사진을 변경하면 핵심 사용자 세부 정보가 동일하게 유지되더라도 프로필 컴포넌트가 완전히 재렌더링됩니다. `React.memo`를 사용하면 대상 업데이트가 가능하고 성능이 크게 향상됩니다.
useMemo, useCallback 및 React.memo 결합
이 세 가지 기술은 함께 사용할 때 가장 효과적입니다. useMemo
는 비용이 많이 드는 계산을 메모이제이션하고, useCallback
은 함수를 메모이제이션하며, React.memo
는 컴포넌트를 메모이제이션합니다. 이러한 기술을 결합하면 React 애플리케이션에서 불필요한 재렌더링 횟수를 크게 줄일 수 있습니다.
예: 복잡한 컴포넌트
이러한 기술을 결합하는 방법을 보여주는 더 복잡한 컴포넌트를 만들어 보겠습니다.
import React, { useState, useCallback, useMemo } from 'react';
function ListItem({ item, onUpdate, onDelete }) {
console.log(`ListItem ${item.id} re-rendered`); // 컴포넌트가 재렌더링될 때를 보여줍니다.
return (
{item.text}
);
}
const MemoizedListItem = React.memo(ListItem);
function List({ items, onUpdate, onDelete }) {
console.log('List re-rendered'); // 컴포넌트가 재렌더링될 때를 보여줍니다.
return (
{items.map((item) => (
))}
);
}
const MemoizedList = React.memo(List);
function App() {
const [items, setItems] = useState([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
]);
const handleUpdate = useCallback((id) => {
setItems((prevItems) =>
prevItems.map((item) =>
item.id === id ? { ...item, text: `Updated ${item.text}` } : item
)
);
}, []);
const handleDelete = useCallback((id) => {
setItems((prevItems) => prevItems.filter((item) => item.id !== id));
}, []);
const memoizedItems = useMemo(() => items, [items]);
return (
);
}
export default App;
이 예제에서:
useCallback
은handleUpdate
및handleDelete
함수를 메모이제이션하여 매번 재현성되는 것을 방지하는 데 사용됩니다.useMemo
는items
배열을 메모이제이션하여 배열 참조가 변경되지 않은 경우List
컴포넌트가 재렌더링되는 것을 방지하는 데 사용됩니다.React.memo
는ListItem
및List
컴포넌트를 메모이제이션하여 props가 변경되지 않은 경우 재렌더링되는 것을 방지하는 데 사용됩니다.
이러한 기술의 조합은 컴포넌트가 필요한 경우에만 재렌더링되도록 하여 상당한 성능 향상을 가져옵니다. 대규모 프로젝트 관리 도구에서 작업 목록이 지속적으로 업데이트, 삭제 및 재정렬되는 것을 상상해 보세요. 이러한 최적화가 없으면 작업 목록에 대한 작은 변경 사항이라도 재렌더링의 연쇄 반응을 트리거하여 애플리케이션을 느리고 응답하지 않게 만듭니다. useMemo
, useCallback
및 React.memo
를 전략적으로 사용하면 복잡한 데이터와 빈번한 업데이트에서도 애플리케이션의 성능을 유지할 수 있습니다.
추가 최적화 기술
useMemo
, useCallback
및 React.memo
는 강력한 도구이지만 React 성능을 최적화하는 유일한 옵션은 아닙니다. 고려해야 할 몇 가지 추가 기술은 다음과 같습니다.
- 코드 분할: 애플리케이션을 필요에 따라 로드할 수 있는 더 작은 청크로 분할합니다. 이렇게 하면 초기 로드 시간이 줄어들고 전반적인 성능이 향상됩니다.
- 지연 로딩: 컴포넌트와 리소스를 필요한 경우에만 로드합니다. 이는 이미지 및 기타 대용량 자산에 특히 유용할 수 있습니다.
- 가상화: 큰 목록 또는 테이블의 보이는 부분만 렌더링합니다. 이는 대용량 데이터 세트를 처리할 때 성능을 크게 향상시킬 수 있습니다.
react-window
및react-virtualized
와 같은 라이브러리가 이를 지원할 수 있습니다. - 디바운싱 및 스로틀링: 함수가 실행되는 속도를 제한합니다. 이는 스크롤 및 크기 조정과 같은 이벤트를 처리하는 데 유용할 수 있습니다.
- 불변성: 실수로 인한 변형을 방지하고 변경 감지를 단순화하기 위해 불변 데이터 구조를 사용합니다.
최적화를 위한 글로벌 고려 사항
글로벌 잠재 고객을 위해 React 애플리케이션을 최적화할 때는 네트워크 대기 시간, 장치 기능 및 지역화와 같은 요소를 고려하는 것이 중요합니다. 몇 가지 팁은 다음과 같습니다.
- CDN(Content Delivery Networks): CDN을 사용하여 사용자에게 더 가까운 위치에서 정적 자산을 제공합니다. 이렇게 하면 네트워크 대기 시간이 줄어들고 로드 시간이 개선됩니다.
- 이미지 최적화: 서로 다른 화면 크기 및 해상도에 맞게 이미지를 최적화합니다. 파일 크기를 줄이려면 압축 기술을 사용하세요.
- 지역화: 각 사용자에게 필요한 언어 리소스만 로드합니다. 이렇게 하면 초기 로드 시간이 줄어들고 사용자 경험이 개선됩니다.
- 적응형 로딩: 사용자의 네트워크 연결 및 장치 기능을 감지하고 그에 따라 애플리케이션의 동작을 조정합니다. 예를 들어 네트워크 연결이 느리거나 구형 장치를 사용하는 사용자의 경우 애니메이션을 비활성화하거나 이미지 품질을 낮출 수 있습니다.
결론
원활하고 반응성이 뛰어난 사용자 경험을 제공하려면 React 애플리케이션 성능을 최적화하는 것이 중요합니다. useMemo
, useCallback
및 React.memo
와 같은 기술을 마스터하고 글로벌 최적화 전략을 고려하여 다양한 사용자 기반의 요구 사항을 충족하도록 확장할 수 있는 고성능 React 애플리케이션을 구축할 수 있습니다. 성능 병목 현상을 식별하고 이러한 최적화 기술을 전략적으로 적용하려면 애플리케이션을 프로파일링하는 것을 잊지 마세요. 너무 일찍 최적화하지 말고 가장 큰 영향을 줄 수 있는 영역에 집중하세요.
이 가이드는 React 성능 최적화를 이해하고 구현하기 위한 견고한 기반을 제공합니다. React 애플리케이션을 계속 개발할 때 성능을 우선시하고 사용자 경험을 개선할 수 있는 새로운 방법을 지속적으로 찾아보세요.