Turbo/Hotwire 및 StimulusJS를 활용한 Rails에서의 편집 충돌 방지

Preventing edit conflicts in Rails with Turbo/Hotwire and StimulusJS | by Nicolás Galdámez | Unagi | Oct, 2025 | Medium

작성자
jeff
발행일
2025년 10월 08일

핵심 요약

  • 1 Rails Task 모델에 locked_by, locked_at 컬럼을 추가하여 레코드 잠금 상태를 관리합니다.
  • 2 Turbo Streams를 통해 실시간으로 잠금 상태를 브로드캐스트하고, StimulusJS로 사용자 이탈 시 자동으로 잠금을 해제합니다.
  • 3 navigator.sendBeacon을 활용하여 페이지 언로드 시에도 안정적인 잠금 해제 요청을 전송하여 편집 충돌을 효과적으로 방지합니다.

도입

협업 기능이 필요한 Rails 애플리케이션에서 여러 사용자가 동시에 동일한 레코드를 편집할 때 발생하는 충돌은 흔한 문제입니다. 전통적인 낙관적 잠금(optimistic locking)이나 복잡한 병합 전략 대신, "다른 사용자가 이미 편집 중입니다. 나중에 다시 시도하세요"와 같은 직관적인 접근 방식이 요구될 때가 있습니다. 본 글은 Turbo/Hotwire 및 StimulusJS를 활용하여 사용자가 작업을 편집할 때 레코드를 잠그고, 잠금 상태를 모든 연결된 클라이언트에 실시간으로 브로드캐스트하며, 사용자가 페이지를 벗어날 때 자동으로 잠금을 해제하는 간단하면서도 효과적인 솔루션을 제시합니다.

본 솔루션은 Rails의 기본 기능을 활용하여 복잡한 웹소켓 관리 없이 실시간 협업 경험을 제공합니다.

Task 모델 설정

Task 모델에 locked_by (User 참조)와 locked_at (datetime) 컬럼을 추가하여 잠금 상태를 관리합니다. ruby class AddLockingToTasks < ActiveRecord::Migration[8.0] def change add_reference :tasks, :locked_by, foreign_key: { to_table: :users } add_column :tasks, :locked_at, :datetime end end Task 모델은 locked_by?, locked?, lock!, unlock! 메서드를 포함하여 잠금 로직을 처리합니다. 특히 after_update_commit 콜백을 통해 tasks Turbo Stream 채널로 변경 사항을 브로드캐스트하여 모든 연결된 클라이언트가 실시간으로 업데이트된 태스크 목록을 받도록 합니다.

컨트롤러 로직

TasksControlleredit 액션은 태스크가 이미 잠겨 있고 현재 사용자가 잠그지 않은 경우 다른 경로로 리다이렉트합니다. 유효한 경우 current_user로 태스크를 잠급니다. update 액션은 태스크가 성공적으로 업데이트되면 잠금을 해제합니다. 또한, 명시적인 잠금 해제를 위한 unlock 액션을 추가하여 현재 사용자가 잠근 경우에만 잠금을 해제하도록 합니다.

UI 잠금 상태 표시

_task.html.erb 파셜은 태스크의 잠금 상태에 따라 다른 UI를 렌더링합니다.

  • 태스크가 잠겨 있으면 locked_by 정보를 표시하고, “Edit” 버튼을 비활성화합니다.

  • tasks/index.html.erb 뷰는 <%= turbo_stream_from "tasks" %>를 통해 tasks 채널을 구독하여, 어떤 태스크의 잠금 상태가 변경되더라도 모든 연결된 클라이언트가 즉시 업데이트를 받습니다.

사용자 이탈 시 잠금 해제

사용자가 “저장” 또는 “취소”를 클릭하지 않고 탭을 닫거나 뒤로 가기 버튼을 누르는 경우에도 잠금이 자동으로 해제되어야 합니다. 이를 위해 StimulusJS 컨트롤러가 사용됩니다.

  • task_lock_controller.jsturbo:before-visit (앱 내비게이션 시) 및 beforeunload (탭 닫기, 페이지 새로고침 시) 이벤트를 수신합니다.

  • 이벤트 발생 시, navigator.sendBeacon()을 사용하여 /tasks/:id/unlock 엔드포인트로 비동기 POST 요청을 보냅니다. sendBeacon()은 페이지 언로드 시 일반 AJAX 요청이 취소되는 것과 달리, 데이터가 안정적으로 전송되도록 설계되었습니다.

결론

이 접근 방식의 핵심은 그 단순함에 있습니다. 복잡한 웹소켓 관리, 상태 동기화 또는 폴링 없이 Turbo Streams와 StimulusJS를 통해 Rails의 컨벤션만으로 모든 것을 처리합니다. 이는 협업 기능 구현이 항상 무거운 인프라를 요구하지 않음을 보여줍니다. Rails 스택에 이미 존재하는 도구들을 신중하게 조합함으로써, 부드럽고 실시간적인 사용자 경험을 제공할 수 있습니다. 각 도구의 장점을 이해하고 이를 효과적으로 결합하는 것이 성공적인 구현의 열쇠입니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!