고급 반응 패턴: 렌더 소품

안녕하세요 👋

꽤 오랫동안 React로 작업했다면 재사용 가능한 구성 요소를 작성하는 데 정통해야 합니다. 재사용 가능한 UI 구성 요소! 그러나 코드베이스가 확장됨에 따라 비즈니스 로직을 공유하고 싶지만 UI가 다를 수 있는 상황에 종종 직면하게 됩니다.

💡 이와 같은 상황은 일부 고급 패턴을 사용할 수 있는 완벽한 기회입니다. Render Props는 그러한 패턴 중 하나입니다.

🚀 렌더링 소품 패턴을 사용하는 일부 라이브러리에는 React Router, Downshift 및 Formik이 포함됩니다.

예부터 시작하겠습니다.

예를 들어 제품을 전시하는 온라인 상점을 구축하고 있습니다.

모든 제품 목록에 필요한 공통 비즈니스 논리가 있습니다.
  • ✈️ 클릭 시 상품 페이지 페이지로 이동
  • 🤑 가격을 기준으로 제품 정렬
  • 💾 제품 저장
  • 사용자가 로그인하지 않은 경우 로컬 저장소에 저장
  • 그렇지 않으면 API 호출을 사용하여 DB에 저장합니다.


  • 🎨 그러나 목록이 렌더링되는 위치에 따라 UI도 다를 수 있습니다. 한 곳에서는 통계 또는 제품 이미지를 보여주고 싶고, 다른 곳에서는 제목만 보여주고 싶을 수도 있습니다.

    🧠 먼저 렌더 소품 구성 요소의 기본 구조를 이해해 봅시다.

    
    const Wrapper = ({ products, render }) => {
    
      // do some stuff
      const updatedProducts = someOperations(products)
    
      // provide some helper funcs for data like sort func
      const sort = () => {}
    
      return render({ updatedProducts, sort })
    }
    
    
    


    👉 Render Props 구성 요소는 UI 구성 요소를 감싸는 래퍼일 뿐입니다. 위의 구성 요소는 productsrender 두 개의 소품을 얻습니다. products는 비즈니스 로직을 사용하여 수정해야 하는 데이터이고 render는 이 수정된 데이터와 일부 다른 도우미 함수가 전달되는 함수입니다.

    🤔 그런데 이 구성 요소를 어떻게 사용합니까?

    // import everything
    
    const HomeScreenProducts = () => {
    
      // assume you have this hook
      const { products } = useData()
    
      return (
        <ProductsWrapper 
         products={products}
         render={
           ({ updatedProducts, sort }) => updatedProducts.map(product => <ProductCard />)
         }
        />
      )
    
    }
    
    


    👉 우리의 HomeScreenProducts 구성 요소는 ProductsWrapper를 사용하고 모든 비즈니스 로직을 처리하도록 합니다. UI는 여전히 호출 구성 요소에 의해 제어됩니다.
    내부render 함수에서는 수정된 제품 데이터를 소비하고 이를 기반으로 UI를 렌더링합니다.

    😰 조금 복잡해 보입니다. 하지만 훨씬 더 깨끗한 API를 갖도록 단순화할 수 있습니다. render 함수를 별도로 전달하는 대신 children 소품을 사용할 수 있습니다.

    업데이트 후 두 구성 요소는 다음과 같습니다.

    
    const Wrapper = ({ products, children }) => {
    
      // same stuff here
    
      return children({ updatedProducts, sort })
    }
    
    



    
    const HomeScreenProducts = () => {
    
      // same stuff here
    
      return (
        <ProductsWrapper products={products}>
          {({ updatedProducts, sort }) => updatedProducts.map(product => <ProductCard />}
        </ProductsWrapper>
      )
    
    }
    
    


    👌 이게 훨씬 낫습니다. children 소품은 이전에 사용했던 render 소품과 동일한 용도로 사용됩니다. 렌더 소품을 작성하는 이 방법이 더 일반적입니다.

    ⚠️ 목록에 key를 추가하는 것을 잊지 마세요.

    💪 이제 렌더링 소품 패턴을 이해했으므로 앞에서 언급한 작업을 완료할 수 있습니다.

    import React from "react";
    import { useAuth } from "./hooks/useAuth";
    
    const ProductsWrapper = ({ products, children }) => {
      const { isLoggedIn } = useAuth();
      const [sortedProducts, setSortedProducts] = React.useState(products);
    
      const sort = (order) => {
        const reorderedProducts = [...products];
    
        reorderedProducts.sort((a, b) => {
          if (order === "desc") {
            return b.price > a.price;
          } else {
            return a.price > b.price;
          }
        });
    
        setSortedProducts(reorderedProducts);
      };
    
      const save = (productId) => {
        if (isLoggedIn) {
          // call API
          console.log("Saving in DB... ", productId);
        } else {
          // save to local storage
          console.log("Saving in local storage... ", productId);
        }
      };
    
      const navigate = () => {
        console.log("Navigating...");
      };
    
      return children({ sortedProducts, sort, save, navigate });
    };
    
    export default ProductsWrapper;
    
    

    ProductsWrapper 구성 요소를 확장하고 여기에 필요한 모든 기능을 추가합니다. children를 함수로 호출하고 데이터 및 헬퍼 함수를 ​​전달합니다.

    import ProductsWrapper from "./ProductsWrapper";
    
    const products = [
      { id: 1, name: "Coffee", price: 2 },
      { id: 2, name: "Choclates", price: 3 },
      { id: 3, name: "Milk", price: 5 },
      { id: 4, name: "Eggs", price: 4 },
      { id: 5, name: "Bread", price: 1 }
    ];
    
    export default function App() {
      return (
        <div className="App">
          <ProductsWrapper products={products}>
            {({ sortedProducts, sort, save, navigate }) => (
              <>
                <div className="flex">
                  <button onClick={() => sort("desc")}>Price: High to Low</button>
                  <button onClick={() => sort("asc")}>Price: Low to High</button>
                </div>
    
                {sortedProducts.map((product) => (
                  <div className="product" key={product.id}>
                    <span onClick={() => navigate(product.id)}>
                      {product.name} - ${product.price}
                    </span>
                    <button className="save-btn" onClick={() => save(product.id)}>
                      save
                    </button>
                  </div>
                ))}
              </>
            )}
          </ProductsWrapper>
        </div>
      );
    }
    


    ✅ 우리의 UI 구성 요소는 ProductsWrapper를 사용하고 UI를 관리합니다. 보시다시피 자유롭게 UI를 수정하거나 이것과 완전히 다르게 보이는 다른 UI 구성 요소를 만들 수 있습니다. 우리의 비즈니스 로직은 한 곳에 있습니다.

    예제를 가지고 놀고 싶다면 codesandbox에서 사용할 수 있습니다: https://codesandbox.io/s/render-props-example-6190fb

    그게 다야! 👋

    🤙 도움이 되었다면 공유하고 에서 저와 연결해 보세요.

    좋은 웹페이지 즐겨찾기