#406 Public Activity
- Download:
- source codeProject Files in Zip (93.9 KB)
- mp4Full Size H.264 Video (22.1 MB)
- m4vSmaller H.264 Video (12.2 MB)
- webmFull Size VP8 Video (15.8 MB)
- ogvFull Size Theora Video (26.1 MB)
Github에서와 같이 흔히 웹사이트상에서 사용자들의 활동을 로그로 남기기를 원합니다. 이러한 것은 소셜네트워크 스타일의 어플리케이션에 적합한 기능인데 이를 통해서 다른 사용자들의 활동내역을 할 수 있습니다.
요리책 어플리케이션이 있다고 가정하겠습니다. 여기에서 사용자들은 요리법을 생성하고 수정하고 공유할 수 있도록 구현하였습니다. 각 요리법에 대해서 댓글을 추가할 수 있고 다른 사용자들을 친구로 표시해 둘 수 있습니다. 이제 친구들의 모든 활동을 볼 수 있는 페이지를 하나 추가해서 새로운 요리법을 추가하거나 댓글을 다는 경우와 같은 활동내역을 알 수 있게 될 것입니다.
이러한 로직을 구현하기 위해서 Public Activity 젬을 사용할 것입니다. 어플리케이션에서 이 젬을 사용하기 위해서는 gemfile에 이 젬을 추가하고 bundle
명령을 실행하여 설치합니다.
gem 'public_activity'
이 젬에서 사용할 데이터베이스 테이블을 생성하기 위해서 publish_activity:migration
이라는 제너레이터를 실행한 후에 데이터베이스를 마이그레이션합니다.
$ rails g public_activity:migration $ rake db:migrate
이로써 사용자들의 활동을 관리하기 위해 액티브레코드 모델에서 사용하게 될 activities
라는 이름의 테이블을 생성하게 됩니다. Public Activity는 Mongoid와도 잘 호환이 되지만 이와 같은 마이그레이션 작업은 필요치 않습니다. 이와 같은 셋업을 사용할 경우에는 관련 문서를 참조하기 바랍니다. 다음 단계는 활동을 추적하고자 하는 모델에 PublicActivity::Model
모듈을 include 하고 tracked
라는 메소드를 호출합니다. 여기서는 Recipe
모델에서 이러한 작업을 하게 될 것입니다.
class Recipe < ActiveRecord::Base include PublicActivity::Model tracked attr_accessible :description, :image_url, :name belongs_to :user has_many :comments, dependent: :destroy end
tracked
메소드는 하나의 모델이 생성되고 업데이트되고 삭제될 때 자동으로 관련 활동에 대한 레코드를 작성하도록 몇가지 콜백 메소드를 설정하게 됩니다. 이러한 작업을 Comment
모델에서도 수행하도록 하여 관련 활동을 추적하고자 합니다. 이제 특정 요리법에 대해서 댓글을 달게 되면 자동으로 Public Activity가 추적하게 될 것입니다.
Activities 웹페이지
다음은 이러한 활동상황을 한 눈에 볼 수 있는 웹페이지를 만들 필요가 있습니다. 이를 위해서 index
액션을 가지는 새로운 컨틀로러를 하나 생성할 것입니다.
$ rails g controller activities index
다음은 라우트 파일을 변경해서 자동으로 생성된 라우트를 activities 리소스로 교체할 것입니다.
resources :activities
이 컨트롤러의 index 액션에서 모든 활동내역을 보여 주고자 합니다. PublicActivity::Activity
를 호출하게 되면 해당 액티브레코드 모델을 반환하여 데이터베이스에 대해 쿼리를 할 수 있게 될 것입니다. 활동내역이 생성된 시점으로 배열순서를 지정하면 최상단에 가장 최근에 작성된 활동내역이 보이게 될 것입니다.
class ActivitiesController < ApplicationController def index @activities = PublicActivity::Activity.order("created_at desc") end end
뷰 페이지에서 이 데이터에 대해서 루프를 돌면서 표시해 줄 것입니다. 이제 각각의 활동에 대해서 어떤 내용이 포함되어 있는지 알게 될 것입니다.
<h1>Friends' Activities</h1> <% @activities.each do |activity| %> <%= activity.inspect %> <% end %>
이제 페이지를 다시 로드하면 방금 전에 추가한 하나의 활동내역을 보게 될 것입니다.
여기서, trackable_id
와 trackable_type
컬럼을 포함해서 활동내역이 가지는 추가 속성들을 볼 수 있을 것입니다. 이것은 polymorpic 관계설정을 위한 것이고 이 활동내역이 Comment
모델과 연관된 것임을 알 수 있습니다. 또한 recipient
와 owner
와도 polymorphic 관계가 설정되어 있습니다. owner는 특정 활동을 수행한 사용자가 되며, 이 값을 지정하면 각 활동내역 옆에 사용자의 이름을 표시할 수 있게 됩니다. 다음에 이러한 작업을 수행할 것입니다.
이 젬은 활동내역을 기록하기 위해 콜백을 사용한다고 이전에 언급한 바 있습니다. 그러나 여기에는 하나의 문제점이 있습니다. 즉, 모델 레이어(layer)에서는 current user에 대해 접근할 수 없기 때문에 활동내역을 기록할 때 owner를 지정할 수 없게 됩니다. Public Activity는 이러한 문제점에 대한 해결책을 가지고 있어서 ApplicationController
에 하나의 모듈을 include해 주면 됩니다.
class ApplicationController < ActionController::Base include PublicActivity::StoreController # Rest of class omitted end
이렇게하면 요청시마다 해당 컨트롤러를 기록하여 모델로부터 해당 컨트롤러를 접근할 수 있게 해 줍니다. tracked
메소드에 owner
옵션을 추가해 주면 Comment
모델에서 해당 컨트롤러에 접근할 수 있게 됩니다.
tracked owner: ->(controller, model) { controller.current_user }
이 옵션에 lambda를 지정해서 사용자의 활동을 추적할 때마다 실행하도록 할 수 있습니다. 컨트롤러와 모델이 이 lambda로 넘겨지게 되어 해당 컨트롤러를 이용하여 current user를 owner로 설정할 수 있게 되는 것입니다. 그러나 이때 약간의 문제점이 발생할 수 있는데, current user
는 ApplicationController
에서 private 메소드로 지정되어 있다는 것입니다. 따라서 이 메소드를 public으로 지정하고 hide_action
을 사용하여 더 이상 액션으로 인식되지 못하도록 할 것입니다.
class ApplicationController < ActionController::Base include PublicActivity::StoreController protect_from_forgery def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end helper_method :current_user hide_action :current_user private def require_login redirect_to login_url, alert: "You must first log in or sign up." if current_user.nil? end end
또 다른 잠재된 문제점은, Comment
모델에서 controller.current_user
호출하게 될 때, 컨트롤러가 존재하지 않는데도 불구하고 현재의 요청 밖에서 레코드를 생성하려고 시도할 경우 예외가 발생한다는 것입니다. 따라서 해당 컨트롤러로부터 current user를 가져오기 전에 그 컨트롤러가 존재하는지를 체크할 것입니다.
tracked owner: ->(controller, model) { controller && controller.current_user }
이러한 해결책은 이상적이지 않아서 모델 내에서 컨트롤러를 접근해야 한다는 것이 조금 어색하게 느껴질 수 있지만 그런데로 동작을 합니다. 또한 Recipe
모델에도 이것을 복사해서 사용자의 정보를 사용합니다. 이제 댓글을 또 하나 추가하고 다시 사용자 활동내역이 있는 페이지로 이동할 것입니다.
이제는 두개의 활동내역을 볼 수 있게 됩니다. 첫번째 것은 가장 최근에 추가되었기 때문에 owner_id
값이 할당된 것을 볼 수 있을 것입니다. 이 페이지 뷰를 수정해서 활동내역이 대신 보이도록 할 것입니다.
<h1>Friends' Activities</h1> <% @activities.each do |activity| %> <div class="activity"> <%= link_to activity.owner.name, activity.owner if activity.owner %> added comment to <%= link_to activity.trackable.recipe.name, activity.trackable.recipe %> </div> <% end %>
각각의 활동내역에 대해서 activity
클래스를 가지는 div
태크로 감싸주고 owner가 할당되어 있는 경우, 그 태그 안에 활동내역의 owner 이름을 표시합니다. 다음에는 해당 활동내역을 기술하는 부분인데, 이것은 활동내역 각각이 다양하게 기술되기 때문에 약간 어려울 수 있습니다. 따라서 우선은 모든 활동이 comments에 대한 것으로 하드코딩하도록 하겠습니다. 그리고 요리명을 해당 요리법으로의 링크로 표시합니다. 해당 요리법으로 이동으로 하기 위해서 activity.trackable
객체를 이용하게 됩니다. 이것은 활동의 대상이 되는 모델을, 여기서는 Comment
모델, 참조하는 polymorphic 관계 객체입니다. 또한 활동내역 목록을 나타내기 위해서 약간의 스타일링 작업을 할 것이며 아래와 같습니다.
.activity { border-bottom: solid 1px #CCC; padding: 16px 0; em { color: #777; font-size: 12px; padding-left: 5px; } }
이제 페이지를 다시 로드하면 아래와 같이 활동내역 목록이 보여져야 합니다.
이것은 상당히 좋아 보이지만 기술부분이 하드코딩되어 있습니다. 그렇다면 어떻게 하면 활동 형태에 따라 동적으로 변경할 수 있을까요? Public Activity는 render_activity
라는 헬퍼 메소드를 자체적으로 제공해 주기 때문에 이를 사용하면 activity를 인수로 넘겨주기만 하면 됩니다.
<h1>Friends' Activities</h1> <% @activities.each do |activity| %> <div class="activity"> <%= link_to activity.owner.name, activity.owner if activity.owner %> <%= render_activity activity %> </div> <% end %>
이렇게 하면 해당 활동의 액션에 대해서 partial 템플릿을 렌더링해 주게 됩니다. 이를 위해서 public_activity
디렉토리에서 해당 뷰 템플릿 파일을 검색하게 되는데, 추적하는 각 모델에 대해서 디렉토리를 생성할 필요가 있습니다. 댓글이 생성될 때 표시할 partial 템플릿 파일이 필요한데 _create.html.erb
라고 파일명을 지정합니다. 이 파일에서 이전과 같이 활동내역을 기술하면 되는 것입니다.
added comment to <%= link_to activity.trackable.recipe.name, activity.trackable.recipe %>
이것은 이전에 했던 것처럼 보일지라도 이제는 _destroy
와 _update
를 포함하는 각각의 활동 형태별로 partial 뷰 템플릿 파일을 정의할 수 있다는 것이 차이라고 할 수 있습니다. 이후에 댓글을 추가하거나 업데이트하면 활동내역 페이지에 어떻게 나타나는지 알 수 있을 것입니다.
이제 모든 것이 잘 동작하는 것 같지만 하나의 요리법을 삭제한 후에 다시 활동내역을 나타내는 페이지를 방문하게 되면 에러가 발생하게 됩니다.
이것은 더 이상 존재하지 않는 요리법에 대해서 activity.trackable.recipe
를 호출하기 때문입니다. 따라서 각 활동 액션에 대한 partial 템플릿 파일에서, 특정 객체가 더 이상 존재하지 않을 경우를 고려하는 것이 중요하므로 다음과 같이 각 템플릿 파일을 수정할 것입니다.
added comment to <% if activity.trackable %> <%= link_to activity.trackable.recipe.name, activity.trackable.recipe %> <% else %> which has since been removed <% end %>
이제 활동내역 페이지를 다시 로드하게 되면 다시 제대로 동작하게 될 것입니다.
액션 제외하기
다음은 특정 액션에 대해서 추적을 제외하는 방법에 대해서 소개하겠습니다. 예를 들어, 업데이트한 댓글들은 추적할 만큼 그리 관심을 끌만한 내용이 아니므로 활동내역 목록에서 제외하기를 원할 수 있습니다. Comment
모델의 tracked
메소드 호출시에 옵션을 지정하면 이러한 경우를 해결할 수 있게 되는데, 추적할 활동만을 표시하기 위해서는 only
옵션을, 제외할 것을 표시하기 위해서는 except
옵션을 지정하면 됩니다.
tracked except: :update, owner: ->(controller, model) { controller && controller.current_user }
tracked
메소드가 이러한 옵션들과 함께 조금 복잡해 지게 되면 추적하는 모델에 대해서 콜백방식으로 모든 활동내역을 추적하는 것이 반드시 최상의 방법이라고 할 수 없습니다. 때로는 컨트롤러를 통해서 대신 처리하는 방법도 고려할 필요가 있습니다. 이를 위해서, PublicActivity::Model
대신에 모델에 PublicActivity::Common
모듈을 include합니다. 이 때는 물론, tracked
메소드 호출을 제거할 수 있습니다.
class Comment < ActiveRecord::Base include PublicActivity::Common attr_accessible :content belongs_to :user belongs_to :recipe end
이제부터는 댓글을 저장하고 업데이트하거나 삭제할 때마다 CommentsController
에서 활동내역을 기록할 수 있습니다.
if @comment.save @comment.create_activity :create, owner: current_user redirect_to @recipe, notice: "Comment was created." else render :new end
이러한 방법을 통해서 활동내역이 생성되는 시점과 방법에 대해서 좀 더 세밀하게 제어할 수 있게 되는데, 이 말은 위에서 언급했던 current user를 지정하는 해결책을 적용할 필요가 없다는 것입니다. 이러한 방법을 이용하면 또한 커스텀 활동내역을 매우 간편하게 만들 수도 있게 됩니다.
이 연재를 마무리하기 위해서, 활동내역 목록을 변경해서 모든 사용자가 아닌 친구들의 활동내역만 보이도록 할 것입니다. 이를 위해서 ActivitiesController
에 아래와 같이 scope을 추가해 줄 수 있습니다.
class ActivitiesController < ApplicationController def index @activities = PublicActivity::Activity.order("created_at desc").where(owner_id: current_user.friend_ids, owner_type: "User") end end
이제 현재 사용자의 친구들의 활동내역만이 나타날 것입니다. owner는 polymorphic으로 관계설정이 될 수 있기 때문에 owner_type
을 User
로 명시적으로 지정하는 것이 좋습니다. 이제 페이지를 다시 로드하게 되면 친구로 표시된 사용자가 없기 때문에 아무런 활동내역이 나타나지 않을 것입니다. 그러나 댓글을 단 다른 사용자를 친구로 표시한다면 해당 사용자의 활동내역을 볼 수 있게 될 것입니다.