웹 애플리케이션에서 실시간 기능의 중요성이 날로 커지고 있지만, 모든 Rails 개발자가 Rails에 내장된 WebSocket 라이브러리인 Action Cable에 익숙한 것은 아닙니다.
Rails Action Cable은 오랫동안 웹소켓을 지원해 왔지만, 그만큼 추가적인 복잡성도 함께 따라왔습니다. Rails 8에는 Solid Cable이라는 새로운 데이터베이스 기반 어댑터가 도입되어 Redis 없이도 Action Cable을 사용할 수 있게 되었습니다. 이 가이드에서는 Solid Cable을 통해 Action Cable을 살펴보고, 실제 실시간 기능을 직접 구축하는 방법을 단계별로 안내합니다. Redis를 신경 쓰지 않고도 Rails 8 앱에 실시간 기능을 얼마나 쉽게 추가할 수 있는지 확인하게 될 것입니다.
직접 따라 하면서 앱을 함께 만들어 보셔도 좋고, 완성된 프로젝트는 GitHub에서 확인하셔도 좋습니다.
왜 Rails Action Cable을 사용해야 할까?
현대적인 웹 앱은 클라이언트에게 업데이트를 실시간으로 푸시해야 하는 경우가 많습니다. 채팅 메시지가 즉시 나타나거나 대시보드 알림이 실시간으로 뜨는 것이 대표적인 예입니다. Action Cable은 Rails에 내장된 솔루션으로, 앱에 WebSocket을 통합하여 서버와 클라이언트 간의 양방향 지속 통신을 가능하게 해줍니다. 개인적으로 Action Cable이 Rails 프레임워크의 일부라는 점에 감사하고 있는데, 진정으로 유용한 웹 앱을 만드는 데 필요한 모든 것을 제공한다는 Rails 철학을 잘 뒷받침해 주기 때문입니다. Action Cable을 사용하면 브라우저가 명시적으로 요청하지 않아도(사용자가 새로고침할 필요 없이!) 서버가 데이터를 브라우저로 보낼 수 있습니다.
Action Cable과 WebSocket을 활용하면 서버 렌더링 방식의 앱에서 구현하기 어려웠던 라이브 인터랙티브 기능을 Rails 앱에 손쉽게 추가할 수 있습니다. 대표적인 활용 사례는 다음과 같습니다.
- 실시간 채팅 애플리케이션
- 알림 및 피드
- 실시간 동기화가 필요한 협업 앱
- 실시간 스포츠 중계나 주식 시세 티커
요약하자면, Action Cable은 전통적인 요청-응답 사이클과 실시간 이벤트 기반 업데이트 사이의 간극을 메워주는 다리 역할을 합니다. 클라이언트 측면에서는 Rails가 제공하는 JavaScript 컨슈머(consumer)를 통해 채널을 구독하고 브로드캐스트를 수신합니다. 개발자는 컨트롤러와 비슷하지만 실시간 스트림을 위한 것인 백엔드 '채널'을 정의하고, 프론트엔드 클라이언트가 이 채널을 구독하는 방식으로 Action Cable을 다루게 됩니다.
그렇다면 Solid Cable이란 무엇일까?
이전 버전의 Rails에서 Action Cable을 사용해 본 적이 있다면, 운영 환경에서는 일반적으로 Redis(또는 PostgreSQL의 NOTIFY)를 사용해 여러 서버 프로세스 간에 메시지를 브로드캐스트했다는 것을 알고 있을 것입니다. pub/sub 서비스(주로 Redis)는 한 Rails 프로세스에서 발생한 메시지를 다른 모든 프로세스에 전달하여, 각 프로세스가 자신에게 연결된 WebSocket 클라이언트로 메시지를 전달할 수 있도록 합니다. 이러한 추가 인프라는 Action Cable을 사용하기 위한 필수 요건이었고, 그동안 상당한 부담이 되어 왔습니다.
Rails 8에서 도입된 Solid Cable은 Redis 같은 외부 pub/sub 서비스 대신 기존 데이터베이스를 백엔드로 활용함으로써 이러한 요구 사항을 없애 줍니다. Active Job의 Solid Queue, 캐싱의 Solid Cache처럼, Solid Cable은 Action Cable을 위한 데이터베이스 기반 어댑터입니다. 들어오는 각 WebSocket 메시지는 데이터베이스 테이블에 기록되고, 모든 Action Cable 인스턴스가 해당 테이블을 폴링(polling)하여 새 메시지를 감지하면 클라이언트로 브로드캐스트합니다. 이 과정은 매우 빠르게(기본값 100밀리초마다) 이루어져 거의 실시간에 가까운 성능을 제공합니다. 또한 메시지는 짧은 시간(기본 24시간) 동안만 저장된 후 정리(pruning)되므로, 최근 발생한 문제를 디버깅하더라도 데이터베이스 용량에 대한 걱정은 필요 없습니다.
전체적으로 Solid Cable은 Rails 8의 "Solid 트리펙타(Solid Trifecta)" 철학에 부합합니다. 즉, 캐싱, 백그라운드 작업, 실시간 메시징을 모두 데이터베이스 기반으로 처리하는 내장 기능 세트입니다. Solid Cable을 통해 작업 큐, 캐싱, WebSocket까지 모두 하나의 데이터베이스로 운영하는 마지막 조각이 맞춰진 셈입니다.
Solid Cable로 Rails 8 앱 만들기
Solid Cable을 직접 다뤄보고 싶으시겠죠? 지금부터 Rails 8 애플리케이션에 Solid Cable을 추가하고, 여러 사용자가 실시간으로 메시지를 주고받을 수 있는 미니멀한 채팅방을 만들어 보겠습니다.
예제 앱 생성하기
이 예제에서는 Solid Cable을 백엔드로 사용하면서 Action Cable의 기본 개념(채널, 구독, 브로드캐스팅)을 배우게 됩니다. Rails 8을 기준으로 진행하니, 다음 명령어로 새 Rails 앱을 생성해 주세요.
rails _8.1.0_ new solid_cable_chat --database=sqlite3
그다음 새로 생성된 solid_cable_chat 디렉터리로 cd 명령어를 이용해 이동합니다.
Rails 8을 사용했기 때문에 Solid Cable이나 다른 젬(gem)을 추가로 설치할 필요가 없습니다. 대부분의 설정이 이미 준비되어 있습니다. 이전 버전 Rails에서 넘어오신 분들을 위해 설정 과정을 하나하나 짚어 드리겠습니다.
Solid Cable 설정하기
먼저 Solid Cable 설치 명령어를 실행합니다.
bin/rails solid_cable:install
이 제너레이터는 두 가지 주요 작업을 수행합니다. 첫째, Solid Cable을 케이블 어댑터로 지정하는 config/cable.yml 설정 파일을 생성합니다. 둘째, Solid Cable의 messages 테이블에 대한 데이터베이스 스키마 정의가 담긴 db/cable_schema.rb 파일을 생성합니다. 최신 버전의 Rails에서는 rails new 실행 시 이 파일들이 자동으로 생성됩니다.
다음으로 Solid Cable을 위한 데이터베이스 설정이 필요합니다. 기본적으로 Rails는 실시간 메시징 데이터를 다른 데이터와 격리하기 위해 Solid Cable에 별도의 데이터베이스를 사용합니다. 개발 환경에서는 동일한 데이터베이스를 쓰거나 별도의 데이터베이스를 구성할 수 있습니다. 여기서는 개발 환경에서 Solid Cable을 위한 별도의 SQLite 데이터베이스를 사용하는 방법을 안내하겠습니다. 이를 위해 새로운 "cable" 데이터베이스 연결을 추가해야 합니다.
Solid Cable을 위한 데이터베이스 구성
config/database.yml 파일을 열고 development 섹션에 cable 데이터베이스를 추가합니다. SQLite(Rails 개발 환경의 기본값)를 사용한다면 다음과 같습니다.
development:
primary:
<<: *default
database: storage/development.sqlite3
cable:
<<: *default
database: storage/development_cable.sqlite3
migrations_paths: db/cable_migrate
production:
primary:
<<: *default
database: storage/production.sqlite3
cache:
<<: *default
database: storage/production_cache.sqlite3
migrations_paths: db/cache_migrate
queue:
<<: *default
database: storage/production_queue.sqlite3
migrations_paths: db/queue_migrate
cable:
<<: *default
database: storage/production_cable.sqlite3
migrations_paths: db/cable_migrate
앞서 말씀드린 대로, 충분히 최신 버전의 Rails를 사용 중이라면 이 설정은 이미 포함되어 있을 것입니다.
이제 config/cable.yml 파일을 열어 보세요. 운영 환경에서는 Solid Cable이 이미 기본 어댑터로 지정되어 있을 것입니다. 우리는 개발 환경에서도 Solid Cable을 활성화하고 싶습니다(localhost에서 채팅을 테스트하기 위해). cable.yml을 편집하여 개발 환경에서도 solid_cable 어댑터를 사용하도록 하고, 방금 구성한 cable 데이터베이스를 가리키도록 설정합니다.
development:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.day
test:
adapter: test
production:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.day
위 cable.yml에서는 개발 환경 어댑터를 solid_cable로 설정하고 운영 환경 설정을 그대로 복사했습니다. connects_to 설정은 Action Cable에게 메시지 저장을 위해(database.yml에 정의된) cable 데이터베이스를 사용하라고 지시하는 부분입니다. 최신 버전의 Rails를 사용하더라도 이 부분은 직접 수정해야 합니다.
소규모 앱이라면 별도 데이터베이스 설정을 제거하고 스키마를 마이그레이션으로 옮겨 기본(primary) 데이터베이스에 Solid Cable 테이블을 함께 두는 방식도 가능합니다. 하지만 메인 앱 데이터와의 성능 간섭 가능성을 피하기 위해 별도 데이터베이스 사용이 권장됩니다.
마지막으로 rails db:prepare를 실행하여 데이터베이스가 준비되었는지 확인합니다. 앱을 실제 배포할 때도 운영 환경에서 이 작업을 수행해야 합니다.
Action Cable 채널 만들기
Action Cable은 데이터 스트림을 처리하는 Ruby 클래스인 '채널(channel)'을 통해 동작합니다. HTTP 요청을 처리하는 컨트롤러와 다소 비슷한 개념입니다. 채팅 기능을 위한 채널을 만들어 보겠습니다. 이름은 UserChatChannel로 하겠습니다. 제너레이터를 사용합니다.
rails generate channel UserChat
생성된 app/channels/user_chat_channel.rb 파일을 열고 새 로직을 추가합니다.
클라이언트가 UserChatChannel을 구독하면(채팅 페이지를 열 때), subscribed 콜백이 호출됩니다. 이 콜백 안에서 stream_from "user_chat_channel"을 호출하여 "user_chat_channel"이라는 브로드캐스트 스트림을 수신하기 시작합니다.
즉, "user_chat_channel 스트림으로 브로드캐스트되는 모든 데이터를 수신해서 이 채널의 클라이언트에게 전달하라"는 의미입니다. 이 채널을 구독한 모든 사용자는 "user_chat_channel"로 전송되는 메시지 브로드캐스트를 받게 됩니다.
또한 커스텀 액션도 정의해야 합니다. 이름을 talk(data)로 하겠습니다. 채널의 public 메서드는 클라이언트 쪽에서 호출할 수 있습니다. 클라이언트가 perform("talk", { content: "Hello World" })를 호출하면 서버에서 talk 메서드가 실행됩니다.
talk 구현은 클라이언트가 보낸 메시지 내용을 받아 ActionCable.server.broadcast를 통해 "user_chat_channel"을 구독 중인 모든 사람에게 전달합니다. 덕분에 모든 구독자(발신자 포함)가 메시지 데이터를 실시간으로 수신합니다. 여기서는 단순히 메시지 텍스트를 담은 해시를 브로드캐스트하지만, 필요에 따라 사용자 이름이나 타임스탬프 같은 다른 정보를 포함할 수도 있습니다. 참고: 실제 앱에서는 이 단계에서 메시지를 데이터베이스에 저장하거나 유효성 검사를 수행하는 것이 좋습니다. 여기서는 단순함을 위해 브로드캐스트만 진행합니다.
class UserChatChannel < ApplicationCable::Channel
def subscribed
stream_from "user_chat_channel"
end
def unsubscribed
# Any cleanup needed when unsubscribing from the channel
end
def talk(data)
message = data["content"]
ActionCable.server.broadcast("user_chat_channel", { content: message })
end
end
클라이언트에서 채널 컨슈머 구현하기
백엔드 구축이 끝났으니, 이제 프론트엔드를 연결하여 사용자가 WebSocket을 통해 메시지를 주고받으며 실시간 기능을 체험할 수 있도록 해야 합니다.
Rails 8에는 Action Cable 관련 JavaScript가 기본 내장되어 있습니다. 제너레이터가 app/javascript/channels/user_chat_channel.js 파일을 만들어 두었으니, 이곳에 클라이언트 동작을 구현하겠습니다.
app/javascript/channels/user_chat_channel.js 파일을 열어 아래와 같이 수정합니다.
import consumer from "channels/consumer";
const userChatChannel = consumer.subscriptions.create("UserChatChannel", {
connected() {
console.log("Connected to UserChatChannel.");
},
disconnected() {
console.log("Disconnected from UserChatChannel.");
},
received(data) {
const messagesDiv = document.getElementById("messages");
if (messagesDiv && data.content) {
const messageElement = document.createElement("p");
messageElement.textContent = data.content;
messagesDiv.appendChild(messageElement);
}
}
});
function sendMessage(content) {
userChatChannel.perform("talk", { content: content });
}
export { sendMessage };
window.sendMessage = sendMessage;
여기서 consumer.subscriptions.create("UserChatChannel", {...})를 사용하여 서버의 UserChatChannel에 대한 구독을 생성합니다. 이 호출은 채널과 상호작용할 수 있는 구독 객체를 반환합니다.
connected() 콜백은 연결이 성립되면 실행됩니다. 여기서는 동작 확인을 위해 콘솔에 로그를 출력합니다.
disconnected() 콜백은 WebSocket 연결이 끊어졌을 때 실행됩니다.
received(data) 콜백이 바로 핵심입니다! 이 콜백은 채널이 서버로부터 브로드캐스트를 수신할 때마다 발동합니다. UserChatChannel#talk에서 { content: message }를 브로드캐스트했으므로, 여기서 data 인자는 바로 그 해시입니다. 새 메시지가 도착하면 이 코드가 모든 연결된 클라이언트의 채팅 화면을 즉시 갱신해 줍니다.
또한 sendMessage(content) 헬퍼 함수를 정의하여 userChatChannel.perform("talk", { content: ... })를 호출합니다. 이것은 사용자가 입력한 메시지 내용과 함께 앞서 정의한 서버 측 talk 액션으로 요청을 보냅니다.
이제 사용자가 메시지를 주고받을 수 있는 간단한 UI가 필요합니다. 기본적인 뷰를 만들어 보겠습니다.
예제 앱용 간단한 UI 만들기
먼저 컨트롤러를 생성합니다.
rails generate controller UserChat index
다음으로 index 뷰를 열어 기본 구조를 작성합니다.
<h1>Chats from Users</h1>
<div id="messages" style="border: 1px solid #ccc; padding: 1em; height: 200px; overflow-y: auto; margin-bottom: 1em;">
<!-- Messages will appear here -->
</div>
<form id="chat-form" onsubmit="event.preventDefault(); sendMessage(document.getElementById('chat-input').value); document.getElementById('chat-input').value = '';">
<input type="text" id="chat-input" placeholder="Type a message..." autocomplete="off" style="width: 80%;" />
<button type="submit">Send</button>
</form>
마지막으로 config/routes.rb에서 루트 경로가 이 컨트롤러를 가리키도록 설정합니다.
root "user_chat#index"
전체 동작 확인하기
간단한 채팅 앱이 테스트 준비를 마쳤습니다! bin/dev로 프로젝트를 실행하고 localhost:3000에 접속해 보세요.

실시간 업데이트를 확인하려면 서로 다른 브라우저 탭 두 개에서 앱을 열어 보세요. 한쪽 탭에서 "첫 번째 탭에서 보내는 인사입니다!" 같은 메시지를 입력합니다.
두 번째 탭에서 메시지를 보내면 첫 번째 탭에 해당 메시지가 즉시 나타나는 것을 볼 수 있습니다!

Rails Action Cable 운영 환경 배포하기
Solid Cable은 WebSocket 메시지를 데이터베이스 테이블에 저장하며, 앞선 예제에서는 기본 cable 데이터베이스를 사용했습니다. Rails 8은 신규 앱에서 Solid Cable에 SQLite를 기본으로 사용하지만, config/database.yml에 cable 섹션을 추가하면 Rails가 지원하는 어떤 데이터베이스든 연결할 수 있습니다.
실제로 운영 환경에서는 실시간 메시징 부하를 다른 데이터로부터 분리하기 위해 Solid Cable에 별도의 데이터베이스를 사용하는 것이 권장됩니다. 예를 들어 메인 앱 데이터는 app_production에 두고, Solid Cable 전용으로 app_production_cable 데이터베이스를 프로비저닝할 수 있습니다.
이런 분리를 통해 채팅이나 알림 트래픽이 메인 애플리케이션 쿼리와 경쟁하는 상황을 예방할 수 있습니다. 물론 소규모 앱이라면 앱 데이터와 Cable 메시지를 하나의 데이터베이스에서 함께 관리해도 대체로 문제없습니다.
주의할 점은 배포 설정에 Solid Cable 데이터베이스가 반드시 포함되도록 하는 것입니다. 별도 데이터베이스를 사용한다면, 운영 환경에서 Rails가 messages 테이블을 생성할 수 있도록 rails db:prepare 또는 rails db:migrate를 실행하는 것을 잊지 마세요.
또한 WebSocket 연결 하나하나가 서버 메모리를 소모한다는 점도 기억하세요. 필요한 동시 접속 수를 처리할 수 있도록 서버 리소스가 충분한지 확인해야 합니다.
폴링 간격 설정하기
Solid Cable의 폴링 주기는 설정 가능하므로, 지연 시간과 데이터베이스 부하 사이의 균형을 조절할 수 있습니다. 간격을 줄이면 폴링이 더 자주 발생하여 새 메시지를 더 빨리 감지하지만, 그만큼 데이터베이스에 더 많은 SELECT 쿼리가 발생합니다.
반대로 간격을 늘리면 데이터베이스 부하는 줄지만 브로드캐스트와 업데이트에 더 많은 지연이 생깁니다. 실무에서는 기본값인 0.1초(초당 10회 폴링)가 대부분의 데이터베이스에 무리를 주지 않으면서도 실시간에 가까운 업데이트를 제공하는 좋은 출발점입니다.
Solid Cable은 Solid 트리펙타의 핵심 기둥입니다
지금까지 Rails Action Cable이 실시간 통신을 위해 WebSocket을 Rails에 어떻게 도입하는지, 그리고 Solid Cable이 어떻게 Redis 없이 이를 가능하게 하는지 살펴보았습니다. Rails에는 또 다른 두 가지 "Solid" 라이브러리가 있다는 것도 알고 계셨나요? Solid Cache는 Redis 없이 캐싱을, Solid Queue는 Redis 없이 백그라운드 작업 처리를 가능하게 해 줍니다.
"Solid 트리펙타"를 활용하면 최소한의 인프라 오버헤드로 인터랙티브한 애플리케이션을 구축할 수 있는 강력한 프레임워크를 손에 넣게 됩니다.
Solid Cable과 그 형제들의 핵심 장점은 바로 단순함입니다. Rails 앱의 실시간 기능은 뒤편에서 앱 자체의 데이터베이스만으로 즉시(out of the box) 동작합니다. 배포도 더 간단해지고(Redis나 추가 서비스 불필요), 많은 애플리케이션에서 성능 또한 충분히 만족스럽습니다.
물론 운영 환경에서 Rails 애플리케이션을 구동할 때는 사용자가 마주칠 수 있는 문제를 모니터링해야 합니다. Action Cable의 컨슈머와 채널에 문제가 생겼을 때 사용자보다 먼저 알 수 있다면 좋지 않을까요?
Honeybadger는 Rails 오류 및 성능 모니터링에 탁월한 선택지로, 실시간 애플리케이션 배포에 필수적인 요소입니다. Honeybadger는 백엔드와 클라이언트 측 등 애플리케이션 어디에서든 오류가 발생하면 즉시 알려주며, 애플리케이션 로그와 성능 데이터를 통합하여 빠른 검색, 문제 해결, 조치를 지원합니다.
지금 Honeybadger에 가입하고 시작해 보세요!