Phoenix LiveComponent Provider 패턴
Source: Dev.to

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의 도움을 받아 작성했지만, 모든 내용을 꼼꼼히 검토하고 정리했습니다. 아이디어, 코드, 의견은 모두 제 것입니다.
