Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Rails에서 Hotwire 활용하기: JavaScript 없이 더 빠른 웹 앱 만들기

페이지 전환과 폼 제출 속도를 높이고, 단 한 줄의 JavaScript 코드도 작성하지 않으면서 복잡한 페이지를 컴포넌트 단위로 분리하고 싶으신가요? 이 글은 Hotwire를 활용해 Rails 개발을 한 단계 끌어올리는 방법과 서버 사이드 렌더링 도구를 사용하는 법을 알려드립니다.

Hotwire란 무엇인가?

Hotwire는 JSON 대신 HTML을 전송하는 방식으로 JavaScript를 직접 작성하지 않고도 현대적인 웹 애플리케이션을 구축할 수 있게 해주는 프레임워크입니다. 덕분에 페이지 로딩 속도가 눈에 띄게 빨라지죠. 렌더링은 서버 측에서 유지되며, Rails가 지향해 온 간결하고 생산적인 개발 경험을 그대로 살리면서도 기존 SPA(싱글 페이지 애플리케이션)의 빠른 속도와 반응성까지 놓치지 않습니다.

Hotwire의 핵심은 Turbo 젬입니다. Turbo는 페이지 탐색과 폼 제출 속도를 높이고, 복잡한 페이지를 독립적인 컴포넌트로 나누며, WebSocket(ActionCable, 채널, 스트리밍 데이터로 구성)을 통해 페이지의 부분 업데이트를 전송하는 상호 보완적인 기술들의 집합입니다.

왜 Hotwire를 사용해야 할까?

JavaScript가 어렵거나 부담스럽다면, 페이지 사이를 훨씬 빠르게 오가는 듯한 사용자 경험을 손쉽게 얻을 수 있는 Hotwire가 좋은 선택지입니다.

그렇다면 Hotwire는 어떻게 동작할까요? Hotwire는 서버 사이드 렌더링(SSR)을 활용해 SPA의 고질적인 문제들을 해결하면서도 SPA가 가진 장점은 그대로 유지합니다. SSR은 기존 방식과 반대로 렌더링 과정 중 일부를 클라이언트가 아닌 서버에서 처리합니다. 렌더링 작업 일부를 서버가 담당하기 때문에 사용자에게 더 효율적인 애플리케이션 로딩 경험을 제공할 수 있습니다. 성능 개선뿐만 아니라 검색 엔진 색인(indexing) 같은 SEO 문제를 다루는 데도 큰 도움이 됩니다.

사용이 어렵지 않나?

전혀 어렵지 않습니다. 필요한 것은 Rails 프로젝트의 기본 구성 요소(Ruby, RoR, ActionCable, WebSocket), JS 의존성을 모두 내려받아 주는 Turbo 젬, 그리고 WebSocket 통신 중 임시 데이터를 저장할 Redis 정도입니다.

SPA 수준의 빠른 속도를 위해 새로운 언어(JS)를 배울 필요가 없다는 점이 가장 큰 매력입니다. Turbo는 Turbo Drive, Frames, Streams, Native 네 가지 구성 요소로 완성됩니다. Drive는 링크와 폼 처리 속도를 높여 불필요한 네트워크 재요청을 줄여주고, Frames는 페이지를 로딩하기 쉬운 독립된 컨텍스트로 분리해 줍니다.

Rails에서 어떻게 사용하나?

이제 실제 예제를 통해 단계별로 알아보겠습니다. 이 튜토리얼에는 아래 항목들이 필요합니다.

  • Ruby
  • Ruby on Rails
  • Redis
  • SQLite (기본 데이터베이스)
  • Hotwire Gem
  • TurboRails
  • StimulusJS
  • WebSocket
  • ActionCable

어떤 프로젝트를 만들까?

간단한 소셜 미디어 프로젝트를 만들어 보겠습니다.

먼저 터미널을 열고 새 Rails 프로젝트를 생성합니다:

rails new social-media

프로젝트 디렉터리로 이동합니다:

cd social-media

Hotwire 추가하기

프로젝트에 Hotwire 젬을 추가합니다:

bundle add hotwire-rails

또는 Gemfile을 열어 직접 아래 코드를 추가해도 됩니다:

gem "hotwire-rails"

두 번째 방법을 사용했다면 bundle 명령을 실행해야 합니다:

bundle install

그리고 Hotwire를 설치합니다:

rails hotwire:install

초기 설정 확인하기

Hotwire 설치 후 생성된 초기 설정을 살펴볼 좋은时机입니다. Gemfile을 열면 아래와 같은 내용이 추가된 것을 확인할 수 있습니다:

gem 'redis', '~> 4.0'

왜 Redis가 필요한가?

Redis 젬이 추가된 이유는 ActionCable이 WebSocket 통신 중 임시 데이터를 저장하기 위해 Redis를 필요로 하기 때문입니다. 하지만 설치만으로는 부족하고, 올바르게 설정되었는지 확인해야 합니다. config/cable.yml 파일을 열었을 때 아래와 같이 되어 있는지 확인하세요:

development:
 adapter: redis
 redis://localhost:6379/1

애플리케이션 실행 시 Redis 서버(redis-server)가 구동 중인지 반드시 확인하세요.

JS 의존성 확인하기

package.json의 의존성 목록을 확인합니다:

dependencies: {
 @hotwired/turbo-rails: ^7.0.0-beta.5,
 @rails/actioncable: ^6.0.0,
 @rails/activestorage: ^6.0.0,
 @rails/ujs: ^6.0.0,
 @rails/webpacker: 4.3.0,
 stimulus: ^2.0.0
}

모델 생성하기

파일들을 모두 확인했다면, 이제 bodylikes 컬럼을 가진 posts 테이블에 대한 views, controllers, models, migrations를 생성합니다. 터미널에서 아래 명령을 실행하세요:

rails g scaffold posts body:text likes:integer

데이터베이스 생성하기

모든 파일이 생성되었다면 변경 사항을 데이터베이스에 반영해야 합니다:

rails db:create db:migrate

문제없이 완료되었다면 서버(rails server)를 실행해 정상 동작 여부를 확인합니다. 서버를 띄운 뒤 https://localhost:3000/posts 페이지로 접속해 보세요:

Rails에서 Hotwire 활용하기: JavaScript 없이 더 빠른 웹 앱 만들기 posts 페이지 스크린샷

게시물 목록 만들기

이제 게시물을 화면에 표시해 보겠습니다. app/views/posts/_post.html.erb 파일을 생성하고 아래 코드를 입력합니다:

<div style="background: lightgrey; width: 300px; padding: 10px;">
  <%= post.body %>
  <br>
  <%= link_to :edit, edit_post_path(post) %>
  <%= button_to "likes (#{post.likes || 0})", post_path(post, like: true), method: :put %>
</div>
<br>

유효성 검증 추가하기

body 필드(빈 값 허용 안 함)에 대한 유효성 검증이 필요합니다. 또한 게시물이 생성되면 첫 게시물 바로 위에 새 게시물이 표시되도록 broadcast를 설정하겠습니다. app/models/post.rb 파일을 수정해 아래 코드를 넣습니다:

class Post < ApplicationRecord
  validates_presence_of :body

  after_create_commit { broadcast_prepend_to :posts }
end

정렬 처리하기

게시물을 최신순으로 정렬해야 합니다. 컨트롤러(app/controllers/posts_controller.rb)를 열어 수정합니다:

...
def index
  @posts = Post.all.order(created_at: :desc)
  @post = Post.new
end
...

마무리 구성하기

이제 index 뷰(app/views/posts/index.html.erb)를 수정해 새 게시물 작성 폼과 전체 게시물 목록을 함께 표시하겠습니다:

<%= turbo_stream_from :posts %>

<%= turbo_frame_tag :post_form do %>
  <%= render 'posts/form', post: @post %>
<% end %>

<%= turbo_frame_tag :posts do %>
  <%= render @posts %>
<% end %>

인덱스로 리다이렉트하기

마지막으로 컨트롤러의 create 메서드를 수정합니다. 게시물 생성 후 show 페이지로 이동하지 않고 한 페이지에서 모든 작업이 이루어지도록 하는 것이죠:

...
def create
  @post = Post.new(post_params)

  respond_to do |format|
  if @post.save
   format.html { redirect_to posts_path }
   format.json { render :show, status: :created, location: @post }
 else
    format.turbo_stream { render turbo_stream: turbo_stream.replace(@post, partial: 'posts/form', locals: { post: @post }) }
    format.html { render :new, status: :unprocessable_entity }
   format.json { render json: @post.errors, status: :unprocessable_entity }
 end
  end
end
...

완성된 페이지는 아래와 같습니다:

Rails에서 Hotwire 활용하기: JavaScript 없이 더 빠른 웹 앱 만들기 최종 페이지 스크린샷

로그 확인하기

게시물이 생성될 때 터미널 로그를 확인해 보세요. 아래와 비슷한 출력이 보일 겁니다:

[ActionCable] Broadcasting to posts: "<turbo-stream action=\"prepend\" target=\"posts\"><template><div style=\"background: lightgrey; width: 300px; padding: 10px;\">\n  first post\n  <br>\n  <a href=\"/posts/1/edit\">edit</a>\n  <form class=\"button_to\" method=\"post\" action=\"/posts/4?like=true\"><input type=\"hidden\" name=\"_method\" value=\"put\" /><input type=\"submit\" value=\"likes (0)\" /><input type=\"hidden\" name=\"authenticity_token\" value=\"<token>==\" /></form>\n</div>\n<br>\n</template></turbo-stream>"

정리하며

이 로그는 ActionCable이 turbo stream과 함께 매우 빠른 성능으로 동작하고 있다는 증거입니다. 응답 시간을 보면 알 수 있죠: Completed 302 Found in 18ms.

배포 관련 FAQ

Rails 서버 외에 별도 실행 파일이 필요한가?

아니요. ActionCable은 Unicorn, Puma, Passenger 같은 주류 웹 서버와 문제없이 함께 동작합니다.

Heroku는 Hotwire를 지원하는가?

네, Heroku는 Hotwire의 기반 기술인 WebSocket을 지원합니다. 추가 궁금한 점이 있다면 Heroku 공식 문서를 참고하세요.

Redis를 사용하는 또 다른 이유

Heroku에서 Redis 없이 앱을 운영하면, 앱이 하나의 dyno를 넘어 확장되었을 때 모든 사용자에게 메시지가 전달되지 않습니다. 현재 앱은 상태 비저장(stateless) 방식이기 때문에, 첫 번째 dyno에 연결된 클라이언트는 두 번째 dyno에 연결된 클라이언트가 보낸 메시지를 받을 수 없습니다. Redis는 메시지 상태를 전역 저장소에 보관함으로써 이 문제를 해결합니다.

Heroku에서 Redis 설정하기

원격 Redis 인스턴스와 CLI 세션을 연결하려면 heroku redis:cli 명령을 사용합니다. 인스턴스를 지정하지 않으면 REDIS_URL에 위치한 인스턴스가 기본으로 사용되며, 인스턴스가 여러 개라면 연결할 인스턴스를 직접 지정해야 합니다.

애플리케이션 서버에서는 어떻게 동작하는가?

각 dyno가 Redis pub-sub 시스템을 사용하도록 설정해야 합니다. 모든 dyno는 동일한 채널(기본 채널)을 구독하고 메시지를 기다립니다. 각 서버가 메시지를 받으면 연결된 클라이언트들에게 해당 메시지를 발행(publish)합니다. subscribe는 블로킹(blocking) 함수이므로 메시지 대기 중에는 실행 흐름이 멈추게 되는데, 따라서 별도의 스레드에서 subscribe를 처리해야 합니다. 또한 subscribe 명령이 실행된 커넥션은 unsubscribe하거나 메시지를 수신하는 용도로만 사용할 수 있기 때문에, 두 번째 Redis 커넥션이 추가로 필요합니다. 자세한 내용은 확장(scaling) 관련 문서를 참고하세요.

보안

현재 상태의 애플리케이션은 다양한 공격에 노출되어 취약합니다. WSS를 설정하고 입력값을 반드시 검증·정제(sanitize)하세요. WebSocket 보안에 대해 더 알아보려면 관련 문서를 참고하시길 권장합니다.

결론

이번 글에서 살펴본 것처럼 새 애플리케이션에서 Hotwire를 사용할 수 있으며, 기존 Turbolinks 프로젝트에서도 업그레이드를 통해 적용 가능합니다.

이 글이 "마법의 프레임워크"라고 불리는 Hotwire의 동작 원리를 이해하는 데 도움이 되었기를 바랍니다. 직접 실습하면서 turbo stream을 통한 실시간 데이터 전송과 WebSocket 기반 조작 및 탐색을 갖춘 작고 간단한 애플리케이션을 개발해 보셨기를 바랍니다.