Rails Stimulus와 Importmaps 2026: 빌드 도구 없이 모던 JavaScript 개발하기

Rails 8에서 Stimulus와 Importmaps를 사용하여 빌드 도구 없이 모던 JavaScript 애플리케이션을 구축하는 방법을 배웁니다. Hotwire 생태계의 실용적인 튜토리얼입니다.

Rails Stimulus와 Importmaps 2026: 빌드 도구 없이 모던 JavaScript 개발하기

Rails 생태계는 오랜 시간에 걸쳐 프론트엔드 개발 접근 방식을 근본적으로 재검토해 왔습니다. 2026년 현재, Rails 8은 Stimulus와 Importmaps를 중심으로 빌드 도구가 필요 없는 모던 JavaScript 개발 경험을 제공합니다. 이 글에서는 이러한 기술들을 사용하여 효율적인 웹 애플리케이션을 구축하는 방법을 자세히 살펴봅니다.

Rails 8에서는 Importmaps가 기본 JavaScript 관리 방법으로 채택되었습니다. 새 프로젝트에서는 추가 설정 없이 바로 사용할 수 있습니다.

Importmaps란 무엇인가

Importmaps는 브라우저의 네이티브 ES 모듈 기능을 활용하여 JavaScript 의존성을 관리하는 메커니즘입니다. webpack이나 esbuild와 같은 기존 번들러를 사용하지 않고도 JavaScript 파일을 브라우저에 직접 제공할 수 있습니다.

Rails에서는 importmap-rails gem이 이 기능을 제공합니다. 다음 명령어로 새 패키지를 추가할 수 있습니다:

bash
bin/importmap pin lodash

이 명령은 config/importmap.rb 파일을 업데이트하고, CDN에서 제공되는 패키지에 대한 참조를 추가합니다:

ruby
# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "lodash", to: "https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"

Stimulus의 기본 개념

Stimulus는 HTML 중심의 JavaScript 프레임워크입니다. React나 Vue와 같은 가상 DOM 기반 프레임워크와 달리, 기존 HTML을 확장하는 형태로 상호작용성을 추가합니다.

Stimulus의 주요 개념은 다음 세 가지입니다:

  • Controllers: JavaScript 동작을 캡슐화하는 클래스
  • Actions: DOM 이벤트와 컨트롤러 메서드를 연결
  • Targets: 컨트롤러 내에서 DOM 요소를 참조

첫 번째 Stimulus 컨트롤러 생성하기

Rails 제너레이터를 사용하여 새 컨트롤러를 생성합니다:

bash
bin/rails generate stimulus toggle

이렇게 하면 app/javascript/controllers/toggle_controller.js가 생성됩니다:

app/javascript/controllers/toggle_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["content"]
  static classes = ["hidden"]
  static values = {
    open: { type: Boolean, default: false }
  }

  connect() {
    this.render()
  }

  toggle() {
    this.openValue = !this.openValue
  }

  openValueChanged() {
    this.render()
  }

  render() {
    this.contentTarget.classList.toggle(this.hiddenClass, !this.openValue)
  }
}

이 컨트롤러를 HTML에서 사용하려면 data-controller 속성을 추가합니다:

html
<div data-controller="toggle" data-toggle-hidden-class="hidden">
  <button data-action="click->toggle#toggle">
    Toggle Content
  </button>
  <div data-toggle-target="content" class="hidden">
    <p>This content can be toggled</p>
  </div>
</div>

Values와 Targets의 고급 사용법

Stimulus 3.x에서는 Values API가 크게 강화되었습니다. 타입 안전성과 기본값 지원으로 더 견고한 코드를 작성할 수 있습니다:

app/javascript/controllers/counter_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["count", "message"]
  static values = {
    count: { type: Number, default: 0 },
    max: { type: Number, default: 100 },
    step: { type: Number, default: 1 }
  }

  increment() {
    if (this.countValue < this.maxValue) {
      this.countValue += this.stepValue
    }
  }

  decrement() {
    if (this.countValue > 0) {
      this.countValue -= this.stepValue
    }
  }

  countValueChanged() {
    this.countTarget.textContent = this.countValue
    this.updateMessage()
  }

  updateMessage() {
    const percentage = (this.countValue / this.maxValue) * 100
    if (percentage >= 100) {
      this.messageTarget.textContent = "Maximum reached!"
    } else if (percentage >= 75) {
      this.messageTarget.textContent = "Almost there!"
    } else {
      this.messageTarget.textContent = ""
    }
  }
}

해당 HTML은 다음과 같습니다:

html
<div data-controller="counter" 
     data-counter-count-value="50" 
     data-counter-max-value="100"
     data-counter-step-value="5">
  <button data-action="click->counter#decrement">-</button>
  <span data-counter-target="count">50</span>
  <button data-action="click->counter#increment">+</button>
  <p data-counter-target="message"></p>
</div>

Turbo Streams와의 통합

Stimulus는 Turbo Streams와 결합하여 서버로부터의 실시간 업데이트에 대응할 수 있습니다. 다음은 알림 시스템 구현 예제입니다:

app/javascript/controllers/notifications_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["list", "badge"]
  static values = {
    unreadCount: { type: Number, default: 0 }
  }

  connect() {
    this.updateBadge()
  }

  markAsRead(event) {
    const notificationId = event.currentTarget.dataset.notificationId
    this.element.querySelector(`[data-notification-id="${notificationId}"]`)
      ?.classList.remove("unread")
    this.unreadCountValue = Math.max(0, this.unreadCountValue - 1)
  }

  markAllAsRead() {
    this.element.querySelectorAll(".unread").forEach(el => {
      el.classList.remove("unread")
    })
    this.unreadCountValue = 0
  }

  unreadCountValueChanged() {
    this.updateBadge()
  }

  updateBadge() {
    if (this.hasBadgeTarget) {
      this.badgeTarget.textContent = this.unreadCountValue
      this.badgeTarget.hidden = this.unreadCountValue === 0
    }
  }
}

서버 측 Rails 컨트롤러에서는 Turbo Stream 응답을 반환할 수 있습니다:

ruby
# app/controllers/notifications_controller.rb
class NotificationsController < ApplicationController
  def mark_as_read
    @notification = current_user.notifications.find(params[:id])
    @notification.update(read_at: Time.current)

    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to notifications_path }
    end
  end
end

폼 유효성 검사 구현

Stimulus를 사용한 실시간 폼 유효성 검사 구현 예제를 살펴봅니다:

app/javascript/controllers/form_validation_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["input", "error", "submit"]
  static values = {
    rules: Object
  }

  connect() {
    this.validateAll()
  }

  validate(event) {
    const input = event.target
    const fieldName = input.dataset.field
    const rules = this.rulesValue[fieldName] || {}
    const errors = this.validateField(input.value, rules)
    
    this.displayErrors(input, errors)
    this.updateSubmitButton()
  }

  validateField(value, rules) {
    const errors = []
    
    if (rules.required && !value.trim()) {
      errors.push("This field is required")
    }
    
    if (rules.minLength && value.length < rules.minLength) {
      errors.push(`Minimum ${rules.minLength} characters required`)
    }
    
    if (rules.pattern && !new RegExp(rules.pattern).test(value)) {
      errors.push(rules.patternMessage || "Invalid format")
    }
    
    if (rules.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      errors.push("Please enter a valid email address")
    }
    
    return errors
  }

  displayErrors(input, errors) {
    const errorContainer = this.errorTargets.find(
      el => el.dataset.for === input.dataset.field
    )
    
    if (errorContainer) {
      errorContainer.textContent = errors[0] || ""
      input.classList.toggle("border-red-500", errors.length > 0)
    }
  }

  validateAll() {
    this.inputTargets.forEach(input => {
      this.validate({ target: input })
    })
  }

  updateSubmitButton() {
    const hasErrors = this.errorTargets.some(el => el.textContent !== "")
    this.submitTarget.disabled = hasErrors
  }
}

HTML 사용 예제:

html
<form data-controller="form-validation"
      data-form-validation-rules-value='{"email":{"required":true,"email":true},"password":{"required":true,"minLength":8}}'>
  <div>
    <label for="email">Email</label>
    <input type="email" 
           id="email"
           data-form-validation-target="input"
           data-field="email"
           data-action="input->form-validation#validate">
    <span data-form-validation-target="error" data-for="email"></span>
  </div>
  
  <div>
    <label for="password">Password</label>
    <input type="password" 
           id="password"
           data-form-validation-target="input"
           data-field="password"
           data-action="input->form-validation#validate">
    <span data-form-validation-target="error" data-for="password"></span>
  </div>
  
  <button type="submit" data-form-validation-target="submit">
    Submit
  </button>
</form>

서드파티 라이브러리 통합

Importmaps를 사용하여 서드파티 라이브러리를 통합하는 방법을 살펴봅니다. 예로 Chart.js를 사용한 차트 표시를 구현합니다:

bash
bin/importmap pin chart.js
app/javascript/controllers/chart_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"
import Chart from "chart.js/auto"

export default class extends Controller {
  static targets = ["canvas"]
  static values = {
    type: { type: String, default: "bar" },
    data: Object,
    options: Object
  }

  connect() {
    this.chart = new Chart(this.canvasTarget, {
      type: this.typeValue,
      data: this.dataValue,
      options: this.optionsValue
    })
  }

  disconnect() {
    if (this.chart) {
      this.chart.destroy()
    }
  }

  dataValueChanged() {
    if (this.chart) {
      this.chart.data = this.dataValue
      this.chart.update()
    }
  }
}

Ruby on Rails 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

성능 최적화 모범 사례

Importmaps와 Stimulus를 사용할 때의 성능 최적화에 대한 몇 가지 모범 사례를 소개합니다.

지연 로딩 활용

Stimulus 3.x에서는 컨트롤러의 지연 로딩이 지원됩니다:

app/javascript/controllers/application.jsjavascript
import { Application } from "@hotwired/stimulus"
import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"

const application = Application.start()
application.debug = false
window.Stimulus = application

lazyLoadControllersFrom("controllers", application)

export { application }

캐시 헤더 설정

Importmaps로 제공되는 JavaScript 파일에 적절한 캐시 헤더를 설정하여 성능을 향상시킬 수 있습니다:

ruby
# config/environments/production.rb
config.public_file_server.headers = {
  "Cache-Control" => "public, max-age=31536000, immutable"
}

조건부 컨트롤러 로딩

특정 페이지에서만 필요한 컨트롤러는 조건부로 로드할 수 있습니다:

ruby
# config/importmap.rb
pin "controllers/admin/dashboard", preload: false
pin "controllers/admin/users", preload: false

HTML에서는 필요한 경우에만 컨트롤러를 사용합니다:

erb
<% if current_user.admin? %>
  <div data-controller="admin--dashboard">
    <!-- Admin dashboard content -->
  </div>
<% end %>

테스트 전략

Stimulus 컨트롤러 테스트는 시스템 테스트와 JavaScript 단위 테스트 모두에서 수행할 수 있습니다:

ruby
# test/system/toggle_test.rb
require "application_system_test_case"

class ToggleTest < ApplicationSystemTestCase
  test "toggles content visibility" do
    visit page_with_toggle_path
    
    assert_no_selector ".toggle-content", visible: true
    
    click_button "Toggle Content"
    
    assert_selector ".toggle-content", visible: true
    
    click_button "Toggle Content"
    
    assert_no_selector ".toggle-content", visible: true
  end
end

결론

Rails 8에서 Stimulus와 Importmaps의 조합은 빌드 도구의 복잡성을 제거하면서 모던 JavaScript 개발 경험을 제공합니다. HTML 중심 접근 방식은 서버 사이드 렌더링의 이점을 유지하면서 필요한 곳에 상호작용성을 추가할 수 있게 합니다.

기존의 SPA 프레임워크와 비교하여, Stimulus는 학습 곡선이 완만하며 기존 Rails 애플리케이션에 점진적으로 도입할 수 있습니다. Importmaps를 통해 npm이나 yarn과 같은 패키지 매니저 없이도 외부 라이브러리를 사용할 수 있어 개발 환경 설정이 간소화됩니다.

이러한 기술들을 결합하여 유지보수성이 높고 성능이 우수한 웹 애플리케이션을 구축할 수 있습니다.

오늘의 챌린지

Ruby on Rails 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 8월 29일 업데이트

공유

관련 기사