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


지난번엔 동호회를 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 파일을 유지할 수 있다.
Subscribe to my newsletter
Read articles from Wonwoo Cho directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
