메모이제이션 및 리렌더링 최적화
React 애플리케이션, 특히 Next.js를 사용한 대규모 프로젝트에서 성능 최적화는 중요한 과제입니다.
이 절에서는 React의 메모이제이션 기법과 리렌더링 최적화 전략에 대해 알아보고, 이들이 애플리케이션 성능에 미치는 영향을 살펴보겠습니다.
useMemo 훅
useMemo
는 계산 비용이 높은 값을 메모이제이션하는 데 사용됩니다.
의존성 배열의 값이 변경될 때만 재계산을 수행합니다.
import { useMemo } from 'react'
function ExpensiveComponent({ data }) {
const expensiveResult = useMemo(() => {
return data.map(item => /* 복잡한 계산 */)
}, [data])
return <div>{expensiveResult}</div>
}
useCallback 훅
useCallback
은 함수를 메모이제이션하여 불필요한 리렌더링을 방지합니다.
주로 자식 컴포넌트에 전달되는 콜백 함수를 최적화할 때 사용됩니다.
import { useCallback } from 'react'
function ParentComponent({ data }) {
const handleClick = useCallback(() => {
console.log(data)
}, [data])
return <ChildComponent onClick={handleClick} />
}
React.memo를 사용한 컴포넌트 최적화
React.memo
는 컴포넌트를 메모이제이션하여 props가 변경되지 않았을 때 리렌더링을 방지합니다.
import React from 'react'
const MemoizedComponent = React.memo(function MyComponent(props) {
// 컴포넌트 로직
})
export default MemoizedComponent
성능 프로파일링 도구
Next.js에서는 React DevTools와 Chrome DevTools의 Performance 탭을 사용하여 성능을 분석할 수 있습니다.
또한, Next.js의 내장 성능 분석 도구를 활용할 수 있습니다.
import { useEffect } from 'react';
import { useReportWebVitals } from 'next/web-vitals';
export default function RootLayout({ children }) {
useEffect(() => {
useReportWebVitals(metric => {
console.log(metric);
});
}, []);
return (
<html lang="en">
<head />
<body>{children}</body>
</html>
);
}
8장 상태 관리 및 폼 처리와의 연관성
메모이제이션과 리렌더링 최적화는 8장에서 다룬 상태 관리 및 폼 처리와 밀접하게 연관됩니다.
복잡한 상태 로직이나 대규모 폼에서 useMemo
와 useCallback
을 적절히 사용하면 불필요한 계산과 리렌더링을 방지할 수 있습니다.
예를 들어, 폼 제출 핸들러를 useCallback
으로 감싸거나, 복잡한 유효성 검사 로직을 useMemo
로 최적화할 수 있습니다.
import { useMemo, useCallback } from 'react'
function ComplexForm({ initialData }) {
const [formData, setFormData] = useState(initialData)
const validateForm = useMemo(() => {
// 복잡한 유효성 검사 로직
}, [formData])
const handleSubmit = useCallback((event) => {
event.preventDefault()
// 폼 제출 로직
}, [formData])
return (
<form onSubmit={handleSubmit}>
{/* 폼 필드들 */}
</form>
)
}
실습 : 대량 데이터 리스트 컴포넌트 최적화
대량의 데이터를 렌더링하는 리스트 컴포넌트를 최적화하는 실습을 해보겠습니다.
- 먼저, 대량의 데이터를 생성하는 함수를 만듭니다.
function generateData(count) {
return Array.from({ length: count }, (_, index) => ({
id: index,
name: `Item ${index}`,
value: Math.floor(Math.random() * 100)
}))
}
- 최적화된 리스트 아이템 컴포넌트를 만듭니다.
const ListItem = React.memo(function ListItem({ item, onItemClick }) {
console.log(`Rendering item ${item.id}`)
return (
<li onClick={() => onItemClick(item.id)}>
{item.name}: {item.value}
</li>
)
})
- 최적화된 리스트 컴포넌트를 만듭니다.
'use client'
import React, { useMemo, useCallback, useState } from 'react'
function OptimizedList({ count }) {
const [selectedId, setSelectedId] = useState(null)
const data = useMemo(() => generateData(count), [count])
const handleItemClick = useCallback((id) => {
setSelectedId(id)
}, [])
return (
<div>
<h2>Optimized List (Item count: {count})</h2>
<ul>
{data.map(item => (
<ListItem
key={item.id}
item={item}
onItemClick={handleItemClick}
/>
))}
</ul>
{selectedId !== null && <p>Selected Item ID: {selectedId}</p>}
</div>
)
}
export default OptimizedList
- 페이지에서 최적화된 리스트 컴포넌트를 사용합니다.
'use client'
import { useState } from 'react'
import OptimizedList from '../components/OptimizedList'
export default function Home() {
const [count, setCount] = useState(1000)
return (
<div>
<h1>Large List Optimization Demo</h1>
<input
type="number"
value={count}
onChange={(e) => setCount(Number(e.target.value))}
min="1"
/>
<OptimizedList count={count} />
</div>
)
}
이 실습에서는 다음과 같은 최적화 기법을 사용했습니다.
React.memo
를 사용하여ListItem
컴포넌트를 메모이제이션합니다.useMemo
를 사용하여 데이터 생성을 최적화합니다.useCallback
을 사용하여handleItemClick
함수를 메모이제이션합니다.
이러한 최적화를 통해 대량의 데이터를 렌더링할 때 성능을 크게 향상시킬 수 있습니다.
특히 리스트의 개별 항목이 변경되지 않았을 때 불필요한 리렌더링을 방지합니다.
메모이제이션과 리렌더링 최적화는 React와 Next.js 애플리케이션의 성능을 크게 향상시킬 수 있는 강력한 도구입니다.
하지만 모든 상황에서 이러한 최적화가 필요한 것은 아니며, 때로는 오히려 성능을 저하시킬 수 있습니다.
따라서 성능 프로파일링 도구를 사용하여 실제로 최적화가 필요한 부분을 식별하고, 적절하게 적용하는 것이 중요합니다.
또한, 코드의 가독성과 유지보수성을 해치지 않는 선에서 최적화를 수행해야 합니다.