react dnd를 사용하여 드래그 앤 드랍을 구현하던 중, 함수가 매번 재생성되어 불필요한 재랜더링을 발생시키는 코드를 만들게 되었다.
다음 코드는 드래그 앤 드롭 기능을 가진 아이템들의 컨테이너를 구현한 코드다.
'use client'
import React, { useState } from 'react'
import update from 'immutability-helper'
import CardContainer from '@/components/CardContainer'
import ErrorList from '@/components/ErrorList'
import ErrorSummaryCard from './ErrorSummaryCard'
export default function DashboardContainer() {
// useState
const [items, setItems] = useState([
{
id: 'item-1',
title: 'Error Overview',
component: <ErrorList />,
},
{ id: 'item-2', title: 'Monthly Uptime' },
{ id: 'item-3', title: 'Error Summary', component: <ErrorSummaryCard /> },
{ id: 'item-4', title: 'Error Overview' },
{ id: 'item-5', title: 'Monthly Uptime' },
{ id: 'item-6', title: 'Error Summary' },
])
// functions
const moveItem = (dragIndex, hoverIndex) => {
// Swap the index of the dragged item with the dropped item
setItems(prevItems =>
update(prevItems, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, prevItems[dragIndex]],
],
}),
)
}
const renderCardItem = (id, title, component, index) => (
// Render CardItem
<CardContainer
key={id}
id={id}
title={title}
index={index}
moveItem={moveItem}
>
{component && component}
</CardContainer>
)
return (
<div className="grid w-full grid-cols-3 gap-4 text-white">
{items.map(({ id, title, component }, index) =>
renderCardItem(id, title, component, index),
)}
</div>
)
}
moveItem 은 드래그한 아이템의 인덱스와 드롭한 아이템의 인덱스를 바꿔주는 함수이고 이를 드래그 가능한 하위 컴포넌트에게 props로 넘겨주고 있다.
renderCardItem은 드래그할 아이템 목록의 렌더링 컴포넌트다.
이 코드에서는 moveItem과 renderCardItem 함수가 매 렌더링마다 새로 생성된다.
이는 하위 컴포넌트가 props로 받은 함수가 변경되었다고 생각하여 불필요한 재렌더링을 발생시키게 됩니다.
물론 드래그할 아이템이 많지 않다면 문제되지 않지만, 몇 백개가 있다고 하면 성능최적화가 필요된다.
이 문제를 해결하기 위해 useCallback을 사용해 함수 메모이제션을 적용한다. 이는 불필요한 함수 재생성을 막고, 하위 컴포넌트의 불필요한 재렌더링도 막아 성능을 최적화할 수 있다.
useCallback은 리렌더링 간에 함수 정의를 캐싱해주는 리액트 훅이다.
정의는 다음과 같이 한다.
useCallback(fn, dependencies)
fn: 캐싱할 함수값. 리액트 첫 렌더링에서 이 함수를 반환하며 다음 렌더링에서 dependencies 값이 이전과 같다면 같은 함수를 반환한다. 반대로 값이 변경되었다면 해당 렌더링 때에는 전달한 함수를 반환하고 다음에는 재사용할 수 있도록 저장한다. 리액트는 함수를 호출하지 않고 개발자가 함수의 호출 여부와 시점을 결정한다.
dependencies: fn 내에서 참조되는 모든 반응형 값의 목록이다. 이 값은 props, state, 컴포넌트 안에서 직접 선언된 모든 변수와 함수를 포함한다.
'use client'
import React, { useCallback, useState } from 'react'
import update from 'immutability-helper'
import CardContainer from '@/components/CardContainer'
import ErrorList from '@/components/ErrorList'
import ErrorSummaryCard from './ErrorSummaryCard'
export default function DashboardContainer() {
// useState
const [items, setItems] = useState([
{
id: 'item-1',
title: 'Error Overview',
component: <ErrorList />,
},
{ id: 'item-2', title: 'Monthly Uptime' },
{ id: 'item-3', title: 'Error Summary', component: <ErrorSummaryCard /> },
{ id: 'item-4', title: 'Error Overview' },
{ id: 'item-5', title: 'Monthly Uptime' },
{ id: 'item-6', title: 'Error Summary' },
])
// functions
const moveItem = useCallback((dragIndex, hoverIndex) => {
// Swap the index of the dragged item with the dropped item
setItems(prevItems =>
update(prevItems, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, prevItems[dragIndex]],
],
}),
)
}, [])
const renderCardItem = useCallback(
(id, title, component, index) => (
// Render CardItem
<CardContainer
key={id}
id={id}
title={title}
index={index}
moveItem={moveItem}
>
{component && component}
</CardContainer>
),
[moveItem],
)
return (
<div className="grid w-full grid-cols-3 gap-4 text-white">
{items.map(({ id, title, component }, index) =>
renderCardItem(id, title, component, index),
)}
</div>
)
}