본문으로 건너뛰기

Turbo와 ActionCable의 충돌: 실시간 Rails 기능에서 발생하는 상태 불일치 문제 해결

The Turbo + ActionCable Trap: When Your Real-Time Rails Feature Fights Itself · edruder.com

작성자
발행일
2026년 01월 31일

핵심 요약

  • 1 Turbo의 폼 제출 생명주기와 ActionCable의 실시간 브로드캐스트가 동시에 발생할 때, 네트워크 지연으로 인해 클라이언트 간의 데이터 상태가 불일치하는 레이스 컨디션이 발생할 수 있습니다.
  • 2 Turbo의 리다이렉트 처리는 Stimulus 컨트롤러의 연결을 해제하고 재연결하는 과정에서 브로드캐스트 메시지를 유실시킬 수 있는 짧은 공백기를 생성하여 실시간 업데이트의 신뢰성을 떨어뜨립니다.
  • 3 이 문제를 해결하기 위해서는 해당 폼에 data-turbo="false"를 설정하여 Turbo의 개입을 차단하고, 서버에서 head :no_content를 반환하여 ActionCable을 통한 단일 업데이트 경로를 확보해야 합니다.

도입

Rails 8 환경에서 Turbo와 ActionCable을 결합하여 실시간 기능을 구현할 때, 개발 환경에서는 발견되지 않던 상태 불일치 버그가 운영 환경의 네트워크 지연 상황에서 빈번하게 발생할 수 있습니다. 특히 사용자가 버튼을 빠르게 여러 번 클릭하는 경우, Turbo의 폼 제출 처리 방식과 ActionCable의 브로드캐스트가 서로 간섭하며 특정 클라이언트의 화면이 최신 상태를 반영하지 못하는 현상이 나타납니다. 본 글은 포커 게임 타이머 구현 사례를 통해 이러한 'Turbo + ActionCable 함정'의 기술적 원인을 분석하고, 실무에서 적용 가능한 구조적인 해결책을 제시합니다.

1. 문제의 발단: 운영 환경에서의 상태 불일치

Rails 8 기반의 포커 타이머 앱에서 사용자가 ‘5분 차감’ 버튼을 빠르게 여러 번 클릭할 때, 기기마다 표시되는 시간이 달라지는 문제가 발생했습니다. 서버의 데이터베이스는 정확한 상태를 유지하고 있었지만, 특정 클라이언트는 마지막 업데이트를 반영하지 못한 채 이전 상태에 머물러 있었습니다. 이 버그는 페이지를 새로고침하면 즉시 해결되었기에 원인 파악이 매우 까다로웠습니다.

2. 초기 아키텍처와 잠재적 위험 요소

해당 기능은 전형적인 Rails 패턴을 따르고 있었습니다: - 사용자가 버튼 클릭 시 Turbo가 폼을 제출합니다. - 서버는 데이터를 업데이트한 후 ActionCable을 통해 모든 클라이언트에 새 상태를 브로드캐스트합니다. - 서버는 제출한 클라이언트에게 리다이렉트 응답을 보냅니다. - Turbo는 리다이렉트를 따라 페이지를 다시 렌더링합니다.

이 과정에서 클릭한 기기는 ActionCable 브로드캐스트Turbo 리다이렉트 재렌더링이라는 두 가지 경로로 데이터를 받게 됩니다. 로컬 환경에서는 지연 시간이 거의 없어 문제가 없었으나, 실제 네트워크 환경에서는 이 두 경로가 서로 충돌하기 시작했습니다.

3. 실패한 첫 번째 시도: 리다이렉트 제거

작성자는 리다이렉트 과정에서 Stimulus 컨트롤러가 연결 해제(disconnect)되고 재연결(connect)되는 찰나의 순간에 ActionCable 구독이 끊기며 브로드캐스트를 놓친다고 판단했습니다. 이에 따라 리다이렉트 대신 head :no_content를 반환하도록 수정했습니다. 하지만 이 수정 후에도 빠른 클릭 시 여전히 상태 불일치가 발생했습니다.

4. 근본 원인: Turbo Navigator의 중단(Abort) 메커니즘

심층 분석 결과, 범인은 Turbo의 내부 생명주기 관리에 있었습니다. Turbo의 Navigator는 새로운 폼 제출이 시작될 때 이전의 진행 중인(in-flight) 요청이 있다면 this.stop()을 호출하여 강제로 중단시킵니다.

  • 사용자가 버튼을 연타하면 이전 요청이 중단되거나, 버튼의 활성/비활성 상태가 꼬이게 됩니다.
  • 서버는 요청을 받아 처리하고 브로드캐스트를 보내지만, 클라이언트 측 Turbo가 요청을 취소하거나 응답 처리를 방해하면서 상태 업데이트의 일관성이 깨집니다.
  • 결과적으로 클릭 횟수, 서버 처리 횟수, 클라이언트 수신 횟수가 일치하지 않는 비대칭적 상황이 발생합니다.

5. 최종 해결책: Turbo 우회 및 단일 경로 확보

문제를 완전히 해결하기 위해 실시간 브로드캐스트가 동반되는 액션에서는 Turbo의 개입을 완전히 제거해야 합니다.

  • Step 1: data-turbo=”false” 설정: 폼 제출 시 Turbo Navigator를 거치지 않고 브라우저 표준 HTTP 요청을 사용하도록 합니다. 이를 통해 요청 중단이나 버튼 상태 관리의 간섭을 피할 수 있습니다.
  • Step 2: head :no_content 반환: 브라우저가 204 응답을 받으면 현재 페이지를 유지하므로, 화면 업데이트는 오직 ActionCable 브로드캐스트를 통해서만 이루어집니다.

이 방식을 통해 모든 클라이언트(클릭한 기기와 관찰하는 기기 모두)가 동일한 ActionCable 경로로만 업데이트를 받게 되어 레이스 컨디션이 원천 차단됩니다.

결론

실시간 기능의 핵심은 데이터의 일관성과 신뢰할 수 있는 전달 경로를 확보하는 것입니다. Turbo와 ActionCable이 동일한 상태를 업데이트하려고 경쟁할 때 발생하는 비대칭성은 디버깅을 매우 어렵게 만드는 주요 원인이 됩니다. 문제의 핵심인 Turbo의 폼 관리 생명주기를 해당 액션에서 분리하고, ActionCable을 유일한 업데이트 경로로 설정함으로써 시스템의 복잡성을 줄이고 안정성을 획기적으로 높일 수 있습니다. 이는 Rails의 편리한 도구들을 결합하여 사용할 때 각 도구의 내부 동작 방식과 상호작용을 정확히 이해하는 것이 얼마나 중요한지 시사합니다.

댓글 0

댓글 작성

댓글 삭제 시 비밀번호가 필요합니다.

이미 계정이 있으신가요? 로그인 후 댓글을 작성하세요.

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