# Ruby on Rails 7: 리액티브 애플리케이션을 위한 Hotwire와 Turbo > Rails 7에서 Hotwire와 Turbo 완벽 가이드. Turbo Drive, Frames, Streams로 JavaScript 없이 리액티브 애플리케이션을 구축하는 방법. - Published: 2026-01-11 - Updated: 2026-04-07 - Author: SharpSkill - Tags: ruby on rails, hotwire, turbo, turbo frames, turbo streams - Reading time: 12 min --- Rails 7은 Hotwire를 기본으로 통합하여 웹 개발에 혁명을 가져왔습니다. 이 스택은 커스텀 JavaScript를 한 줄도 작성하지 않고도 고도로 리액티브한 애플리케이션을 구축할 수 있게 해줍니다. Turbo Drive, Turbo Frames, Turbo Streams는 기존 SPA 접근 방식을 "HTML over the wire" 철학으로 대체합니다. > **왜 Hotwire인가?** > > Hotwire는 프론트엔드 복잡성을 획기적으로 줄여줍니다. 동적 인터페이스에 React나 Vue가 필요 없습니다. 서버가 바로 사용 가능한 HTML을 전송하고, Turbo가 DOM 업데이트를 자동으로 처리합니다. ## Hotwire 아키텍처 이해하기 Hotwire는 JavaScript 프레임워크의 전형적인 복잡성 없이 매끄러운 사용자 경험을 제공하기 위해 함께 작동하는 세 가지 보완적인 기술로 구성됩니다. **Turbo Drive**는 링크 클릭과 폼 제출을 가로채서 내비게이션을 가속화합니다. 전체 페이지를 새로고침하는 대신 `` 콘텐츠만 교체되며, JavaScript와 CSS 컨텍스트가 유지됩니다. **Turbo Frames**는 페이지를 독립적인 섹션으로 분해합니다. 각 프레임은 개별적으로 업데이트될 수 있어 페이지의 나머지 부분에 영향을 주지 않고 타겟 인터랙션이 가능합니다. **Turbo Streams**는 WebSocket을 통해 또는 HTTP 요청에 대한 응답으로 실시간 업데이트를 가능하게 합니다. 선언적 DOM 조작을 위한 8가지 CRUD 액션을 사용할 수 있습니다. ```ruby # Gemfile # Installing Turbo Rails (included by default in Rails 7+) gem 'turbo-rails' ``` 기존 프로젝트에서는 설치에 단 하나의 명령어만 필요합니다. ```bash # installation.sh # Installing Turbo in an existing Rails project rails turbo:install # Verify the JavaScript import is present cat app/javascript/application.js # Should contain: import "@hotwired/turbo-rails" ``` ## Turbo Drive 초기 설정 Turbo Drive는 Rails 7에서 기본적으로 활성화되어 있습니다. 모든 내비게이션이 추가 설정 없이 자동으로 "turbo"가 됩니다. 동작은 data 속성을 통해 커스터마이즈할 수 있습니다. ```erb <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %>
<%= yield %> ``` 필요한 경우 특정 링크나 폼에서 Turbo Drive를 비활성화할 수 있습니다. ```erb <%= link_to "Dashboard", dashboard_path %> <%= link_to "Download PDF", export_path, data: { turbo: false } %> <%= form_with model: @document, data: { turbo: false } do |f| %> <%= f.file_field :attachment %> <%= f.submit "Upload" %> <% end %> <%= link_to "Delete", item_path(@item), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %> ``` > **Turbo Drive 캐시** > > Turbo Drive는 방문한 페이지를 캐시합니다. 에셋의 `data-turbo-track="reload"` 속성은 CSS/JS 파일이 변경될 때 전체 새로고침을 강제합니다. ## 타겟 업데이트를 위한 Turbo Frames Turbo Frames는 독립적으로 업데이트되는 페이지 영역을 정의합니다. 각 프레임은 고유한 식별자를 가지며 일치하는 프레임을 포함하는 응답에만 반응합니다. ```erb

Messages

<%= render @messages %>
<%= link_to "Load more", messages_path(page: @next_page) %>
<%= render "form", message: Message.new %> ``` 업데이트가 작동하려면 서버 응답에 같은 식별자를 가진 프레임이 포함되어야 합니다. ```erb

<%= message.content %>

<%= link_to "Edit", edit_message_path(message) %> <%= button_to "Delete", message_path(message), method: :delete %>
``` ### Turbo Frames를 이용한 지연 로딩 프레임은 `src` 속성을 사용하여 비동기적으로 콘텐츠를 로드할 수 있습니다. 로딩 중에는 초기 콘텐츠가 표시됩니다. ```erb

Dashboard

Loading statistics...

Loading notifications...

<%= render "activity/placeholder" %> ``` 컨트롤러는 요청된 프레임만 포함하는 뷰로 응답합니다. ```ruby # app/controllers/dashboard_controller.rb class DashboardController < ApplicationController def stats @user_count = User.count @message_count = Message.count @active_today = User.where("last_seen_at > ?", 24.hours.ago).count # Partial rendering for the frame render partial: "dashboard/stats" end end ``` ```erb

<%= @user_count %>

Users

<%= @message_count %>

Messages

<%= @active_today %>

Active today

``` ## 실시간 업데이트를 위한 Turbo Streams Turbo Streams는 DOM 조작을 위한 8가지 액션을 제공합니다: `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`, `morph`. 이러한 액션은 HTTP 응답이나 WebSocket을 통해 트리거될 수 있습니다. ```ruby # app/controllers/messages_controller.rb class MessagesController < ApplicationController def create @message = current_user.messages.build(message_params) respond_to do |format| if @message.save # Turbo Stream response to add the message format.turbo_stream do render turbo_stream: [ turbo_stream.prepend("messages", @message), turbo_stream.update("message_count", partial: "messages/count"), turbo_stream.replace("new_message", partial: "messages/form", locals: { message: Message.new }) ] end format.html { redirect_to messages_path, notice: "Message created" } else format.turbo_stream do render turbo_stream: turbo_stream.replace( "new_message", partial: "messages/form", locals: { message: @message } ) end format.html { render :new, status: :unprocessable_entity } end end end def destroy @message = Message.find(params[:id]) @message.destroy respond_to do |format| # Removal with animation format.turbo_stream { render turbo_stream: turbo_stream.remove(@message) } format.html { redirect_to messages_path, notice: "Message deleted" } end end end ``` ### 전용 Turbo Stream 템플릿 더 복잡한 응답의 경우 `.turbo_stream.erb` 템플릿이 더 큰 유연성을 제공합니다. ```erb <%= turbo_stream.prepend "messages" do %> <%= render @message %> <% end %> <%= turbo_stream.update "message_count" do %> <%= Message.count %> messages <% end %> <%= turbo_stream.replace "new_message" do %> <%= render "form", message: Message.new %> <% end %> <%= turbo_stream.prepend "flash_messages" do %>
Message sent successfully!
<% end %> ``` > **응답 형식** > > Turbo Stream 응답은 content-type `text/vnd.turbo-stream.html`을 가져야 합니다. Rails는 `respond_to`와 `turbo_stream` 형식으로 이를 자동 처리합니다. ## Action Cable을 이용한 실시간 브로드캐스팅 Turbo Streams는 WebSocket 브로드캐스팅에서 진정한 위력을 발휘합니다. 사용자들은 폴링 없이 즉시 업데이트를 받습니다. ```ruby # app/models/message.rb class Message < ApplicationRecord belongs_to :room belongs_to :author, class_name: "User" # Automatic broadcast after creation after_create_commit do broadcast_append_to( room, target: "messages", partial: "messages/message", locals: { message: self } ) end # Broadcast after update after_update_commit do broadcast_replace_to( room, target: dom_id(self), partial: "messages/message", locals: { message: self } ) end # Broadcast after deletion after_destroy_commit do broadcast_remove_to(room, target: dom_id(self)) end end ``` 뷰는 `turbo_stream_from` 헬퍼로 해당 스트림을 구독합니다. ```erb

<%= @room.name %>

<%= turbo_stream_from @room %>
<%= render @room.messages.order(created_at: :asc) %>
<%= form_with model: [@room, Message.new], id: "new_message" do |f| %> <%= f.text_area :content, placeholder: "Your message..." %> <%= f.submit "Send" %> <% end %> ``` ### 무거운 작업을 위한 비동기 브로드캐스팅 요청을 차단하지 않기 위해 브로드캐스팅은 백그라운드에서 수행할 수 있습니다. ```ruby # app/models/report.rb class Report < ApplicationRecord belongs_to :user after_create_commit :generate_async private def generate_async GenerateReportJob.perform_later(self) end end ``` ```ruby # app/jobs/generate_report_job.rb class GenerateReportJob < ApplicationJob queue_as :default def perform(report) # Simulating a long operation report.update!(status: "processing") # Notify user of start report.broadcast_replace_to( report.user, :reports, target: dom_id(report), partial: "reports/report" ) # Generate the report result = ReportGenerator.new(report).generate report.update!(content: result, status: "completed") # Notify user of completion report.broadcast_replace_to( report.user, :reports, target: dom_id(report), partial: "reports/report" ) end end ``` ## Turbo 고급 패턴 ### 인라인 폼 편집 일반적인 패턴은 정적 콘텐츠를 인라인 편집 폼으로 대체하는 것입니다. ```erb
<%= task.content %>
<%= link_to "Edit", edit_task_path(task) %> <%= button_to "Delete", task_path(task), method: :delete, data: { turbo_confirm: "Delete this task?" } %>
``` ```erb <%= form_with model: @task do |f| %> <%= f.text_field :content, autofocus: true %>
<%= f.submit "Save" %> <%= link_to "Cancel", task_path(@task) %>
<% end %>
``` ### 부모 프레임 외부로의 내비게이션 기본적으로 프레임 내의 링크는 해당 프레임 안에 머무릅니다. `data-turbo-frame` 속성을 사용하면 다른 프레임이나 전체 페이지를 타겟으로 지정할 수 있습니다. ```erb ``` ```erb
<%= yield %>
``` > **Turbo 디버깅** > > 개발 환경에서 JavaScript 콘솔을 열고 `Turbo.setProgressBarDelay(0)`를 입력하면 프로그레스 바를 즉시 볼 수 있습니다. `Turbo.session.drive = false`로 Turbo Drive를 임시로 비활성화할 수 있습니다. ## 에러 처리와 로딩 상태 좋은 UX를 위해서는 로딩 상태와 네트워크 오류 처리가 필요합니다. ```erb <%= form_with model: @model, data: { controller: "form-loading", action: "turbo:submit-start->form-loading#disable turbo:submit-end->form-loading#enable" } do |f| %> <%= yield f %> <% end %> ``` ```javascript // app/javascript/controllers/form_loading_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["submit", "text", "spinner"] disable() { this.submitTarget.disabled = true this.textTarget.classList.add("hidden") this.spinnerTarget.classList.remove("hidden") } enable() { this.submitTarget.disabled = false this.textTarget.classList.remove("hidden") this.spinnerTarget.classList.add("hidden") } } ``` ### 서버 오류 처리 ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base rescue_from ActiveRecord::RecordNotFound do |exception| respond_to do |format| format.turbo_stream do render turbo_stream: turbo_stream.replace( "main_content", partial: "shared/not_found" ) end format.html { render "errors/not_found", status: :not_found } end end end ``` ## 성능 최적화 여러 기법으로 성능 좋은 Turbo 애플리케이션을 보장할 수 있습니다. ```ruby # app/controllers/messages_controller.rb class MessagesController < ApplicationController # Avoid N+1 queries with eager loading def index @messages = Message.includes(:author, :room) .order(created_at: :desc) .page(params[:page]) end # Fragment caching for static elements def show @message = Message.find(params[:id]) fresh_when @message end end ``` ```erb <% cache message do %>
<%= message.content %> By <%= message.author.name %>
<% end %> ``` ## 결론 Hotwire와 Turbo는 기존 JavaScript 프레임워크의 복잡성을 제거하여 Rails 개발을 혁신합니다. Turbo Drive는 내비게이션을 가속화하고, Turbo Frames는 타겟 업데이트를 가능하게 하며, Turbo Streams는 강력한 실시간 기능을 제공합니다. ### Hotwire 시작 체크리스트 - 내장된 Hotwire 통합을 위해 Rails 7 이상 사용하기 - 세 가지 컴포넌트 이해하기: Drive, Frames, Streams - Turbo Frames가 유용한 페이지 영역 식별하기 - 컨트롤러에서 `respond_to`와 `format.turbo_stream` 사용하기 - 실시간 기능을 위한 브로드캐스팅 구현하기 - 간단한 JavaScript 인터랙션을 위해 Stimulus와 결합하기 이 "HTML over the wire" 접근 방식은 Ruby on Rails를 강력하게 만드는 단순성과 생산성을 유지하면서 모던하고 리액티브한 애플리케이션을 구축할 수 있게 해줍니다. 결과적으로 유지보수할 코드가 줄어들고, 뛰어난 성능과 최적의 개발자 경험을 얻을 수 있습니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/ruby-on-rails/ruby-on-rails-7-hotwire-turbo