Phoenix LiveComponent Provider 패턴

발행: (2026년 2월 1일 오전 01:46 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

Cover image for Phoenix LiveComponent Provider Pattern

Mykolas Mankevicius

React를 사용해 본 적이 있다면, 아마 Provider 패턴을 사용해 보았을 것입니다. 이것은 자식 컴포넌트를 감싸고 컨텍스트를 통해 데이터를 제공하는 컴포넌트입니다.

Phoenix LiveView에서는 LiveComponent의 :let 지시자와 assign_async를 결합하여 비슷한 것을 구현할 수 있습니다.

문제

제품 상세 페이지에서 유사한 제품들을 보여줘야 했습니다. 가장 단순한 방법은 부모 LiveView의 mount에서 이를 로드하는 것이었습니다:

def mount(params, _session, socket) do
  product = get_product!(params)

  # Blocking the mount with an async search operation 🙈
  {:ok, similar_products} = Search.similar_products(product.id, limit: 12)

  socket
  |> assign(:product, product)
  |> assign(:similar_products, similar_products)
  |> ok()
end

여기에는 두 가지 문제가 있습니다:

  • 마운트 차단 – 검색 작업 때문에 전체 페이지 렌더링이 지연됩니다.
  • 관심사의 분산 – 유사 제품 로직이 부모 LiveView에 섞여 있습니다.

Source:

공급자 패턴

대신, 자체 데이터 로딩을 처리하고 :let을 통해 결과를 자식에게 노출하는 공급자 컴포넌트를 만들었습니다.

공급자 (LiveComponent)

defmodule MarkoWeb.Components.SimilarProducts.Provider do
  use MarkoWeb, :live_component

  @impl LiveComponent
  def update(assigns, socket) do
    {%{product_id: product_id, limit: limit}, assigns} =
      Map.split(assigns, [:product_id, :limit])

    socket
    |> assign(assigns)
    |> assign_async(:search, fn ->
      {:ok, result} = Search.similar_products(product_id, limit: limit)
      {:ok, %{search: result}}
    end)
    |> ok()
  end

  @impl LiveComponent
  def render(assigns) do
    ~H"""
    
      
        
          
        
          {render_slot(@loading)}
        
      

      
        {render_slot(@inner_block, %{items: @search.result.hits, query: @search.result.query})}
      

      
        
          
        
          {render_slot(@no_results)}
        
      
    
    """
  end

  defp loading(assigns) do
    ~H"""
    
    """
  end

  defp no_results(assigns) do
    ~H"""
    
No results

    """
  end
end

핵심 라인은 다음과 같습니다:

{render_slot(@inner_block, %{items: @search.result.hits, query: @search.result.query})}

맵을 전달함으로써 공급자가 노출하는 내용을 정확히 제어할 수 있습니다. 나중에 데이터를 더 추가해야 할 경우? 키 하나만 더 추가하면 됩니다.

컴포넌트 모듈

defmodule MarkoWeb.Components.SimilarProducts do
  use MarkoWeb, :component

  # === 편의성 컴포넌트 ===

  attr :product, Product, required: true
  attr :limit, :integer, required: true
  slot :loading
  slot :no_results

  def grid(assigns) do
    ~H"""
    
      {render_slot(slot)}
      {render_slot(slot)}
      
    
    """
  end

  # === 공급자 (맞춤 렌더링용) ===

  attr :id, :any, default: nil
  attr :product, Product, required: true
  attr :limit, :integer, required: true
  slot :inner_block, required: true
  slot :loading
  slot :no_results

  def provider(assigns) do
    ~H"""
    
      {render_slot(@inner_block, context)}
    
    """
  end

  # === 내부 프레젠테이션 ===

  defp grid_inner(assigns) do
    ~H"""
    

      - 

    

    """
  end
end

사용법

대부분의 경우, 제공자에 대해 알 필요조차 없습니다:

# Grid layout – just use it

맞춤 로딩/빈 상태 – 여전히 간단합니다:

# (Insert custom loading / empty state markup here)

맞춤 렌더링? 제공자를 사용하세요:

# (Insert custom rendering markup here)
# Example call inside a template
<.similar_products.provider
  product={@product}
  limit={12}
  loading={...}
  no_results={...}>
  <:inner_block let={%{items: items, query: query}}>
    <!-- custom rendering using items and query -->
  </:inner_block>
</.similar_products.provider>

왜 이렇게 작동하는가

  • Simple things are simple – 기본 그리드는 바로 사용할 수 있습니다.
  • Complex things are possible – 필요할 때 제공자가 전체 시각 제어를 노출합니다.
  • Self‑contained async – 제공자가 데이터 가져오기를 자체적으로 관리합니다.
  • Extensible – 더 많은 데이터가 필요하나요? 컨텍스트 맵에 키를 추가하세요.

This is essentially React’s Context Provider pattern, but in Phoenix LiveView.

LiveView

제품 상세 페이지가 유사 제품 로딩을 관리하던 방식에서, 단순히 어디에 표시될지 선언하는 방식으로 바뀌었습니다. 훨씬 깔끔해졌습니다.

이 게시물은 AI의 도움을 받아 작성했지만, 모든 내용을 꼼꼼히 검토하고 정리했습니다. 아이디어, 코드, 의견은 모두 제 것입니다.

Back to Blog

관련 글

더 보기 »

CSS 선택자 101

CSS 선택자 – 요소를 타깃팅하는 방법 HTML을 작성했습니다. 이제 CSS로 멋지게 꾸미고 싶습니다. 하지만 CSS에게 어떤 요소를 스타일링해야 하는지 어떻게 알려줄까요? T...