Rails7 ransack,hotwire(turbo_frame,stimulus)을 이용해 검색기능 구현하기

Posted by negabaro kim on Wednesday, May 24, 2023 Tags: rails/hotwire   6 minute read

ransack

ransack은 검색기능을 간단히 구현가능한 gem이다.

이 포스트에서는 ransack의 간단한 설정방법과 아래 4단계로 검색기능을 구현하는 방법을 정리해봤다.

1. ransack만 사용해서 서버사이드 랜더링 검색
2. hotwire의 turbo_frame을 이용해 페이지 리로드없이 검색결과 구현
3. hotwire의 stimulus을 이용해 검색버튼 클릭하지않고 form input시 검색되게끔 구현
4. 쿼리부하를 줄이기 위해 Debounce

How to install

issue라는 모델에title, content컬럼이 있다는 가정하에 도입순서를 정리해봤다.

Gemfile추가

gem 'ransack'
$ bundle install

controller설정

 def index
    @q = Issue.ransack(params[:q])
    @issues = @q.result.order(id: :desc).page(params[:page]).per(3)
  end

params[:q]부분이 검색 키워드이다.

ransack class의 경우 result로 리스트 확인이가능 result이후는 기존의 레일즈 설정과 동일하다.

model설정

class Issue < ApplicationRecord
  def self.ransackable_attributes(auth_object = nil)
    ["title", "content"]
  end
end

이 설정이 없으면 ransackで"Ransack needs attributes explicitly allowlisted..에러 발생

view설정

<%= search_form_for @q, url: issues_path do |f| %>
    <div class="input-group">
      <%= f.search_field :title_cont, class: "input input-bordered w-full", placeholder: "Search…" %>
      <button class="btn btn-square">
        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
      </button>
    </div>
<% end %>
<%= render 'issues' %> <<// 부분은 검색결과 부분

여기서 포인트는 title_cont부분인데 검색 대상컬럼 뒤에 _cont를 붙이는게 규칙이다.

이 예제에서는 title의 내용을 검색하므로 title_cont로 설정해줬다.

1단계 구현부분 정리

여기까지만 구현하면 ransack만 사용해서 서버사이드 랜더링 검색의 완성이다. 검색 키워드를 입력후 검색버튼을 클릭하면 정상동작할것이다.

2단계 구현부분정리

다음단계로 검색시 페이지 리로드가 없게끔 해보자.

hotwire의 turbo_frame을 사용하면 간단히 구현이 가능하다. 검색결과 부분을 turbo_frame으로 묶어주기만 하면된다.

  <%= turbo_frame_tag 'issues' do %>
    <%= render 'issues' %>
  <% end %>

여기까지 설정이 끝나면 아래화면과 같이 페이지 리로드없이 검색이 가능해진다.

Image from Gyazo

혹시 turbo_frame추가후 검색동작이 안될경우 html: { data: { turbo_frame: "issues" } }설정을 추가해보자(memo2를 참고)

3단계 구현부분정리

3단계로 검색버튼을 클릭하지 않고 검색폼에 키워드 입력시 자동으로 검색결과가 표시되게끔 구현해보자.

항상 javascript,jquery,vue,react로 input의 이벤트를 취득해서 해당form의 값을 api로 보내서 검색결과 돔을 갱신하는 일련의 작업이 필요한데 hotwire의 stimulus기능을 이용하면 최소한의 js로 구현이 가능하다

rails g stimulus search-form

커맨드로 필요한js파일을 만들어주자. app/javascript/controllers/search_form_controller.js 파일을 아래와 같이 변경해주자

import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
  submit() {
    console.log("submit");
    this.element.requestSubmit(); //requestSubmit을 쓰는 이유는 memo4,5를 참고
  }
}

app/javascript/controllers/index.js에 위에서 생성한 search_form_controller를 추가해주자.

import SearchFormController from "./search_form_controller.js";
application.register("searchForm", SearchFormController);

마지막으로 search_form view를 아래와 같이 바꿔주자

<%= search_form_for @q, url: issues_path,
  html:{
    data: {
      turbo_frame: "issues",
      controller: "searchForm",
      action: "input->searchForm#submit"
    } } do |f| %>

<div class="input-group">
    <%= f.search_field :title_cont, class: "input input-bordered w-full", placeholder: "Search…" %>
</div>

<% end %>

검색아이콘은 필요없어져서 지우고 search_form_for하위에 turbo_frame,controller,action을 추가해준게 끝이다.

controller: "searchForm" -> 위에서 선언해준 js가 attach됨

action: "input->searchForm#submit" -> input이벤트시 searchForm의 submit함수를 실행하라는 의미 c(ffeescript냄새가 나는 루비스러운 문법이다.)

위 설정만으로 3단계 설정은 끝이다.

아래와 같이 동작함.

Image from Gyazo

4단계 구현부분정리

4단계로 키워드 입력시 쿼리가 발생하므로 간단히 Debounce시켜주자

import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
  submit() {
    clearTimeout(this.timeout);

    this.timeout = setTimeout(() => {
      this.element.requestSubmit();
    }, 200);
  }
}

포스트는 여기까지다. 읽어주셔서 감사하무니다.

memo

1.

위에서 설명한 self.ransackable_attributes설정이 없으면 ransackで"Ransack needs attributes explicitly allowlisted..에러 발생

2.

view설정시 주의할 부분은 다른 turbo_frame하위에 검색폼이 있을 경우

html: { data: { turbo_frame: "issues" } }설정을 추가해줘야 정상적으로 검색이 가능해짐

<%= search_form_for @q, url: issues_path, html: { data: { turbo_frame: "issues" } } do |f| %>

필자의 경우 new버튼을 누르면 검색폼이 사라지게끔 구현하고 싶었기 때문에 검색폼 위에 다른 turbo_frame_tag가 필요했음.

<%= turbo_frame_tag "new_issue" do %>
  <%= render 'search_form' %>
<% end %>

3.

importmap-rails사용시 Stimulus컨트롤러가 자동으로 등록되므로 index.js에 추가해주는 부분도 필요없어진다고

4.

submit이 아니고 requestSubmit을 쓴 이유 submit의 경우 직접 폼에 리퀘스트를 해버려서 turbo리퀘스트의 인터셉트가 동작하지않음.

5.

safari에서 requestSubmit은 동작하지 않지만 Turbo polyfill이 들어가 있어서 문제없이 동작함.

6.

Debounce란? -> 반복되는 처리를 1단계로 묶어서 처리하는방식

7.

선언한 stimulus와 DOM이 attach한 상태인지 확인하는법

data-controller에 stimulus에서 선언한 컨트롤러명이 들어가 있어야함

ex)

images라는 컨트롤러명일 경우

<input multiple="multiple" data-controller="images" ... />

stimulus의 connect()가 호출되면 attach된 상태이다.

8.

Missing target element “preview” for “images” controller

references