루비 온 레일즈를 위한 관리 프레임워크, ActiveAdmin

2020. 7. 3. 17:57위키

Ruby on Rails 를 위한 관리자 프레임워크!

 

ActiveAdmin 은 루비 온 레일즈를 위한 관리 프레임워크입니다. 이 글은 ActiveAdmin 에 대한 가이드입니다. ActiveAdmin.info 에 대한 내용을 다루고 있습니다. 개발환경과 코드는 아래를 참고하시기 바랍니다.

macOS Catalina
ruby 2.6.6
rails 5.2.3
 

hwangwoojin/rails-blog

This is my code from getting started code at RailsGuides v5.2 : https://guides.rubyonrails.org/v5.2/getting_started.html - hwangwoojin/rails-blog

github.com

 

설치하기

기존 루비 온 레일즈에 ActiveAdmin 을 적용하는 방법은 매우 간단합니다. 먼저 루비 온 레일즈 프로젝트에 있는 Gemfile 에 다음 Gem 들을 추가해줍니다. 

# ActiveAdmin
gem 'activeadmin'
# Plus integrations with:
gem 'devise'
gem 'cancancan'
gem 'draper'
gem 'pundit'

activeadmin Gem 아래에 있는 4개의 Gem 들은 사용하면 유용한 Gem 들입니다. 자세히는 다루지 않지만 하나만 짚고 가자면 devise 는 ActiveAdmin 을 위한 훌륭한 보안 옵션을 제공해줍니다. 어쨋든 Gem 을 설치한 후에 다음 명령어를 통해 ActiveAdmin 을 설치할 수 있습니다. 여기서 g 는 generate 명령어의 줄임말입니다.

$ rails g active_admin:install

# 만약 Devise 를 사용하지 않는다면
# rails g active_admin:install --skip-users

# 만약 이미 존재하는 user 클래스를 사용하고 싶다면 
# rails g active_admin:install User

만약 위 명령어를 눌렀는데 무한 로딩이 걸린다면 아래 명령어를 입력한 후 다시 시도해보세요.

$ spring stop

성공했다면 ActiveAdmin 관련 파일이 이것저것 많이 설치가 됩니다. 시작전에 먼저 마이그레이션도 해주어야 합니다.

rails db:migrate
rails db:seed

다 끝났으면 웹서버를 켜보도록 하겠습니다.

rails server

그러면 이제 http://localhost:3000/ 의 모습이 조금 변경되면서 http://localhost:3000/admin/ 에 ActiveAdmin 페이지가 생성됩니다. Hello, World 의 모양이 바뀌는 이유는 ActiveAdmin 의 stylesheet 를 같이 사용하기 때문입니다.

 

왼쪽은 localhost:3000, 오른쪽은 localhost:3000/admin

 

그럼 이제 ActiveAdmin 에 로그인을 해보도록 하겠습니다. 로그인 정보는 Email: admin@example.com, Password: password 입니다. 가끔 로그인이 안될때가 있는데 그럴때는 Ruby on Rails 콘솔로 접속한 후 새로 어드민 계정을 생성해주면 됩니다.

$ rails console
> AdminUser.create!(:email => 'admin@example.com', :password => 'password', :password_confirmation => 'password')

 

그러면 이제 ActiveAdmin 을 위한 모든 준비는 끝났습니다.

 

http://localhost:3000/admin

 

기본 설정하기

ActiveAdmin 의 기본적인 설정은 config/initializers/active_admin.rb 에서 할 수 있습니다. 이 글에서는 기본적인 것들만 만져보도록 하겠습니다. 첫번째는 Authentication 입니다. 로그인을 할 때 권한을 증명하는 부분이죠. 대표적으로 다음과 같은 옵션을 설정할 수 있습니다.

# 보안 증명을 강제로 하도록 하는 메서드
config.authentication_method = :authenticate_admin_user!
# 현재 유저가 누구인지 알기 위한 메서드
config.current_user_method = :current_admin_user

위 설정들을 사용할 생각이 없다면 false 값을 주면 됩니다.

config.authentication_method = false
config.current_user_method = false

 

두번째는 사이트 설정입니다. 사이트의 기본적인 제목, 주소, 이미지 등을 설정할 수 있습니다. 참고로 이미지의 위치는 app/assets/images/logo.png 입니다.

# 사이트의 제목
config.site_title = "My Admin Site"
# 사이트 주소
config.site_title_link  = "/"
# 사이트 이미지
config.site_title_image = "logo.png"

 

세번째는 국가 설정입니다. internationalization 이라고도 하며 i18n 이라고도 하는 부분입니다. 처음에 ActiveAdmin 을 설치하면 영어밖에는 없지만, config/locales 에 ko.yml 을 추가하는 식으로 한국을 위한 설정을 할 수 있습니다. ko.yml 파일에 대해서는 ActiveAdmin 깃허브 페이지 를 참고하시기 바랍니다. 단 여기에는 Devise 를 위한 번역 옵션은 없으니 Devise-i18n 은 따로 설정해주어야 합니다.

 

네번째는 네임스페이스입니다. ActiveAdmin 의 모든 resource 들은 다 네임스페이스를 가집니다. 기본적인 네임스페이스는 admin 입니다. 만약에 admin 페이지와 super_admin 페이지가 있고, 둘 다 각각 다른 타이틀을 가지는 것을 원한다고 생각해 보자구요. 그러면 다음과 같이 다른 네임스페이스를 주어 구별해 줄 수 있습니다.

ActiveAdmin.setup do |config|
  config.site_title = "My Default Site Title"

  config.namespace :admin do |admin|
    admin.site_title = "Admin Site"
  end

  config.namespace :super_admin do |super_admin|
    super_admin.site_title = "Super Admin Site"
  end
end

만약 멀티-테넌트 모델의 어플리케이션인 경우 route_option 을 통해 다음과 같이도 설정 가능합니다.

config.namespace :site_1 do |admin|
  admin.route_options = { path: :admin, constraints: ->(request){ request.domain == "site1.com" } }
end

만약에 path 를 사용하지 않고 subdomain 으로 네임스페이스를 주려면 다음과 같이 하면 됩니다.

config.namespace :admin do |admin|
  admin.route_options = { path: '', subdomain: 'admin' }
end

 

다섯번째는 comment 입니다. comments 는 ActiveAdmin 에서 기본적으로 제공하는 것들 중 하나입니다. dashboard, user 그리고 그 옆에 comment 가 있죠.

 

 

뭔가 유용한 기능 같지만, 아직은 무슨 기능인지 잘 모르겠으므로 그냥 끄도록 하겠습니다. 다음 코드를 통해 Comment 옵션을 끌 수 있습니다. 적용이 안될경우, 웹서버를 한번 껏다가 켜면 적용이 됩니다.

# 모든 어플리케이션에서 comment 끄기
config.comments = false

# 해당 네임스페이스에서 comment 끄기
config.namespace :admin do |admin|
    admin.comments = false
end

 

사실 뭐하는건지 몰라서 꺼버렸다는게 정설.

 

여섯번째는 Utility Navigation 입니다. 이게 뭐냐면 어드민 페이지의 오른쪽 제일 위에 보이는 유저 정보와 로그아웃을 말합니다.

 

 

이를 수정하는 코드는 다음과 같습니다. 여기서는 admin@example.com 을 My Great Website 로 바꾸어보도록 하겠습니다.

config.namespace :admin do |admin|
  admin.build_menu :utility_navigation do |menu|
    menu.add label: "My Great Website", url: "http://www.mygreatwebsite.com", html_options: { target: :blank }
    admin.add_logout_button_to_menu menu
  end
end

 

 

마지막은 Footer 입니다. 보통 페이지의 가장 아래를 Footer 라고 하죠. 초기 설정은 “Powered by ActiveAdmin” 입니다. 이를  My Great Footer 로 바꾸어보도록 하겠습니다. 코드는 다음과 같습니다.

config.footer = 'My Great Footer'

 

Resource 다루기

ActiveAdmin 에서 resource 는 Ruby on Rails 에서 모델을 의미합니다. 하지만 resource 를 생성하기 전에 반드시 해당 모델을 먼저 생성해야 합니다. 모델을 생성하는 Rails 코드는 다음과 같습니다. 여기서는 Post 모델을 생성하겠습니다.

$ bin/rails g model Post
$ bin/rails db:migrate

모델을 생성했으면 이제 resource 를 생성할 차례입니다.

$ rails g active_admin:resource Post

여기까지 문제없이 수행했다면 웹서버를 켰을 때 다음과 같이 Post 라는 이름의 resource 가 생성되는 것을 확인할 수 있습니다.

 

 

그리고 app/admin/posts.rb 라는 파일이 생성됩니다. 이 파일에서 resource 에 대한 자세한 것들을 설정할 수 있습니다. permit_params 를 사용하면 Post 가 어떤 속성을 바꿀 수 있는지를 명시할 수 있습니다.

# app/admin/posts.rb

permit_params :title, :content, :publisher_id

여러 속성값을 전달하는 경우가 있을수 있습니다. 예를 들어 array 속성이나 HABTM(has_and_belongs_to_many) 으로 선언된 속성이 있습니다. 이 경우 다음과 같이 선언해주어야 합니다. HABTM 이 뭐냐구요?

# app/admin/posts.rb

# 만약 HABTM 이 roles 인 경우
permit_params :title, :content, :publisher_id, role_ids: []

만약 여러 속성들을 가지고 있는 params 인 경우도 이와 비슷합니다. 단 이 경우 추가적인 조작이 필요합니다. 새로 클래스를 생성해서 accepts_nested_attributes_for 를 설정해주어야 합니다.

# app/admin/posts.rb

ActiveAdmin.register Post do
  permit_params :title, :content, :publisher_id,
    tags_attributes: [:id, :name, :description, :_destroy]
end

# 중첩된 속성을 위한 코드
class Post < ActiveRecord::Base
  accepts_nested_attributes_for :tags, allow_destroy: true
end

반면에 resource 자체가 중첩된(nested) 경우belongs_to 를 먼저 선언하고, permit_params 를 선언해야 합니다. 순서에 주의하시기 바랍니다.

# app/admin/posts.rb

belongs_to :user
permit_params :title, :content, :publisher_id

동적으로 permit_params 를 설정하고 싶은 경우는 아래와 같습니다. 이 코드는 만약 현재 사용자가 어드민일 경우에 author_id 라는 속성을 추가로 수정할 수 있도록 합니다.

# app/admin/posts.rb

permit_params do
  params = [:title, :content, :publisher_id]
  params.push :author_id if current_user.admin?
  params
end

이렇게 permit_params 메서드를 통해 허용한 속성들은 permitted_params 를 통해 가져올 수 있습니다. params 를 통해서도 parameter 들을 가져올 수 있지만 이보다 permitted_params 를 통해 strong parameter 들만 가져오는 것이 훨씬 더 좋은 방법입니다. 보통 create 메서드를 사용할 때 다음과 같이 사용하는 것이 정석적입니다.

# app/admin/posts.rb

controller do
  def create
    # 좋은 방법
    @post = Post.new(permitted_params[:post])
    # 안좋은 방법
    @post = Post.new(params[:post])
  end
end

 

resource 에서는 기본적으로 모든 CRUD 가 가능합니다. 이를 제한하지 않으면 문제가 생길 수 있겠죠? 이는 actions 에서 설정할 수 있습니다. 아래 코드는 resource 에서 update, destroy 를 제외한 모든 action 들을 허용한다는 의미입니다.

# app/admin/posts.rb

actions :all, except: [:update, :destroy]

액션의 이름은 config/locales 디렉토리에 정의되어 있으며, 원하는대로 수정 가능합니다.

 

resource 의 이름을 바꾸고 싶다면 resource 를 선언한 클래스에서 as 로 원하는 이름을 적어주면 됩니다. 만약 Post resource 를 Article 이라는 이름으로 바꾸고 싶다면 아래 코드처럼 구현하면 되죠.

# app/admin/posts.rb

ActiveAdmin.register Post, as: "Article"

이제 새로고침하고 http://localhost:3000/admin/articles 에 접속하면 정상적으로 동작하는 것을 확인할 수 있습니다. Post 가 아닌 Article 이 되어 말이죠.

 

resource 뿐 아니라 네임스페이스의 이름도 변경할 수 있습니다. 기본은 default 이지만 다음 코드처럼 today 나 false 로 설정할 수 있습니다.

# app/admin/posts.rb

# /today/posts 로 설정하고 싶은 경우
ActiveAdmin.register Post, as: "Article", namespace: :today

# /posts 로 설정하고 싶은 경우
ActiveAdmin.register Post, as: "Article", namespace: false

만약 namespace 를 today 로 설정하게되면 기본 어드민 페이지에서 Article 이 사라지고, http://localhost:3000/today/articles 에서 짜잔하고 등장합니다.

 

이번에는 resource 의 menu 에 대해 다루어보도록 하겠습니다. menu 란 resource 를 만들었을 때 어드민 왼쪽 위에 생성되는 것들을 말합니다. Dashboard, Admin Users 그리고 위에서 생성한 Posts 등입니다. 만약 Posts 를 그냥 안보이게 해버리고 싶다면 menu 에 false 를 주면 됩니다. 이는 if 문을 통해 동적으로 설정할 수도 있습니다.

# app/admin/posts.rb

ActiveAdmin.register Post do
  menu false
  # 만약 현재 유저가 posts 를 edit 할 수 있을때만 보여주고 싶을 때
  # menu if: proc{ current_user.can_edit_posts? }
end

메뉴는 라벨을 주어 이름을 설정할 수도 있습니다.

# app/admin/posts.rb

menu label: "Article"

라벨을 해도 이름이 바뀌고, as: 를 해도 이름이 바뀌죠. 이 둘의 차이는 실제 주소가 바뀌는지의 차이입니다. as 를 통해 선언하게 되면 주소도 바뀌는(/admin/article) 반면에 라벨을 하게 되면 주소는 그대로인채(admin/posts) 메뉴의 이름만 변경되는 식입니다.

 

메뉴의 위치는 priority 를 주어 설정할 수 있습니다. priority 가 1에 가까우면 왼쪽, 크면 클수록 오른쪽입니다. 기본값은 10 이기 때문에 Posts 의 priority 를 1로 주게되면 My Posts 가 메뉴에서 왼쪽으로 이동합니다.

# app/admin/posts.rb

menu priority: 1

 

Admin Users 보다는 왼쪽으로 갔지만, Dashboard 를 넘지는 못했네요.

 

메뉴를 드랍다운으로 구현하고 싶으면 parent 를 사용합니다. 직접 보면 이해가 더 쉬울 것이라 생각합니다.

# app/admin/posts.rb

menu parent: "Blog"

 

Blog 의 하위로 Posts 가 들어갔습니다

 

다중으로도 가능합니다. 예를 들어 Admin 하위에 Blog 하위에 Posts 를 두고 싶다면 다음과 같이 하면 됩니다.

# app/admin/posts.rb

menu parent: ["Admin", "Blog"]

 

 

app/admin/posts.rb 에서만 메뉴를 변경할 수 있는 것은 아닙니다. config/initializer/active_admin.rb 파일에서도 config 를 수정해서 메뉴를 수정할 수 있습니다. 단 이 경우 resource 를 새로 불러와야 하므로 웹서버를 껏다가 켜야 적용이 됩니다. 꽤 간단한 부분이므로 설명은 config 를 어떻게 구현했는지에 대한 코드와 결과 화면으로 대체하겠습니다.

# config/initializer/active_admin.rb

config.namespace :admin do |admin|
  admin.build_menu do |menu|
    menu.add label: "The Application", url: "/", priority: 0

    menu.add label: "Sites" do |sites|
      sites.add label: "Google",
                url: "http://google.com",
                html_options: { target: :blank }

      sites.add label: "Facebook",
                url: "http://facebook.com"

      sites.add label: "Github",
                url: "http://github.com"
    end
  end
end

네임스페이스 사용에만 주의하면 크게 어려운 부분은 없습니다. 정상적으로 입력했다면 다음과 같은 화면이 나오게 됩니다.

 

 

scope 를 사용한다면 어드민마다 페이지에 접근하는 범위를 다르게 할 수도 있습니다. 이부분은 아직 저도 이해중...

# app/admin/posts.rb

# posts 를 current_user.posts 에만 제한하고 싶을 때
scope_to :current_user

# 위 경우에서 default 값이 없을 때
scope_to :current_user, association_method: :blog_posts

# 블록 단위로 범위를 정할 때
scope_to do
  User.most_popular_posts
end

# 동적으로 정할 때
scope_to :current_user, if: proc{ current_user.limited_access? }
scope_to :current_user, unless: proc{ current_user.admin? }

 

1:N 관계에 있는 데이터베이스에서 쿼리문을 실행시킬 때, 관련 테이블의 정보를 모두 가져오므로 성능상 문제가 발생할 수 있습니다. 이를 N+1 문제라고도 하는데 이 경우 참조할 정보를 미리 명시하는 eager loading 을 통해 성능을 향상시킬 수 있습니다. 이는 include 키워드를 사용해서 구현할 수 있습니다.

# app/admin/posts.rb

includes :author, :categories

 

위에서는 resource 를 하나만 선언했지만 만약에 resource 가 여러개있다면 서로 포함관계가 있을수도 있겠죠. 이를 해주는 것이 belongs_to 입니다. 예를 들어 project 하나가 여러개의 ticket 을 가질 수 있다고 가정해보죠. 그러면 이를 다음과 같이 구현할 수 있습니다. 아 물론 그 전에 Model 과 Resource 선언하는거 잊지 마시구요.

# app/admin/projects.rb

ActiveAdmin.register Project
# app/admin/tickest.rb

ActiveAdmin.register Ticket do
  belongs_to :project
end

그러면 ticket resource 는 projects/1/tickets, projects/2/tickets 와 같이 projects 하위에 id 값을 가진채로 설정이 됩니다.