π κ³Όλν λ©λͺ¨μ΄μ μ΄μ (Memoization) λ¨μ©: λΆνμν 볡μ‘μ±κ³Ό μ€λ²ν€λ
Summary
React useMemo
μ useCallback
ν
μ μ€μ μ±λ₯ λ³λͺ© μμ΄ κ³Όλνκ² μ¬μ©νλ©΄, μ€νλ € λ©λͺ¨λ¦¬ λ° μ°μ° μ€λ²ν€λλ₯Ό λ°μμν€κ³ μ½λ 볡μ‘μ±μ μ¦κ°μμΌ μ ν리μΌμ΄μ
μ ν¨μ¨μ±μ μ νμν΅λλ€. λ©λͺ¨μ΄μ μ΄μ
μ μΈ‘μ ν νμν κ³³μλ§ μ μ€νκ² μ μ©ν΄μΌ ν©λλ€.
Why Wrong?
Reactμ useMemo
μ useCallback
ν
μ μ»΄ν¬λνΈμ λΆνμν λ λλ§μ λ§κ³ κ³μ° λΉμ©μ΄ λμ μμ
μ μ΅μ ννλ λ° μ μ©ν©λλ€. νμ§λ§ μ€μ μ±λ₯ λ³λͺ© μμ΄ μ΄ ν
λ€μ κ³Όλνκ² μ¬μ©νλ©΄, μ€νλ € λ€μκ³Ό κ°μ λ¬Έμ μ μ μΌκΈ°νλ©° μν°ν¨ν΄μ΄ λ©λλ€.
- λΆνμν μ€λ²ν€λ:
useMemo
μuseCallback
μ μ΄μ κ°μ μ μ₯νκ³ μμ‘΄μ± λ°°μ΄μ κ°λ€κ³Ό νμ¬ κ°λ€μ λΉκ΅νλ μ체μ μΈ μ€λ²ν€λλ₯Ό κ°μ§λλ€. λ¨μν κ°μ΄λ ν¨μμ μ΄ ν μ μ μ©νλ©΄, μ΄λ¬ν λΉκ΅ μ°μ° λ° λ©λͺ¨λ¦¬ ν λΉ μ€λ²ν€λκ° λ©λͺ¨μ΄μ μ΄μ μΌλ‘ μ»λ μ μ¬μ μ΄μ λ³΄λ€ μ»€μ Έ μ λ°μ μΈ μ ν리μΌμ΄μ μλλ₯Ό μ νμν¬ μ μμ΅λλ€. - λ©λͺ¨λ¦¬ μ¬μ©λ μ¦κ°: λ©λͺ¨μ΄μ μ΄μ λ κ°μ κ°λΉμ§ 컬λ μ μ λμμμ μ μΈλκ³ λ©λͺ¨λ¦¬μ μ μ§λ©λλ€. νΉν λ§μ μμ μ»΄ν¬λνΈλ μμ£Ό λ³κ²½λλ λ°μ΄ν°λ₯Ό λ©λͺ¨μ΄μ μ΄μ νλ©΄ λ©λͺ¨λ¦¬ μ¬μ©λμ΄ λΆνμνκ² μ¦κ°νμ¬ μ ν리μΌμ΄μ μ λ¨ μ¬μ©λμ λμΌ μ μμ΅λλ€.
- μ½λ 볡μ‘μ± μ¦κ° λ° κ°λ
μ± μ ν: λΆνμν λ©λͺ¨μ΄μ μ΄μ
μ μ½λλ₯Ό λ κΈΈκ³ μ΄ν΄νκΈ° μ΄λ ΅κ² λ§λ€λ©°, μμ‘΄μ± λ°°μ΄(
deps array
)μ μ ννκ² κ΄λ¦¬ν΄μΌ νλ λΆλ΄μ κ°μ€μν΅λλ€. μλͺ»λ μμ‘΄μ± λ°°μ΄μ μμμΉ λͺ»ν λμμ΄λ λ²κ·Έλ‘ μ΄μ΄μ§ μ μμΌλ©°, λλ²κΉ μ 볡μ‘νκ² λ§λλλ€. - ν΄λ‘μ (Closure) λ¬Έμ :
useCallback
μ΄λuseMemo
μ μμ‘΄μ± λ°°μ΄μ νΉμ κ°μ΄ λλ½λλ©΄, ν΄λ‘μ μ μν΄ stale closure λ¬Έμ κ° λ°μνμ¬ μ€λλ κ°μ μ°Έμ‘°νκ±°λ ν¨μκ° μμκ³Ό λ€λ₯΄κ² λμν μ μμ΅λλ€.
How to Fix?
λͺ¨λ useMemo
μ useCallback
μ¬μ©μ λ°λμ μΈ‘μ
μ κΈ°λ°μΌλ‘ ν΄μΌ ν©λλ€. React Developer Toolsμ profilerμ κ°μ λꡬλ₯Ό μ¬μ©νμ¬ μ€μ μ±λ₯ λ³λͺ© μ§μ μ μ νν νμ
ν ν, λ€μ μμΉμ λ°λΌ λ©λͺ¨μ΄μ μ΄μ
μ μ μ©ν©λλ€.
- μΈ‘μ μ°μ (Profile First): μ΄λ€ μ»΄ν¬λνΈκ° λΆνμνκ² λ§μ΄ λ λλ§λκ±°λ κ³μ° λΉμ©μ΄ λμμ§ νμΈνμ§ μκ³ λ¬΄μ‘°κ±΄μ μΈ λ©λͺ¨μ΄μ μ΄μ μ νΌν©λλ€. μ€μ λ‘ μ±λ₯ λ¬Έμ κ° λ°μνλ μ§μ μλ§ μ§μ€ν©λλ€.
useMemo
λ κ³μ° λΉμ©μ΄ λμ κ°μλ§ μ¬μ©:useMemo
λ λκ·λͺ¨ λ°μ΄ν° νν°λ§, μ λ ¬, 볡μ‘ν κ°μ²΄ μμ± λ± CPUλ₯Ό λ§μ΄ μ¬μ©νλ μ°μ° κ²°κ³Όλ₯Ό μΊμ±νμ¬ λΆνμν μ¬κ³μ°μ λ°©μ§ν λ μ¬μ©ν©λλ€. νΉνReact.memo
λ‘ κ°μΈμ§ μμ μ»΄ν¬λνΈμ propsλ‘ μ λ¬λλ 볡μ‘ν κ°μ²΄λ λ°°μ΄μΌ κ²½μ° μ μ©ν©λλ€.useCallback
μ μ°Έμ‘° μμ νκ° νμν μ½λ°±μλ§ μ¬μ©:useCallback
μ μμ μ»΄ν¬λνΈμ μ½λ°± ν¨μλ₯Ό propsλ‘ μ λ¬ν λ, νΉν ν΄λΉ μμ μ»΄ν¬λνΈκ°React.memo
λ‘ κ°μΈμ Έ μμ΄ λΆνμν 리λ λλ§μ λ°©μ§ν΄μΌ ν κ²½μ°μ μ¬μ©ν©λλ€. ν¨μ μ°Έμ‘°κ° μμ£Ό λ³κ²½λμ΄ μμ μ»΄ν¬λνΈμ λΆνμν 리λ λλ§μ μ λ°νλ μν©μμλ§ κ³ λ €ν©λλ€. μΌλ°μ μΈ μ΄λ²€νΈ νΈλ€λ¬λuseCallback
μμ΄ ν¨μλ₯Ό μ§μ μ μΈνλ κ²μ΄ λ κ°κ²°νκ³ ν¨μ¨μ μΌ μ μμ΅λλ€.- μμ‘΄μ± λ°°μ΄ κ΄λ¦¬: μμ‘΄μ± λ°°μ΄μ μ ννκ² κ΄λ¦¬νμ¬ λΆνμν μ¬κ³μ°μ λ°©μ§νκ³ , λΉ λ°°μ΄(
[]
) μ¬μ© μ ν΄λ‘μ λ¬Έμ μ μ μν©λλ€. ESLintμexhaustive-deps
λ£°μ νμ©νμ¬ μμ‘΄μ± λ°°μ΄ λλ½μ λ°©μ§νλ κ²μ΄ μ’μ΅λλ€. - λμ κ³ λ €: λ©λͺ¨μ΄μ μ΄μ μ΄μ μ μ»΄ν¬λνΈ κ΅¬μ‘°λ₯Ό κ°μ νκ±°λ, μν(state)λ₯Ό λΆλ¦¬νμ¬ λ¦¬λ λλ§ λ²μλ₯Ό μ΅μννλ κ²μ΄ λ κ·Όλ³Έμ μ΄κ³ ν¨μ¨μ μΈ ν΄κ²°μ± μΌ μ μμ΅λλ€. λλ‘λ μν κ΄λ¦¬ λΌμ΄λΈλ¬λ¦¬μ μ λ ν°(selector) κΈ°λ₯μ νμ©νλ κ²μ΄ λ λμ μ μμ΅λλ€.
Before Code (Bad)
// src/components/ProductList.jsx
import React, { useState, useMemo, useCallback } from 'react';
// ProductListItemμ React.memoλ‘ κ°μΈμ Έμμ§ μμ μ½λ°± ν¨μμ μ°Έμ‘° μμ νκ° ν° μλ―Έκ° μμ
const ProductListItem = ({ product, onAddToCart }) => {
// console.log(`Rendering ProductListItem: ${product.name}`); // λΆνμν λ λλ§ νμΈμ©
return (
<li>
{product.name} - ${product.price}
<button onClick={() => onAddToCart(product.id)}>Add to Cart</button>
</li>
);
};
function ProductList({ products }) {
const [searchTerm, setSearchTerm] = useState('');
// π
ββοΈ λΆνμν useMemo 1: κ°λ¨ν νν°λ§ λ‘μ§μ μ€λ²ν€λ μΆκ°
const filteredProducts = useMemo(() => {
// console.log('Filtering products with useMemo...');
return products.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [products, searchTerm]); // μμ‘΄μ± λ°°μ΄ λ³κ²½ μλ§λ€ λΉκ΅ μ°μ° μν
// π
ββοΈ λΆνμν useCallback 1: ProductListItemμ΄ React.memoλ‘ κ°μΈμ Έ μμ§ μμ μ½λ°± μ°Έμ‘° μμ νμ μ΄μ μ΄ μμ
const handleAddToCart = useCallback((productId) => {
console.log(`Added product ${productId} to cart`);
}, []); // μμ‘΄μ± λ°°μ΄μ΄ λΉμ΄μμ΄ ν¨μ μ°Έμ‘°κ° λ³νμ§ μμ§λ§, λΆνμν μ€λ²ν€λ λ°μ
// π
ββοΈ λΆνμν useMemo 2: λ°°μ΄ κΈΈμ΄ κ³μ° κ°μ μμ£Ό κ°λ¨ν κ³μ°μ μ€λ²ν€λ μΆκ°
const totalItemsCount = useMemo(() => {
// console.log('Calculating total items with useMemo...');
return filteredProducts.length;
}, [filteredProducts]); // μ΄ μμ κ°λ¨ν κ³μ°μ΄λΌ useMemoκ° νμ μμ μ μμ
return (
<div>
<input
type="text"
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<h2>Products ({totalItemsCount} items)</h2>
<ul>
{filteredProducts.map(product => (
<ProductListItem key={product.id} product={product} onAddToCart={handleAddToCart} />
))}
</ul>
</div>
);
}
export default ProductList;
After Code (Good)
// src/components/ProductList.jsx
import React, { useState } from 'react'; // useMemo, useCallback ν
μ κ±°
// ProductListItemμ React.memoλ‘ κ°μΈμ Έμμ§ μμ μ½λ°± ν¨μμ μ°Έμ‘° μμ νκ° ν° μλ―Έκ° μμ
const ProductListItem = ({ product, onAddToCart }) => {
// console.log(`Rendering ProductListItem: ${product.name}`);
return (
<li>
{product.name} - ${product.price}
<button onClick={() => onAddToCart(product.id)}>Add to Cart</button>
</li>
);
};
function ProductList({ products }) {
const [searchTerm, setSearchTerm] = useState('');
// β
κ°λ¨ν νν°λ§ λ‘μ§μ useMemo μμ΄ μ§μ μ€ν
// (λ°μ΄ν° μμ΄ λ§€μ° λ§μ κ³μ° λΉμ©μ΄ λμ κ²½μ°μλ§ useMemo κ³ λ €)
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// β
μμ μ»΄ν¬λνΈκ° React.memoλ‘ κ°μΈμ Έ μμ§ μλ€λ©΄ useCallback λΆνμ
// (ν¨μ μμ²΄κ° λ³΅μ‘νκ±°λ, λ€λ₯Έ κ³³μμ μ°Έμ‘° μμ νκ° νμν κ²½μ°κ° μλλΌλ©΄)
const handleAddToCart = (productId) => {
console.log(`Added product ${productId} to cart`);
};
// β
κ°λ¨ν κ³μ°μ useMemo μμ΄ μ§μ μν
const totalItemsCount = filteredProducts.length;
return (
<div>
<input
type="text"
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<h2>Products ({totalItemsCount} items)</h2>
<ul>
{filteredProducts.map(product => (
<ProductListItem key={product.id} product={product} onAddToCart={handleAddToCart} />
))}
</ul>
</div>
);
}
export default ProductList;