동호회 관리 플랫폼 개발기 #3 - Member Resource

Wonwoo ChoWonwoo Cho
3 min read

지난번엔 동호회를 Club 이라는 리소스로 만들었다.
동호회에 속한 User 를 의미하는 Member 리소스를 생성해보자.

Resource 정의

defmodule Hoing.Clubs.Member do
  # ...
  attributes do
    uuid_v7_primary_key :id

    attribute :role, :atom, 
      constraints: [one_of: [:owner, :manager, :member]],
      default: :member

    timestamps()
  end

  relationships do
    belongs_to :club, Hoing.Clubs.Club
    belongs_to :user, Hoing.Accounts.User
  end
end

속성은 최대한 단순하게 설정했다.
Member 는 role 을 가지며, 앞으로 멤버가 할 수 있는 행위를 role 을 통해서 제한한다.

Action 정의

defmodule Hoing.Clubs.Member do
    # ...
    actions do
        defaults [:read]

        create :create do
          accept [:club_id, :user_id, :role]
        end
    end
end

action 도 최대한 단순하게 필요한 것만 작성했다.

이제 Club 을 생성할 때 자동으로 생성한 User 를 Member 로 추가하는 코드를 작성해보자.
그리고 이 Member 는 :owner Role 을 가져야 한다.
기존의 Club :create action 을 수정하자.

Create action 에 hook 추가

defmodule Hoing.Clubs.Club do
   actions do
    # ...
    create :create do
      accept [:name, :description]

      change after_action(fn _changeset, club, context ->
        creator = context.actor

        Hoing.Clubs.Member
        |> Ash.Changeset.for_create(:create, %{
          club_id: club.id,
          user_id: creator.id,
          role: :owner
        })
        |> Ash.create!()

        {:ok, club}
      end)
    end
  end
end

after_action 은 3개의 인자를 받는다.
그중 2번째 인자로 record 를 받고, 3번째 인자를 통해서 해당 action 을 실행한 actor 를 받을 수 있다.

테스트

아래와 같이 실행해보자. 물론 Clubs Domain 에 create action 이 define 되어 있어야 한다.

club = Hoing.Clubs.create_club!(%{name: "My Club"}, actor: user)

club 변수를 inspect 해보면 member 가 not loaded 상태이다.
club 의 member 를 load 해보자.

club |> Ash.load!(:member)
%Hoing.Clubs.Club{
  id: "0198087a-289a-7d95-ad13-592598cb057e",
  name: "My Club",
  description: nil,
  inserted_at: ~U[2025-07-13 10:28:00.282870Z],
  updated_at: ~U[2025-07-13 10:28:00.282870Z],
  members: [
    %Hoing.Clubs.Member{
      id: "0198087a-28a4-788a-af2c-09581ef83bc4",
      role: :owner,
      inserted_at: ~U[2025-07-13 10:28:00.292553Z],
      updated_at: ~U[2025-07-13 10:28:00.292553Z],
      club_id: "0198087a-289a-7d95-ad13-592598cb057e",
      user_id: "59aa8cf6-716a-4b81-b333-ed99b808ba0e",
      club: #Ash.NotLoaded<:relationship, field: :club>,
      user: #Ash.NotLoaded<:relationship, field: :user>,
      __meta__: #Ecto.Schema.Metadata<:loaded, "members">
    }
  ],
  __meta__: #Ecto.Schema.Metadata<:loaded, "clubs">
}

member 가 잘 생성되었고, role 도 :owner 로 설정된 것을 확인할 수 있다.

Extra

role 에 대한 정의가 좀 산만하다.
Ash 는 enum 타입을 module 로 정의할 수 있도록 지원한다.
모듈코드는 단순히 Ash.Type.Enum behaviour 를 사용하면 된다.

defmodule Hoing.Clubs.Role do
  use Ash.Type.Enum, values: [:owner, :admin, :member]
end

생성한 Role 을 Member 에 적용하면 아래와 같이 코드가 깔끔해진다.

defmodule Hoing.Clubs.Member do
  attributes do
    # ...
    attribute :role, Hoing.Clubs.Role do
      allow_nil? false
      default :member
    end
    # ...
  end
end

한군데 더 산만한 코드가 있다.
Club 생성 시 after_action 에 전달하는 함수가 익명함수로 로직이 리소스파일에 그대로 노출된다.
앞으로 리소스파일은 점점 커질테니 미리 별도의 파일에 분리하는게 좋을 것 같다.

defmodule Hoing.Clubs.Changes.CreateOwnerMember do
  def after_action(_changeset, club, context) do
    creator = context.actor

    Hoing.Clubs.Member
    |> Ash.Changeset.for_create(:create, %{
      club_id: club.id,
      user_id: creator.id,
      role: :owner
    })
    |> Ash.create!()

    {:ok, club}
  end
end

after_action 함수를 정의하는 별도의 모듈을 생성하고 아래와 같이 Club 리소스에도 적용한다.

defmodule Hoing.Clubs.Club do
  alias Hoing.Clubs.Changes.CreateOwnerMember
  # ...
  actions do
    create :create do
      change after_action(&CreateOwnerMember.after_action/3)
    end
    # ...
  end
end

이렇게 하면 핵심이 되는 정보들만 있는 깔끔한 Resource 파일을 유지할 수 있다.

0
Subscribe to my newsletter

Read articles from Wonwoo Cho directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Wonwoo Cho
Wonwoo Cho