July 12, 2025

πŸ“‰ κ³Όλ„ν•œ λ©”λͺ¨μ΄μ œμ΄μ…˜(Memoization) λ‚¨μš©: λΆˆν•„μš”ν•œ λ³΅μž‘μ„±κ³Ό μ˜€λ²„ν—€λ“œ

React
JavaScript
μ•„ν‚€ν…μ²˜
μ»΄ν¬λ„ŒνŠΈ
UX

Summary

React useMemo와 useCallback 훅을 μ‹€μ œ μ„±λŠ₯ 병λͺ© 없이 κ³Όλ„ν•˜κ²Œ μ‚¬μš©ν•˜λ©΄, 였히렀 λ©”λͺ¨λ¦¬ 및 μ—°μ‚° μ˜€λ²„ν—€λ“œλ₯Ό λ°œμƒμ‹œν‚€κ³  μ½”λ“œ λ³΅μž‘μ„±μ„ μ¦κ°€μ‹œμΌœ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ νš¨μœ¨μ„±μ„ μ €ν•˜μ‹œν‚΅λ‹ˆλ‹€. λ©”λͺ¨μ΄μ œμ΄μ…˜μ€ μΈ‘μ • ν›„ ν•„μš”ν•œ κ³³μ—λ§Œ μ‹ μ€‘ν•˜κ²Œ μ μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

Why Wrong?

React의 useMemo와 useCallback 훅은 μ»΄ν¬λ„ŒνŠΈμ˜ λΆˆν•„μš”ν•œ λ Œλ”λ§μ„ 막고 계산 λΉ„μš©μ΄ 높은 μž‘μ—…μ„ μ΅œμ ν™”ν•˜λŠ” 데 μœ μš©ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ‹€μ œ μ„±λŠ₯ 병λͺ© 없이 이 훅듀을 κ³Όλ„ν•˜κ²Œ μ‚¬μš©ν•˜λ©΄, 였히렀 λ‹€μŒκ³Ό 같은 λ¬Έμ œμ μ„ μ•ΌκΈ°ν•˜λ©° μ•ˆν‹°νŒ¨ν„΄μ΄ λ©λ‹ˆλ‹€.

  1. λΆˆν•„μš”ν•œ μ˜€λ²„ν—€λ“œ: useMemo와 useCallback은 이전 값을 μ €μž₯ν•˜κ³  μ˜μ‘΄μ„± λ°°μ—΄μ˜ κ°’λ“€κ³Ό ν˜„μž¬ 값듀을 λΉ„κ΅ν•˜λŠ” 자체적인 μ˜€λ²„ν—€λ“œλ₯Ό κ°€μ§‘λ‹ˆλ‹€. λ‹¨μˆœν•œ κ°’μ΄λ‚˜ ν•¨μˆ˜μ— 이 훅을 μ μš©ν•˜λ©΄, μ΄λŸ¬ν•œ 비ꡐ μ—°μ‚° 및 λ©”λͺ¨λ¦¬ ν• λ‹Ή μ˜€λ²„ν—€λ“œκ°€ λ©”λͺ¨μ΄μ œμ΄μ…˜μœΌλ‘œ μ–»λŠ” 잠재적 이점보닀 컀져 μ „λ°˜μ μΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 속도λ₯Ό μ €ν•˜μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.
  2. λ©”λͺ¨λ¦¬ μ‚¬μš©λŸ‰ 증가: λ©”λͺ¨μ΄μ œμ΄μ…˜λœ 값은 κ°€λΉ„μ§€ μ»¬λ ‰μ…˜μ˜ λŒ€μƒμ—μ„œ μ œμ™Έλ˜κ³  λ©”λͺ¨λ¦¬μ— μœ μ§€λ©λ‹ˆλ‹€. 특히 λ§Žμ€ 수의 μ»΄ν¬λ„ŒνŠΈλ‚˜ 자주 λ³€κ²½λ˜λŠ” 데이터λ₯Ό λ©”λͺ¨μ΄μ œμ΄μ…˜ν•˜λ©΄ λ©”λͺ¨λ¦¬ μ‚¬μš©λŸ‰μ΄ λΆˆν•„μš”ν•˜κ²Œ μ¦κ°€ν•˜μ—¬ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 램 μ‚¬μš©λŸ‰μ„ 높일 수 μžˆμŠ΅λ‹ˆλ‹€.
  3. μ½”λ“œ λ³΅μž‘μ„± 증가 및 가독성 μ €ν•˜: λΆˆν•„μš”ν•œ λ©”λͺ¨μ΄μ œμ΄μ…˜μ€ μ½”λ“œλ₯Ό 더 κΈΈκ³  μ΄ν•΄ν•˜κΈ° μ–΄λ ΅κ²Œ λ§Œλ“€λ©°, μ˜μ‘΄μ„± λ°°μ—΄(deps array)을 μ •ν™•ν•˜κ²Œ 관리해야 ν•˜λŠ” 뢀담을 κ°€μ€‘μ‹œν‚΅λ‹ˆλ‹€. 잘λͺ»λœ μ˜μ‘΄μ„± 배열은 μ˜ˆμƒμΉ˜ λͺ»ν•œ λ™μž‘μ΄λ‚˜ λ²„κ·Έλ‘œ μ΄μ–΄μ§ˆ 수 있으며, 디버깅을 λ³΅μž‘ν•˜κ²Œ λ§Œλ“­λ‹ˆλ‹€.
  4. ν΄λ‘œμ €(Closure) 문제: useCallbackμ΄λ‚˜ useMemo의 μ˜μ‘΄μ„± 배열에 νŠΉμ • 값이 λˆ„λ½λ˜λ©΄, ν΄λ‘œμ €μ— μ˜ν•΄ stale closure λ¬Έμ œκ°€ λ°œμƒν•˜μ—¬ 였래된 값을 μ°Έμ‘°ν•˜κ±°λ‚˜ ν•¨μˆ˜κ°€ μ˜ˆμƒκ³Ό λ‹€λ₯΄κ²Œ λ™μž‘ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

How to Fix?

λͺ¨λ“  useMemo와 useCallback μ‚¬μš©μ€ λ°˜λ“œμ‹œ 츑정을 기반으둜 ν•΄μ•Ό ν•©λ‹ˆλ‹€. React Developer Tools의 profiler와 같은 도ꡬλ₯Ό μ‚¬μš©ν•˜μ—¬ μ‹€μ œ μ„±λŠ₯ 병λͺ© 지점을 μ •ν™•νžˆ νŒŒμ•…ν•œ ν›„, λ‹€μŒ 원칙에 따라 λ©”λͺ¨μ΄μ œμ΄μ…˜μ„ μ μš©ν•©λ‹ˆλ‹€.

  1. μΈ‘μ • μš°μ„  (Profile First): μ–΄λ–€ μ»΄ν¬λ„ŒνŠΈκ°€ λΆˆν•„μš”ν•˜κ²Œ 많이 λ Œλ”λ§λ˜κ±°λ‚˜ 계산 λΉ„μš©μ΄ 높은지 ν™•μΈν•˜μ§€ μ•Šκ³  무쑰건적인 λ©”λͺ¨μ΄μ œμ΄μ…˜μ„ ν”Όν•©λ‹ˆλ‹€. μ‹€μ œλ‘œ μ„±λŠ₯ λ¬Έμ œκ°€ λ°œμƒν•˜λŠ” μ§€μ μ—λ§Œ μ§‘μ€‘ν•©λ‹ˆλ‹€.
  2. useMemoλŠ” 계산 λΉ„μš©μ΄ 높은 κ°’μ—λ§Œ μ‚¬μš©: useMemoλŠ” λŒ€κ·œλͺ¨ 데이터 필터링, μ •λ ¬, λ³΅μž‘ν•œ 객체 생성 λ“± CPUλ₯Ό 많이 μ‚¬μš©ν•˜λŠ” μ—°μ‚° κ²°κ³Όλ₯Ό μΊμ‹±ν•˜μ—¬ λΆˆν•„μš”ν•œ μž¬κ³„μ‚°μ„ λ°©μ§€ν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€. 특히 React.memo둜 감싸진 μžμ‹ μ»΄ν¬λ„ŒνŠΈμ— props둜 μ „λ‹¬λ˜λŠ” λ³΅μž‘ν•œ κ°μ²΄λ‚˜ 배열일 경우 μœ μš©ν•©λ‹ˆλ‹€.
  3. useCallback은 μ°Έμ‘° μ•ˆμ •ν™”κ°€ ν•„μš”ν•œ μ½œλ°±μ—λ§Œ μ‚¬μš©: useCallback은 μžμ‹ μ»΄ν¬λ„ŒνŠΈμ— 콜백 ν•¨μˆ˜λ₯Ό props둜 전달할 λ•Œ, 특히 ν•΄λ‹Ή μžμ‹ μ»΄ν¬λ„ŒνŠΈκ°€ React.memo둜 감싸져 μžˆμ–΄ λΆˆν•„μš”ν•œ λ¦¬λ Œλ”λ§μ„ λ°©μ§€ν•΄μ•Ό ν•  κ²½μš°μ— μ‚¬μš©ν•©λ‹ˆλ‹€. ν•¨μˆ˜ μ°Έμ‘°κ°€ 자주 λ³€κ²½λ˜μ–΄ μžμ‹ μ»΄ν¬λ„ŒνŠΈμ˜ λΆˆν•„μš”ν•œ λ¦¬λ Œλ”λ§μ„ μœ λ°œν•˜λŠ” μƒν™©μ—μ„œλ§Œ κ³ λ €ν•©λ‹ˆλ‹€. 일반적인 이벀트 ν•Έλ“€λŸ¬λŠ” useCallback 없이 ν•¨μˆ˜λ₯Ό 직접 μ„ μ–Έν•˜λŠ” 것이 더 κ°„κ²°ν•˜κ³  효율적일 수 μžˆμŠ΅λ‹ˆλ‹€.
  4. μ˜μ‘΄μ„± λ°°μ—΄ 관리: μ˜μ‘΄μ„± 배열을 μ •ν™•ν•˜κ²Œ κ΄€λ¦¬ν•˜μ—¬ λΆˆν•„μš”ν•œ μž¬κ³„μ‚°μ„ λ°©μ§€ν•˜κ³ , 빈 λ°°μ—΄([]) μ‚¬μš© μ‹œ ν΄λ‘œμ € λ¬Έμ œμ— μœ μ˜ν•©λ‹ˆλ‹€. ESLint의 exhaustive-deps 룰을 ν™œμš©ν•˜μ—¬ μ˜μ‘΄μ„± λ°°μ—΄ λˆ„λ½μ„ λ°©μ§€ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.
  5. λŒ€μ•ˆ κ³ λ €: λ©”λͺ¨μ΄μ œμ΄μ…˜ 이전에 μ»΄ν¬λ„ŒνŠΈ ꡬ쑰λ₯Ό κ°œμ„ ν•˜κ±°λ‚˜, μƒνƒœ(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;