react데이터 핸들링
개요
일반적으로 array를 사용해서 여러개의 데이터를 관리한다. 하지만 이렇게 관리시 얕은 비교를 통해서 React.memo를 사용해도 리스트 전체 데이터가 리렌더링되는 방식으로 된다.
Map을 사용하면 변경된 항목만 새로운 객체를 생성하고 나머지 항목은 기존참조를 유지하게 된다. 따라서 나머지 항목들은 리렌더링되지 않아 React.memo와 사용시 렌더링 최적화에 우수하다. / 여러 항목들을 같이 관리해야할 상황에서 update가 잦을 때 사용하면 좋다.
기존 Array 방식의 문제점
일반적으로 React에서 리스트 데이터를 관리할 때 배열을 사용합니다:
const [users, setUsers] = useState([
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
]);
// 특정 사용자 업데이트
const updateUser = (id, newData) => {
setUsers(prevUsers =>
prevUsers.map(user =>
user.id === id ? { ...user, ...newData } : user
)
);
};
이 방식의 문제점:
- 전체 배열이 새로 생성됨: 하나의 항목만 변경되어도 전체 배열이 재생성
- 모든 컴포넌트 리렌더링: 배열 참조가 변경되어 모든 자식 컴포넌트가 리렌더링
- O(n) 검색 복잡도: 특정 항목을 찾기 위해 전체 배열을 순회
Map을 활용한 효율적인 데이터 관리
Map 데이터 구조를 사용하면 이러한 문제점들을 해결할 수 있습니다:
const [usersMap, setUsersMap] = useState(new Map([
[1, { id: 1, name: 'Alice', age: 25 }],
[2, { id: 2, name: 'Bob', age: 30 }],
[3, { id: 3, name: 'Charlie', age: 35 }]
]));
// 특정 사용자 업데이트
const updateUser = (id, newData) => {
setUsersMap(prevMap => {
const newMap = new Map(prevMap);
const existingUser = newMap.get(id);
newMap.set(id, { ...existingUser, ...newData });
return newMap;
});
};
Map 활용의 주요 이점
1. 빠른 검색 성능
// Array: O(n) 복잡도
const user = users.find(u => u.id === targetId);
// Map: O(1) 복잡도
const user = usersMap.get(targetId);
2. 부분적 업데이트
Map을 사용하면 변경된 항목만 새로운 객체를 생성하고, 나머지는 기존 참조를 유지할 수 있습니다:
const useOptimizedUsers = () => {
const [usersMap, setUsersMap] = useState(new Map());
const updateUser = useCallback((id, updates) => {
setUsersMap(prev => {
const newMap = new Map(prev);
const existingUser = newMap.get(id);
// 기존 객체와 비교하여 실제 변경이 있을 때만 업데이트
if (existingUser && !isEqual(existingUser, { ...existingUser, ...updates })) {
newMap.set(id, { ...existingUser, ...updates });
return newMap;
}
return prev; // 변경사항이 없으면 기존 Map 반환
});
}, []);
return { usersMap, updateUser };
};
3. 메모이제이션과의 완벽한 조합
React.memo와 함께 사용하면 더욱 효과적입니다:
const UserItem = React.memo(({ user, onUpdate }) => {
console.log(`Rendering user: ${user.name}`); // 변경된 항목만 출력됨
return (
<div>
<h3>{user.name}</h3>
<p>Age: {user.age}</p>
<button onClick={() => onUpdate(user.id, { age: user.age + 1 })}>
Increase Age
</button>
</div>
);
});
const UserList = () => {
const { usersMap, updateUser } = useOptimizedUsers();
return (
<div>
{Array.from(usersMap.values()).map(user => (
<UserItem
key={user.id}
user={user}
onUpdate={updateUser}
/>
))}
</div>
);
};
실전 활용 예제: 쇼핑 카트
const useShoppingCart = () => {
const [itemsMap, setItemsMap] = useState(new Map());
const addItem = useCallback((product) => {
setItemsMap(prev => {
const newMap = new Map(prev);
const existingItem = newMap.get(product.id);
if (existingItem) {
newMap.set(product.id, {
...existingItem,
quantity: existingItem.quantity + 1
});
} else {
newMap.set(product.id, { ...product, quantity: 1 });
}
return newMap;
});
}, []);
const removeItem = useCallback((productId) => {
setItemsMap(prev => {
const newMap = new Map(prev);
newMap.delete(productId);
return newMap;
});
}, []);
const updateQuantity = useCallback((productId, quantity) => {
if (quantity <= 0) {
removeItem(productId);
return;
}
setItemsMap(prev => {
const newMap = new Map(prev);
const item = newMap.get(productId);
if (item) {
newMap.set(productId, { ...item, quantity });
}
return newMap;
});
}, [removeItem]);
const totalPrice = useMemo(() => {
return Array.from(itemsMap.values())
.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}, [itemsMap]);
return {
items: Array.from(itemsMap.values()),
itemsMap,
addItem,
removeItem,
updateQuantity,
totalPrice
};
};
성능 비교
// 성능 측정을 위한 간단한 벤치마크
const measurePerformance = (operation, iterations = 1000) => {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
operation();
}
const end = performance.now();
return end - start;
};
// Array vs Map 비교
const arrayData = Array.from({ length: 1000 }, (_, i) => ({ id: i, value: i }));
const mapData = new Map(arrayData.map(item => [item.id, item]));
// 검색 성능
const arraySearchTime = measurePerformance(() => {
arrayData.find(item => item.id === 500);
});
const mapSearchTime = measurePerformance(() => {
mapData.get(500);
});
console.log(`Array 검색: ${arraySearchTime}ms`);
console.log(`Map 검색: ${mapSearchTime}ms`);
주의사항 및 Best Practices
1. Map 크기가 작을 때는 Array가 더 효율적일 수 있음
데이터 크기가 매우 작다면 (< 10개) Array의 단순함이 더 나을 수 있습니다.
2. 직렬화 고려
Map은 JSON.stringify로 직접 직렬화되지 않으므로, 필요시 변환 로직이 필요합니다:
// Map을 일반 객체로 변환
const mapToObject = (map) => Object.fromEntries(map);
// 일반 객체를 Map으로 변환
const objectToMap = (obj) => new Map(Object.entries(obj));
3. 커스텀 훅으로 추상화
재사용성을 위해 Map 기반 상태 관리를 커스텀 훅으로 추상화하는 것이 좋습니다:
const useMapState = (initialData = []) => {
const [dataMap, setDataMap] = useState(
new Map(initialData.map(item => [item.id, item]))
);
const operations = useMemo(() => ({
get: (id) => dataMap.get(id),
set: (id, data) => setDataMap(prev => new Map(prev).set(id, data)),
delete: (id) => setDataMap(prev => {
const newMap = new Map(prev);
newMap.delete(id);
return newMap;
}),
clear: () => setDataMap(new Map()),
values: () => Array.from(dataMap.values()),
keys: () => Array.from(dataMap.keys()),
size: dataMap.size
}), [dataMap]);
return [dataMap, operations];
};
결론
Map 데이터 구조를 활용한 상태 관리는 React 애플리케이션의 성능을 크게 향상시킬 수 있는 강력한 기법입니다. 특히 대량의 데이터를 다루거나 빈번한 업데이트가 발생하는 경우, Map의 O(1) 검색 성능과 부분적 업데이트 특성이 큰 이점을 제공합니다.
하지만 모든 상황에서 Map이 최선의 선택은 아닙니다. 데이터의 크기, 사용 패턴, 팀의 익숙함 등을 종합적으로 고려하여 적절한 데이터 구조를 선택하는 것이 중요합니다. Map을 도입할 때는 점진적으로 적용하면서 실제 성능 향상을 측정해보는 것을 권장합니다.