Musta React

[✍🏼 1/4 (Sat)] Prop을 넘겨주는 방식에 따른 useMemo() 사용 여부 - 부모 컴포넌트에서 분해 or 자식 컴포넌트에서 분해

taytay 2025. 1. 4. 15:03
import { useCallback, useMemo, useState } from "react";
import Product from "./Product";
import Shipping from "./Shipping";
import Product2 from "./Product2";

function App() {
  // 🖍️Product2 문제해결🖍️
  // product prop에 해당하는 data 값을 리턴해주는 함수를 만들고, 이 함수의 리턴값을 메모이제이션하는 useMemo()를 사용하면, 매번 캐시된 값을 반환하여 수량이 변경되어도 불필요하게 리렌더링 되지 않는다!
  // useMemo: 함수를 인자로 전달하고, 전달된 함수의 실행 결과(리턴값)를 memoize 함
  // 헷갈리지말자, useMemo는 함수가 아니라, 함수의 "리턴값"을 메모이제이션하는 것..
  // 따라서, 리렌더링 될 때마다 새롭게 생성되는 객체타입의 데이터를 몽땅 useMemo()로 메모이제이션하면 된다!!!
  // > 함수를 메모이제이션하는 것은 useCallback!! = 보통, prop자체가 함수일 경우 사용
  // > 컴포넌트를 메모이제이션하는 것은 memo!!
  const data = useMemo(
    () => ({
      _id: 2,
      price: 125000,
      shippingFees: 3000,
      name: "나이키 잼",
      quantity: 35,
      buyQuantity: 10,
      mainImage: "/files/00-nike/NIKE_JAM_01.jpg",
      content:
        "나이키가 세계적인 무대에 오르는 브레이크 댄서를 위해 제작한 첫 신발인 잼과 함께 몸과 마음, 정신을 하나로 만들어 보세요. 신발의 모든 디테일을 꼼꼼히 제작했기 때문에 자신 있게 사이퍼에 도전할 수 있습니다. 유연하고 내구성이 뛰어난 갑피가 몸을 따라 움직이며, 중창의 텍스처 처리된 핸드 그립 덕분에 공중에서 신발을 쉽게 잡을 수 있습니다. 그리고 위아래가 뒤집힌 로고를 배치해 프리즈 동작을 할 때 로고가 똑바로 보이는 재미를 더했죠.",
    }),
    []
  );

  const [quantity, setQuantity] = useState(1);
  const [shippingFees, setShippingFees] = useState(data.shippingFees);
  // (물건 가격) * (수량)
  const productPrice = data.price * quantity;

  // 수량이 변경되면 배송비 다시 계산
  const handleQuantityChange = (e) => {
    const newQuantity = Number(e.target.value); // 인풋값은 항상 string으로 넘어온다
    setQuantity(newQuantity);
    setShippingFees(data.shippingFees * Math.ceil(newQuantity / 5));
  };

  // Shipping component
  // 👉 fee : number type() -> 수량이 5단위로 넘어가지 않는 이상(=> 화면 리렌더링 필수), 값은 동일하므로 수량 바뀔 때마다 render❌
  // 👉 handlePayment : func type(참조형) -> 앱 리렌더링 시, 이 함수자체는 바뀐 값(state system X)이 없음에도 불구하고, 참조형 데이터는 힙에 매번 새로운 객체로 만들어지므로, 다른 데이터라고 판단해, 수량 바뀔 때마다 render됨 => 매우 불필요한 성능을 저해하는 작업!
  // 이때, handlePayment()함수만 메모이제이션하여 굳이 리렌더링 될 필요없도록 만들고 싶다면!
  // -> 함수를 메모이제이션하는 훅인 useCallback()을 써주면 된다..

  // cf-1) useMemo(): 함수를 인자로 전달하고, 전달된 함수의 리턴값(실행결과)을 메모라이즈함.
  // ex) 05-useMemo/isPrime()
  // cf-2) React.memo(): 컴포넌트 자체를 인자로 전달하고, 전달된 컴포넌트를 메모라이즈함.
  // ex) const Product = memo(function Product({ name, price, mainImage, content }) {
  const handlePayment = useCallback(() => {
    alert(`배송비 ${shippingFees}원이 추가됩니다. 상품을 결제하시겠습니까?`);
  }, [shippingFees]);
  // 함수 안에 함수가 있고, 그 함수 내부에 지역변수를 참조하고 있다 -> "Closure"
  // useCallback()으로 인해 해당 함수가 메모이제이션되어 최초로 마운트 됐을 당시에 캐시되어있던 데이터(data.shippingfees = 3000)를 불러오기 때문에 수량을 변경해 배송비가 변경됐을 때도 3000이 나오는 것! => dependencies에 shippingfees를 넣어주어 해당 값이 바뀌면(= 수량이 5단위로 증가되는 순간에만 shippingfees가 handleQuantityChange 함수에 의해 바뀜) 메모이제이션된 캐시된 데이터를 쓰는게 아닌, 새롭게 호출하여 변경된 shippingfees(= state value)가 들어가게끔 해준다!

  return (
    <>
      <h1>
        06 useCallback(함수 자체를 memoize), React.memo(컴포넌트를 memoize)
      </h1>
      <Product2 product={data} />

      {/* Product */}
      <Product
        name={data.name}
        price={data.price}
        mainImage={data.mainImage}
        content={data.content}
      />

      <h2>수량 선택</h2>
      <div>
        가격: {data.price.toLocaleString()}원
        <br />
        수량:
        {/* 제어 컴포넌트로 만든다 = state 시스템으로 관리한다. => 값이 변경됐을 때, 화면도 같이 변경되어야 할 때.. */}
        <input
          type="number"
          min="1"
          max={data.quantity - data.buyQuantity}
          value={quantity}
          onChange={handleQuantityChange}
        />
        (배송비는 5개당 {data.shippingFees.toLocaleString()}원씩 추가됩니다.)
        <br />
        상품 금액: {productPrice.toLocaleString()}원
      </div>

      {/* Shipping */}
      <Shipping fees={shippingFees} handlePayment={handlePayment} />
    </>
  );
}

export default App;

 

🗣️ : <Product>처럼 data속성의 여러 속성값들을 Prop으로 미리 쪼개서 보내지 않고, <Product2>는 data 객체 뭉텅이를 product라는 이름의 prop으로 보낸다음, Product2컴포넌트 안에서 다시 또 객체 {}로 나눠서 사용하고 있는 것에서 오는 문제점을 해결하기 위해 useMemo()를 써서 함수의 리턴값(=data 객체)을 메모이제이션(캐시)해서 객체 안의 속성값들이 바껴도 불필요하게 리렌더링되지 않도록 했다고 배웠는데, 이 부분이 잘 이해가 안가.. 일단 난 갠적으로 <Product>처럼 Prop을 일일이 미리 다 쪼개서 보내는 방식으로 쓰긴 하겠지만, useMemo()를 공부하기 위해 알아둬야 할 것 같아서 물어봐! <Product2> 부분 + useMemo()로 해결한 부분을 쉽게 설명해줄수있니?

 

🤖 : <Product2>에서 data 객체를 prop으로 통째로 전달하고, 이후 컴포넌트 내부에서 data 객체의 속성들을 분해해 사용하는 방식은 간단하지만, 효율성 측면에서 문제가 발생할 수 있습니다. 이 문제를 해결하기 위해 useMemo()를 사용한 이유를 단계적으로 설명하겠습니다.

 

1. 문제의 원인: 객체와 참조형 데이터의 특징

  • <Product2> 방식:
    • data 객체를 통째로 product prop으로 전달하면, 매번 컴포넌트가 리렌더링될 때마다 새로운 data 객체가 생성됩니다.
    • React는 참조형 데이터(객체, 배열 등)가 리렌더링될 때마다 새로운 메모리 주소를 가지는 것으로 간주합니다.
    • 따라서, data 객체의 내용이 동일하더라도, React는 새로운 객체로 인식하고 <Product2>를 다시 렌더링합니다.

2. useMemo()를 사용한 해결

  • useMemo()의 역할:
    • useMemo()는 특정 값(여기서는 data 객체)을 메모이제이션하여, 종속성(dependencies)이 변경되지 않는 한 해당 값을 재생성하지 않습니다.
    • 즉, data 객체가 동일하다면, React는 이전에 생성된 data 객체를 재사용합니다.
    • useMemo()는 빈 종속성 배열 []을 가졌으므로, 이 data 객체는 컴포넌트가 처음 렌더링될 때 한 번만 생성되고, 이후 동일한 객체가 계속 재사용됩니다.
const data = useMemo(
  () => ({
    _id: 2,
    price: 125000,
    shippingFees: 3000,
    name: "나이키 잼",
    quantity: 35,
    buyQuantity: 10,
    mainImage: "/files/00-nike/NIKE_JAM_01.jpg",
    content: "나이키 잼 설명...",
  }),
  []
);

 

3. 왜 <Product2>에서 문제가 발생했을까?

  • <Product2>가 data 객체를 직접 prop으로 받으면서, 매번 새로운 객체가 전달되었기 때문에 불필요한 리렌더링이 발생했습니다.
  • <Product>처럼 객체의 속성값을 개별적으로 prop으로 전달하면, 개별 prop의 변경 여부만 React가 감지하면 되므로 불필요한 렌더링이 줄어듭니다.

 

 

4. <Product> vs <Product2> 방식

<Product> 방식:

<Product
  name={data.name}
  price={data.price}
  mainImage={data.mainImage}
  content={data.content}
/>

 

  • 장점:
    • 각 prop이 독립적이므로, 특정 prop만 변경되면 React는 해당 prop이 변경되었음을 정확히 감지.
    • 불필요한 리렌더링이 줄어듦.
  • 단점:
    • 코드가 길어지고, prop이 많아질수록 관리가 번거로움.

 

<Product2> 방식:

<Product2 product={data} />

 

 

  • 장점:
    • prop으로 객체를 통째로 전달하므로 코드가 간결함.
  • 단점:
    • 객체 자체가 변경되면 React는 새 객체로 간주하여 불필요한 리렌더링이 발생.

 

5. 핵심 요약

  • 문제: <Product2> 방식에서는 객체 자체가 변경되었는지 감지하기 어렵기 때문에, 불필요한 리렌더링 발생.
  • 해결: useMemo()를 사용해 data 객체를 메모이제이션하여, 동일한 데이터는 재사용.
  • useMemo()의 역할:
    • data 객체가 생성되는 비용이 크거나, 객체를 prop으로 전달하여 불필요한 렌더링을 피하고자 할 때 유용.
  • 실제 개발 팁:
    • 데이터가 많고, 복잡한 객체를 자주 전달해야 한다면 useMemo()를 활용.
    • 단순한 컴포넌트라면 <Product> 방식처럼 개별 prop으로 전달하는 것도 충분히 좋은 방법.

 

 

🗣️ :  이건 지금 고정 데이터를 쓰고 있는 경우지만, 나중에 api서버에서 데이터를 받아온다고 했을 때도 만약 Product2 방식처럼 Prop을 넘겨준다면 useMemo()가 필요하니? 이 부분을 react query나 react-redux query를 쓰면 캐싱 기능이 자동으로 적용돼서 굳이 사용하지 않아도 되는 걸로 기억하는데...

🤖 : 좋은 질문이야! 고정 데이터를 사용하는 경우와 API 서버에서 데이터를 받아오는 경우에는 상황에 따라 useMemo()가 필요할 수도 있고, 그렇지 않을 수도 있어. React Query나 React-Redux Query와 같은 라이브러리가 이를 어떻게 처리하는지도 관련이 있어.

 

1. API로 데이터를 받아올 때 useMemo()의 필요성

  • 데이터가 불변하거나 변경될 일이 거의 없는 경우:
    API에서 데이터를 받아와서 상태에 저장하고, 이를 자식 컴포넌트에 넘겨준다면 보통은 useMemo()가 필요 없을 가능성이 높아. 이유는 React Query나 Redux Query와 같은 라이브러리가 제공하는 캐싱 덕분에 데이터를 상태로 관리하거나 다시 가져올 때 효율적으로 처리되기 때문이야.
  • 데이터가 변경되거나 가공될 경우:
    만약 API에서 받아온 데이터를 컴포넌트 내부에서 가공해서 사용하고, 이 가공된 데이터를 자식 컴포넌트에 넘겨준다면 useMemo()를 사용해 메모이제이션하는 것이 유용할 수 있어. 이렇게 하면 동일한 입력 데이터에 대해 가공 로직이 반복 실행되지 않아 성능이 최적화돼.

 

2. React Query나 React-Redux Query의 역할

이 라이브러리들은 데이터를 가져오고 관리하는 데 필요한 많은 기능을 제공해. 특히 useMemo()와 겹치는 역할을 하는 몇 가지가 있어:

  • 자동 캐싱:
    데이터를 가져오면 동일한 요청에 대해 캐싱된 데이터를 반환하기 때문에 불필요한 중복 요청을 방지해.
  • 데이터 무효화 및 갱신:
    캐싱된 데이터가 오래된 경우 무효화(invalidate)하고, 새로운 데이터를 가져오도록 자동으로 처리해.
  • 의존성 관리:
    서버에서 데이터를 가져오는 로직은 React Query가 알아서 관리하므로, 데이터의 변경이 필요할 때 직접 useMemo()를 사용할 필요가 줄어들어

 

3. Product2 스타일의 Prop 전달과 useMemo()의 필요성

  • useMemo()가 필요할 때:
    Product2처럼 객체를 통째로 Prop으로 넘기고, 이 객체가 부모에서 변경될 가능성이 있다면 useMemo()를 사용해서 메모이제이션하면 불필요한 리렌더링을 막을 수 있어. 특히, 객체를 새로 생성하는 로직이 매번 실행되면 React는 참조 값이 변경되었다고 판단해 자식 컴포넌트를 리렌더링하거든.
  • React Query 사용 시:
    React Query를 사용하면 데이터를 캐싱하고 관리해주기 때문에 부모 컴포넌트에서 객체를 매번 새로 생성하지 않아도 될 확률이 높아. 따라서 useMemo() 없이도 불필요한 리렌더링을 줄이는 효과를 얻을 수 있어.

 

4. 최종 정리

  • API 데이터를 받을 때 React Query나 Redux Query 사용:
    이 경우 useMemo()를 직접 사용할 필요가 줄어듦. 라이브러리가 데이터를 효율적으로 관리하기 때문이야.
  • React Query를 사용하지 않고 데이터를 상태로 관리하는 경우:
    데이터를 가공하거나 객체로 묶어서 넘길 때 불필요한 리렌더링을 방지하려면 useMemo()를 사용하는 것이 유용할 수 있음.
  • 권장 스타일:
    개인적으로 Product처럼 Prop을 필요한 값만 쪼개서 전달하는 방식을 추천해. 이렇게 하면 리렌더링 관리가 더 쉬워지고, 의도하지 않은 참조 변경으로 인한 리렌더링을 줄일 수 있어.