느려도 한걸음씩

React Pattern - (2) render props 패턴 본문

FE develop/React Pattern

React Pattern - (2) render props 패턴

hoj0806 2025. 4. 11. 06:14

Render Props 패턴

React 애플리케이션을 개발하다 보면 공통적인 UI 구조에 서로 다른 데이터를 넣어야 하는 상황을 자주 마주하게 됩니다.
그럴 때 유용하게 쓸 수 있는 패턴 중 하나가 바로 Render Props 패턴입니다.

이번 글에서는 실제 예제 코드를 통해 Render Props가 무엇인지, 그리고 적용 전후의 차이점과 장점을 살펴보겠습니다.

 

 

 

다음은 List 컴포넌트를 사용한 예제입니다. 이 컴포넌트는 products나 companies 같은 데이터를 받아 리스트를 렌더링합니다.

import { useState } from "react";
import { faker } from "@faker-js/faker";
import "./styles.css";

const products = Array.from({ length: 20 }, () => {
  return {
    productName: faker.commerce.productName(),
    description: faker.commerce.productDescription(),
    price: faker.commerce.price()
  };
});

const companies = Array.from({ length: 15 }, () => {
  return {
    companyName: faker.company.name(),
    phrase: faker.company.catchPhrase()
  };
});

function ProductItem({ product }) {
  return (
    <li className="product">
      <p className="product-name">{product.productName}</p>
      <p className="product-price">${product.price}</p>
      <p className="product-description">{product.description}</p>
    </li>
  );
}

function CompanyItem({ company, defaultVisibility }) {
  const [isVisible, setIsVisisble] = useState(defaultVisibility);

  return (
    <li
      className="company"
      onMouseEnter={() => setIsVisisble(true)}
      onMouseLeave={() => setIsVisisble(false)}
    >
      <p className="company-name">{company.companyName}</p>
      {isVisible && (
        <p className="company-phrase">
          <strong>About:</strong> {company.phrase}
        </p>
      )}
    </li>
  );
}

function List({ title, items }) {
  const [isOpen, setIsOpen] = useState(true);
  const [isCollapsed, setIsCollapsed] = useState(false);

  const displayItems = isCollapsed ? items.slice(0, 3) : items;

  function toggleOpen() {
    setIsOpen((isOpen) => !isOpen);
    setIsCollapsed(false);
  }

  return (
    <div className="list-container">
      <div className="heading">
        <h2>{title}</h2>
        <button onClick={toggleOpen}>
          {isOpen ? <span>&or;</span> : <span>&and;</span>}
        </button>
      </div>
      {isOpen && (
        <ul className="list">
          {displayItems.map((product) => (
            <ProductItem key={product.productName} product={product} />
          ))}
        </ul>
      )}

      <button onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}>
        {isCollapsed ? `Show all ${items.length}` : "Show less"}
      </button>
    </div>
  );
}

export default function App() {
  return (
    <div>
      <h1>Render Props Demo</h1>

      <div className="col-2">
        <List title="Products" items={products} />
      </div>
    </div>
  );
}

 

이 구조는 ProductItem에 대해서만 하드코딩되어 있기 때문에,
다른 형태의 데이터를 렌더링하려면 컴포넌트를  하나 더 만들거나 조건 분기(if문)를 넣어야 합니다.

이로 인해 다음과 같은 문제가 발생합니다:

  • List 컴포넌트의 재사용성이 낮음
  • 하드코딩된 구조로 인해 UI 로직이 고정됨
  • 다른 데이터(예: companies)를 다루려면 별도의 리스트 컴포넌트를 만들어야 함 → 중복 증가

 

 

✅ Render Props란?

Render Props는 말 그대로 렌더링을 위한 prop(함수)을 컴포넌트에 전달하는 패턴입니다.

즉, 어떤 컴포넌트 내부에서 렌더링할 UI를 직접 정하지 않고,
외부에서 전달받은 함수를 이용해 유연하게 렌더링하는 방식입니다.

 

 

🔁 Render Props 패턴으로 리팩토링

List 컴포넌트를 다음과 같이 수정해 봅시다

 

function List({ title, items, render }) {
  const [isOpen, setIsOpen] = useState(true);
  const [isCollapsed, setIsCollapsed] = useState(false);

  const displayItems = isCollapsed ? items.slice(0, 3) : items;

  function toggleOpen() {
    setIsOpen((isOpen) => !isOpen);
    setIsCollapsed(false);
  }

  return (
    <div className="list-container">
      <div className="heading">
        <h2>{title}</h2>
        <button onClick={toggleOpen}>
          {isOpen ? <span>&or;</span> : <span>&and;</span>}
        </button>
      </div>
      {isOpen && <ul className="list">{displayItems.map(render)}</ul>}

      <button onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}>
        {isCollapsed ? `Show all ${items.length}` : "Show less"}
      </button>
    </div>
  );
}

 

이제 List는 어떤 아이템을 어떻게 렌더링할지 신경 쓰지 않고,
그 책임을 props로 전달받은 render 함수에 위임합니다.

사용자는 아래처럼 자유롭게 구성할 수 있어요

 

<List
  title="Products"
  items={products}
  render={(product) => (
    <ProductItem key={product.productName} product={product} />
  )}
/>

<List
  title="Companies"
  items={companies}
  render={(company) => (
    <CompanyItem key={company.companyName} company={company} defaultVisibility={false} />
  )}
/>

 

 

🌟 Render Props 패턴의 장점

1. 재사용성과 유연성 증가

  • List는 이제 어떤 데이터든, 어떤 컴포넌트든 렌더링 가능
  • 하나의 컴포넌트를 다양한 상황에 맞춰 사용할 수 있음

2. 로직과 UI 분리

  • 리스트 관리(열림/닫힘, 접기 등)는 List가 담당
  • 렌더링할 컴포넌트는 외부에서 결정 → 책임 분리

3. 중복 제거

  • ProductList, CompanyList 같은 컴포넌트를 별도로 만들 필요가 없음
  • 동일한 로직을 재사용하며 UI는 다르게 구성 가능

 

전체코드(render props 패턴 적용)

import { useState } from "react";
import { faker } from "@faker-js/faker";
import "./styles.css";

const products = Array.from({ length: 20 }, () => {
  return {
    productName: faker.commerce.productName(),
    description: faker.commerce.productDescription(),
    price: faker.commerce.price()
  };
});

const companies = Array.from({ length: 15 }, () => {
  return {
    companyName: faker.company.name(),
    phrase: faker.company.catchPhrase()
  };
});

function ProductItem({ product }) {
  return (
    <li className="product">
      <p className="product-name">{product.productName}</p>
      <p className="product-price">${product.price}</p>
      <p className="product-description">{product.description}</p>
    </li>
  );
}

function CompanyItem({ company, defaultVisibility }) {
  const [isVisible, setIsVisisble] = useState(defaultVisibility);

  return (
    <li
      className="company"
      onMouseEnter={() => setIsVisisble(true)}
      onMouseLeave={() => setIsVisisble(false)}
    >
      <p className="company-name">{company.companyName}</p>
      {isVisible && (
        <p className="company-phrase">
          <strong>About:</strong> {company.phrase}
        </p>
      )}
    </li>
  );
}

function List({ title, items, render }) {
  const [isOpen, setIsOpen] = useState(true);
  const [isCollapsed, setIsCollapsed] = useState(false);

  const displayItems = isCollapsed ? items.slice(0, 3) : items;

  function toggleOpen() {
    setIsOpen((isOpen) => !isOpen);
    setIsCollapsed(false);
  }

  return (
    <div className="list-container">
      <div className="heading">
        <h2>{title}</h2>
        <button onClick={toggleOpen}>
          {isOpen ? <span>&or;</span> : <span>&and;</span>}
        </button>
      </div>
      {isOpen && <ul className="list">{displayItems.map(render)}</ul>}

      <button onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}>
        {isCollapsed ? `Show all ${items.length}` : "Show less"}
      </button>
    </div>
  );
}

export default function App() {
  return (
    <div>
      <h1>Render Props Demo</h1>

      <div className="col-2">
        <List
          title="Products"
          items={products}
          render={(product) => (
            <ProductItem key={product.productName} product={product} />
          )}
        />
        <List
          title="Companies"
          items={companies}
          render={(company) => (
            <CompanyItem
              key={company.companyName}
              company={company}
              defaultVisibility={false}
            />
          )}
        />
      </div>
    </div>
  );
}