1 Action Cable이란?
Action Cable은 WebSocket을 Rails 애플리케이션의 나머지 부분과 원활하게 통합합니다. 실시간 기능을 Rails 애플리케이션의 다른 부분과 동일한 스타일과 형식으로 Ruby로 작성할 수 있으며, 동시에 높은 성능과 확장성을 제공합니다. 클라이언트 측 JavaScript 프레임워크와 서버 측 Ruby 프레임워크를 모두 제공하는 풀스택 솔루션입니다. Active Record나 선택한 ORM으로 작성된 전체 도메인 모델에 접근할 수 있습니다.
2 용어
Action Cable은 HTTP 요청-응답 프로토콜 대신 WebSocket을 사용합니다. Action Cable과 WebSocket 모두 다소 생소한 용어들을 소개합니다:
2.1 연결(Connection)
연결(Connection)은 클라이언트-서버 관계의 기초를 형성합니다. 하나의 Action Cable 서버는 여러 개의 연결 인스턴스를 처리할 수 있습니다. WebSocket 연결 하나당 하나의 연결 인스턴스를 갖습니다. 한 사용자가 여러 브라우저 탭이나 디바이스를 사용하는 경우 애플리케이션에 대해 여러 개의 WebSocket을 열 수 있습니다.
2.2 소비자(Consumer)
WebSocket 연결의 클라이언트를 소비자(consumer)라고 합니다. Action Cable에서 소비자는 클라이언트 측 JavaScript 프레임워크에 의해 생성됩니다.
각 소비자(consumer)는 복수의 채널들을 구독할 수 있습니다. 각 채널은 일반적인 MVC 구조에서 컨트롤러가 하는 것과 유사하게 논리적 작업 단위를 캡슐화합니다. 예를 들어, ChatChannel
과 AppearancesChannel
을 가질 수 있으며, 소비자는 이러한 채널들 중 하나 또는 모두를 구독할 수 있습니다. 최소한 소비자는 하나의 채널은 구독해야 합니다.
2.3 Subscribers (구독자)
소비자(consumer)가 채널을 구독하면 구독자(subscriber)로서 동작합니다. 구독자와 채널 사이의 연결을 놀랍게도 구독(subscription)이라고 부릅니다. 하나의 소비자는 주어진 채널에 대해 여러 번 구독자로 동작할 수 있습니다. 예를 들어, 소비자는 여러 채팅방에 동시에 구독할 수 있습니다. (그리고 한 명의 실제 사용자가 여러 개의 소비자를 가질 수 있다는 점을 기억하세요. 탭이나 디바이스마다 하나의 연결이 열려있을 수 있습니다.) Pub/Sub(게시-구독) 또는 Publish-Subscribe는 정보 발신자(publisher)가 개별 수신자를 지정하지 않고 추상화된 수신자(subscriber) 클래스에 데이터를 전송하는 메시지 큐 패러다임을 의미합니다. Action Cable은 서버와 다수의 클라이언트 간 통신을 위해 이 방식을 사용합니다.
2.4 브로드캐스팅(Broadcasting)
브로드캐스팅은 발행-구독(pub/sub) 링크로, 브로드캐스터가 전송하는 모든 내용이 해당 브로드캐스팅을 스트리밍하는 채널 구독자들에게 직접 전송됩니다. 각 채널은 0개 이상의 브로드캐스팅을 스트리밍할 수 있습니다.
3 서버 측 컴포넌트
3.1 연결(Connections)
서버가 WebSocket을 수락할 때마다 연결 객체가 인스턴스화됩니다. 이 객체는 이후 생성되는 모든 채널 구독(channel subscriptions)의 상위 객체가 됩니다. 연결 자체는 인증과 인가를 제외한 어떤 특정 애플리케이션 로직도 다루지 않습니다. WebSocket 연결의 클라이언트를 연결 소비자(consumer)라고 합니다. 개별 사용자는 브라우저 탭, 창 또는 기기마다 하나의 소비자-연결 쌍을 생성합니다.
연결은 [ActionCable::Connection::Base
][]를 확장하는 ApplicationCable::Connection
의 인스턴스입니다. ApplicationCable::Connection
에서는 들어오는 연결을 인가하고 사용자가 식별될 수 있는 경우 연결을 수립합니다.
3.1.1 연결 설정
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
여기서 [identified_by
][]는 나중에 특정 연결을 찾는 데 사용할 수 있는 연결 식별자를 지정합니다. 식별자로 표시된 모든 것은 해당 연결에서 생성된 모든 채널 인스턴스에 동일한 이름의 위임자를 자동으로 생성한다는 점에 주의하세요.
이 예제는 애플리케이션의 다른 곳에서 사용자 인증을 이미 처리했으며, 성공적인 인증이 사용자 ID가 포함된 암호화된 쿠키를 설정했다는 것을 전제로 합니다.
새로운 연결이 시도될 때 쿠키는 자동으로 연결 인스턴스로 전송되며, 이를 사용하여 current_user
를 설정합니다. 동일한 현재 사용자로 연결을 식별함으로써, 나중에 주어진 사용자의 모든 열린 연결을 검색할 수 있으며(그리고 사용자가 삭제되거나 권한이 없어진 경우 모든 연결을 끊을 수 있습니다).
인증 방식에 세션을 사용하고, 세션에 쿠키 저장소를 사용하며, 세션 쿠키 이름이 _session
이고 사용자 ID 키가 user_id
인 경우 다음 방식을 사용할 수 있습니다:
verified_user = User.find_by(id: cookies.encrypted["_session"]["user_id"])
3.1.2 예외 처리
기본적으로 처리되지 않은 예외는 캐치되어 Rails 로거에 기록됩니다. 이러한 예외를 전역적으로 가로채서 외부 버그 추적 서비스에
3.2 채널
채널(channel)은 일반적인 MVC 설정에서 컨트롤러가 하는 것과 비슷한 논리적 작업 단위를 캡슐화합니다. 기본적으로 Rails는 채널 생성기를 처음 사용할 때 채널들 간의 공유 로직을 캡슐화하기 위해 부모 ApplicationCable::Channel
클래스([ActionCable::Channel::Base
]를 상속)를 생성합니다.
3.2.1 부모 채널 설정
# app/channels/application_cable/channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
여러분의 채널 클래스는 다음 예시처럼 작성할 수 있습니다:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
end
# app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
end
소비자(consumer)는 이러한 채널들 중 하나 또는 둘 모두를 구독할 수 있습니다.
3.2.2 구독
소비자는 구독자(subscribers)로서 채널을 구독합니다. 이들의 연결을 구독(subscription)이라고 합니다. 생성된 메시지는 채널 소비자가 보낸 식별자를 기반으로 이러한 채널 구독으로 라우팅됩니다.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
# 소비자가 이 채널의 구독자가
# 되는데 성공했을 때 호출됩니다.
def subscribed
end
end
3.2.3 예외 처리
ApplicationCable::Connection
과 마찬가지로, 특정 채널에서 [rescue_from
]을 사용하여 발생한 예외를 처리할 수 있습니다:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
rescue_from "MyError", with: :deliver_error_message
private
def deliver_error_message(e)
# broadcast_to(...)
end
end
3.2.4 채널 콜백
[ActionCable::Channel::Callbacks
]는 채널의 생명주기 동안 호출되는 콜백 훅을 제공합니다:
4 클라이언트 측 컴포넌트
4.1 연결(Connections)
Consumer는 자신의 측에서 연결 인스턴스가 필요합니다. Rails에서 기본적으로 생성되는 다음 JavaScript를 사용하여 연결을 설정할 수 있습니다:
4.1.1 Consumer 연결
// app/javascript/channels/consumer.js
// Action Cable은 Rails에서 WebSocket을 다루기 위한 프레임워크를 제공합니다.
// `bin/rails generate channel` 명령을 사용하여 WebSocket 기능이 포함된 새 채널을 생성할 수 있습니다.
import { createConsumer } from "@rails/actioncable"
export default createConsumer()
이는 기본적으로 서버의 /cable
에 대해 연결할 consumer를 준비합니다. 관심 있는 구독을 하나 이상 지정하기 전까지는 연결이 설정되지 않습니다.
consumer는 선택적으로 연결할 URL을 지정하는 인자를 받을 수 있습니다. 이는 문자열이나 WebSocket이 열릴 때 호출되어 문자열을 반환하는 함수가 될 수 있습니다.
// 다른 URL로 연결 지정
createConsumer('wss://example.com/cable')
// 또는 HTTP를 통한 웹소켓 사용 시
createConsumer('https://ws.example.com/cable')
// URL을 동적으로 생성하는 함수 사용
createConsumer(getWebSocketURL)
function getWebSocketURL() {
const token = localStorage.get('auth-token')
return `wss://example.com/cable?token=${token}`
}
4.1.2 구독자(Subscriber)
consumer는 주어진 채널에 대한 구독을 생성함으로써 구독자가 됩니다:
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" })
// app/javascript/channels/appearance_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "AppearanceChannel" })
이는 구독을 생성하지만, 수신된 데이터에 응답하는 데 필요한 기능은 나중에 설명됩니다.
consumer는 주어진 채널에 대해 여러 번 구독자로 작동할 수 있습니다. 예를 들어, consumer는 동시에 여러 채팅방을 구독할 수 있습니다:
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "ChatChannel", room: "1st Room" })
consumer.subscriptions.create({ channel: "ChatChannel", room: "2nd Room" })
5 클라이언트-서버 상호작용
5.1 스트림
스트림(Streams)은 채널이 발행된 콘텐츠(브로드캐스트)를 구독자에게 전달하는 메커니즘을 제공합니다. 예를 들어, 다음 코드는 :room
파라미터의 값이 "Best Room"
일 때 [stream_from
][]을 사용하여 chat_Best Room
이라는 브로드캐스팅을 구독합니다:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
end
그런 다음 Rails 애플리케이션의 다른 곳에서 [broadcast
][]를 호출하여 해당 채팅방에 브로드캐스트할 수 있습니다:
ActionCable.server.broadcast("chat_Best Room", { body: "This Room is Best Room." })
스트림이 모델과 관련되어 있다면, 브로드캐스팅 이름을 채널과 모델로부터 생성할 수 있습니다. 예를 들어, 다음 코드는 [stream_for
][]를 사용하여 posts:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE
와 같은 브로드캐스팅을 구독합니다. 여기서 Z2lkOi8vVGVzdEFwcC9Qb3N0LzE
는 Post 모델의 GlobalID입니다.
class PostsChannel < ApplicationCable::Channel
def subscribed
post = Post.find(params[:id])
stream_for post
end
end
그런 다음 [broadcast_to
][]를 호출하여 이 채널에 브로드캐스트할 수 있습니다:
PostsChannel.broadcast_to(@post, @comment)
5.2 브로드캐스팅(Broadcasting)
브로드캐스팅은 발행자(publisher)가 전송한 모든 내용이 해당 브로드캐스팅을 스트리밍하고 있는 채널 구독자들에게 직접 라우팅되는 발행-구독(pub/sub) 링크입니다. 각 채널은 0개 이상의 브로드캐스팅을 스트리밍할 수 있습니다.
브로드캐스팅은 순수하게 온라인 큐이며 시간 의존적입니다. 사용자가 스트리밍(특정 채널을 구독)하고 있지 않다면, 나중에 연결하더라도 브로드캐스트를 수신할 수 없습니다.
5.3 구독
소비자가 채널을 구독하면 구독자로 활동하게 됩니다. 이러한 연결을 구독(subscription)이라고 합니다. 수신되는 메시지는 케이블 소비자가 전송한 식별자를 기반으로 이러한 채널 구독으로 라우팅됩니다.
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
received(data) {
this.appendLine(data)
},
appendLine(data) {
const html = this.createLine(data)
const element = document.querySelector("[data-chat-room='Best Room']")
element.insertAdjacentHTML("beforeend", html)
},
createLine(data) {
return `
<article class="chat-line">
<span class="speaker">${data["sent_by"]}</span>
<span class="body">${data["body"]}</span>
</article>
`
}
})
5.4 채널에 매개변수 전달하기
구독을 생성할 때 클라이언트 측에서 서버 측으로 매개변수를 전달할 수 있습니다. 예시:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
end
subscriptions.create
의 첫 번째 인자로 전달된 객체는 케이블 채널에서 params 해시가 됩니다. 키워드 channel
은 필수입니다:
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
received(data) {
this.appendLine(data)
},
appendLine(data) {
const html = this.createLine(data)
const element = document.querySelector("[data-chat-room='Best Room']")
element.insertAdjacentHTML("beforeend", html)
},
createLine(data) {
return `
<article class="chat-line">
<span class="speaker">${data["sent_by"]}</span>
<span class="body">${data["body"]}</span>
</article>
`
}
})
# 앱의 어딘가에서 이것이 호출됩니다.
# 아마도 NewCommentJob에서 호출될 수 있습니다.
ActionCable.server.broadcast(
"chat_#{room}",
{
sent_by: "Paul",
body: "This is a cool chat app."
}
)
5.5 메시지 재브로드캐스팅
일반적인 유스케이스는 한 클라이언트가 보낸 메시지를 다른 연결된 클라이언트들에게 재브로드캐스트(rebroadcast)하는 것입니다.
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
def receive(data)
ActionCable.server.broadcast("chat_#{params[:room]}", data)
end
end
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
const chatChannel = consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
received(data) {
// data => { sent_by: "Paul", body: "This is a cool chat app." }
}
})
chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
재브로드캐스트는 메시지를 보낸 클라이언트를 포함한 모든 연결된 클라이언트들이 수신하게 됩니다. 파라미터들은 채널을 구독했을 때와 동일하다는 점에 유의하세요.
6 풀스택 예제
다음 설정 단계들은 두 예제 모두에 공통적으로 적용됩니다:
- 연결 설정하기
- 부모 채널 설정하기
- 컨슈머 연결하기 ### 예제 1: 사용자 접속 상태
다음은 사용자의 온라인 여부와 현재 페이지를 추적하는 간단한 채널 예제입니다. (사용자가 온라인일 때 사용자 이름 옆에 초록색 점을 표시하는 것과 같은 실시간 상태 표시 기능을 구현할 때 유용합니다).
서버 측 접속 상태(appearance) 채널을 생성합니다:
# app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
def subscribed
current_user.appear
end
def unsubscribed
current_user.disappear
end
def appear(data)
current_user.appear(on: data["appearing_on"])
end
def away
current_user.away
end
end
구독이 시작되면 subscribed
콜백이 실행되며, 이때 "현재 사용자가 접속했음"을 표시합니다. 이 appear/disappear API는 Redis, 데이터베이스 또는 다른 저장소로 구현할 수 있습니다.
클라이언트 측 접속 상태 채널 구독을 생성합니다:
// app/javascript/channels/appearance_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("AppearanceChannel", {
// 구독이 생성될 때 한 번 호출됩니다.
initialized() {
this.update = this.update.bind(this)
},
// 서버에서 구독 준비가 완료되면 호출됩니다.
connected() {
this.install()
this.update()
},
// WebSocket 연결이 종료되면 호출됩니다.
disconnected() {
this.uninstall()
},
// 서버에서 구독이 거부되면 호출됩니다.
rejected() {
this.uninstall()
},
update() {
this.documentIsActive ? this.appear() : this.away()
},
appear() {
// 서버의 `AppearanceChannel#appear(data)`를 호출합니다.
this.perform("appear", { appearing_on: this.appearingOn })
},
away() {
// 서버의 `AppearanceChannel#away`를 호출합니다.
this.perform("away")
},
install() {
window.addEventListener("focus", this.update)
window.addEventListener("blur", this.update)
document.addEventListener("turbo:load", this.update)
document.addEventListener("visibilitychange", this.update)
},
uninstall() {
window.removeEventListener("focus", this.update)
window.removeEventListener("blur", this.update)
document.removeEventListener("turbo:load", this.update)
document.removeEventListener("visibilitychange", this.update)
},
get documentIsActive() {
return document.visibilityState === "visible" && document.hasFocus()
},
get appearingOn() {
const element = document.querySelector("[data-appearing-on]")
return element ? element.getAttribute("data-appearing-on") : null
}
})
6.1 클라이언트-서버 상호작용
클라이언트는
createConsumer()
를 통해 서버에 연결합니다(consumer.js
). 서버는 이 연결을current_user
로 식별합니다.클라이언트는 `consumer.subscriptions.create({ channel:
예제 2: 새로운 웹 알림 수신하기
앞선 appearance 예제는 WebSocket 연결을 통해 서버 기능을 클라이언트 측에서 호출하는 것에 관한 것이었습니다. 하지만 WebSocket의 훌륭한 점은 양방향 통신이 가능하다는 것입니다. 이제 서버가 클라이언트의 동작을 호출하는 예제를 살펴보겠습니다.
이는 관련 스트림으로 브로드캐스트할 때 클라이언트 측 웹 알림을 트리거할 수 있게 해주는 웹 알림 채널입니다:
서버 측 웹 알림 채널을 생성합니다:
# app/channels/web_notifications_channel.rb
class WebNotificationsChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
클라이언트 측 웹 알림 채널 구독을 생성합니다:
// app/javascript/channels/web_notifications_channel.js
// 웹 알림 전송 권한을 이미 요청했다고 가정한 클라이언트 측 코드
import consumer from "./consumer"
consumer.subscriptions.create("WebNotificationsChannel", {
received(data) {
new Notification(data["title"], { body: data["body"] })
}
})
애플리케이션의 다른 곳에서 웹 알림 채널 인스턴스로 컨텐츠를 브로드캐스트합니다:
# 앱의 어딘가에서 호출됨, 아마도 NewCommentJob에서
WebNotificationsChannel.broadcast_to(
current_user,
title: "New things!",
body: "All the news fit to print"
)
WebNotificationsChannel.broadcast_to
호출은 각 사용자별로 구분된 브로드캐스팅 이름 아래에 현재 구독 어댑터(subscription adapter)의 pubsub 큐에 메시지를 배치합니다. ID가 1인 사용자의 경우, 브로드캐스팅 이름은 web_notifications:1
이 됩니다.
채널은 web_notifications:1
에 도착하는 모든 것을 received
콜백을 호출하여 클라이언트로 직접 스트리밍하도록 지시받았습니다. 인자로 전달되는 데이터는 서버 측 브로드캐스트 호출의 두 번째 매개변수로 전송된 해시이며, 전송을 위해 JSON으로 인코딩되고 received
에 도착하는 데이터 인자로 언패킹됩니다.
6.2 더 완벽한 예시들
Rails 앱에서 Action Cable을 설정하고 채널을 추가하는 방법에 대한 전체 예시는 rails/actioncable-examples 저장소를 참조하세요.
7 구성
Action Cable은 두 가지 필수 구성이 필요합니다: 구독 어댑터(subscription adapter)와 허용된 요청 출처(allowed request origins)입니다.
7.1 구독 어댑터(Subscription Adapter)
기본적으로 Action Cable은 config/cable.yml
에서 설정 파일을 찾습니다.
이 파일은 각 Rails 환경에 대한 어댑터를 지정해야 합니다. 어댑터에 대한 추가 정보는 의존성 섹션을 참조하세요.
development:
adapter: async
test:
adapter: test
production:
adapter: redis
url: redis://10.10.3.153:6381
channel_prefix: appname_production
7.1.1 어댑터 설정
다음은 최종 사용자가 사용할 수 있는 구독 어댑터 목록입니다.
7.1.1.1 Async 어댑터
Async 어댑터는 개발/테스트용으로 만들어졌으며 프로덕션 환경에서는 사용하면 안 됩니다.
7.1.1.2 Redis 어댑터
Redis 어댑터를 사용하려면 Redis 서버를 가리키는 URL을 제공해야 합니다.
또한 여러 애플리케이션에서 동일한 Redis 서버를 사용할 때 채널 이름 충돌을 방지하기 위해 channel_prefix
를 제공할 수 있습니다. 자세한 내용은 Redis Pub/Sub 문서를 참조하세요.
Redis 어댑터는 SSL/TLS 연결도 지원합니다. 필요한 SSL/TLS 매개변수는 설정 YAML 파일의 ssl_params
키에 전달할 수 있습니다.
production:
adapter: redis
url: rediss://10.10.3.153:tls_port
channel_prefix: appname_production
ssl_params:
ca_file: "/path/to/ca.crt"
ssl_params
에 제공되는 옵션들은 OpenSSL::SSL::SSLContext#set_params
메소드에 직접 전달되며 SSL 컨텍스트의 유효한 속성이면 모두 사용할 수 있습니다.
다른 사용 가능한 속성에 대해서는 OpenSSL::SSL::SSLContext 문서를 참조하세요.
방화벽 뒤에서 자체 서명된 인증서를 사용하는 Redis 어댑터에서 인증서 검사를 건너뛰려면 ssl verify_mode
를 OpenSSL::SSL::VERIFY_NONE
으로 설정해야 합니다.
경고: 보안 의미를 완전히 이해하지 않는 한 프로덕션 환경에서 VERIFY_NONE
을 사용하는 것은 권장되지 않습니다. Redis 어댑터에서 이 옵션을 설정하려면 설정을 ssl_params: { verify_mode: <%= OpenSSL::SSL::VERIFY_NONE %> }
로 해야 합니다.
7.1.1.3 PostgreSQL 어댑터
PostgreSQL 어댑터는 Active Record의 연결 풀을 사용하며, 따라서 애플리케이션의 config/database.yml
데이터베이스 설정을 연결에 사용합니다.
이는 향후 변경될
7.2 허용된 요청 출처
Action Cable은 서버 설정에 배열로 전달된 특정 출처(origin)의 요청만 허용합니다. 이 출처는 문자열 또는 정규식 인스턴스가 될 수 있으며, 이를 기준으로 일치 여부를 확인합니다.
config.action_cable.allowed_request_origins = ["https://rubyonrails.com", %r{http://ruby.*}]
모든 출처의 요청을 허용하고 보호를 비활성화하려면 다음과 같이 설정합니다:
config.action_cable.disable_request_forgery_protection = true
기본적으로 Action Cable은 개발 환경에서 실행될 때 localhost:3000으로부터의 모든 요청을 허용합니다.
7.3 Consumer 설정
HTML 레이아웃의 HEAD 부분에 [action_cable_meta_tag
][]를 호출하여 URL을 설정할 수 있습니다. 이는 일반적으로 환경 설정 파일에서 [config.action_cable.url
][]을 통해 설정된 URL이나 경로를 사용합니다.
7.4 워커 풀 구성
워커 풀(Worker Pool)은 서버의 메인 스레드와 분리하여 연결 콜백과 채널 동작을 실행하는 데 사용됩니다. Action Cable은 워커 풀에서 동시에 처리되는 스레드 수를 애플리케이션에서 구성할 수 있도록 합니다.
config.action_cable.worker_pool_size = 4
또한 서버는 워커 수만큼의 데이터베이스 연결을 제공해야 한다는 점에 유의해야 합니다. 기본 워커 풀 크기는 4로 설정되어 있으므로, 최소 4개의 데이터베이스 연결을 사용할 수 있도록 해야 합니다. 이는 config/database.yml
파일의 pool
속성을 통해 변경할 수 있습니다.
7.5 클라이언트 측 로깅
클라이언트 측 로깅(Client-side Logging)은 기본적으로 비활성화되어 있습니다. ActionCable.logger.enabled
를 true로 설정하여 활성화할 수 있습니다.
import * as ActionCable from '@rails/actioncable'
ActionCable.logger.enabled = true
7.6 기타 설정
연결별 로거(per-connection logger)에 적용되는 로그 태그를 설정하는 것이 또 다른 일반적인 옵션입니다. 다음은 사용자 계정 ID가 있으면 해당 ID를, 없으면 "no-account"를 태깅하는 예시입니다:
config.action_cable.log_tags = [
-> request { request.env["user_account_id"] || "no-account" },
:action_cable,
-> request { request.uuid }
]
모든 설정 옵션의 전체 목록은 ActionCable::Server::Configuration
클래스를 참조하세요.
8 독립 실행형 케이블 서버 운영하기
Action Cable은 Rails 애플리케이션의 일부로 실행하거나 독립 서버로 실행할 수 있습니다. 개발 환경에서는 Rails 앱의 일부로 실행하는 것이 일반적으로 괜찮지만, 프로덕션 환경에서는 독립 실행형으로 운영해야 합니다.
8.1 앱 내부
Action Cable은 Rails 애플리케이션과 함께 실행될 수 있습니다. 예를 들어, /websocket
에서 WebSocket 요청을 수신하려면 [config.action_cable.mount_path
][]에 해당 경로를 지정하세요:
# config/application.rb
class Application < Rails::Application
config.action_cable.mount_path = "/websocket"
end
레이아웃에서 [action_cable_meta_tag
][]가 호출된 경우 ActionCable.createConsumer()
를 사용하여 cable 서버에 연결할 수 있습니다. 그렇지 않은 경우, createConsumer
의 첫 번째 인자로 경로를 지정해야 합니다(예: ActionCable.createConsumer("/websocket")
).
서버의 각 인스턴스와 서버가 생성하는 각 워커마다 새로운 Action Cable 인스턴스가 생성되지만, Redis 또는 PostgreSQL 어댑터가 연결 간 메시지를 동기화된 상태로 유지합니다.
8.2 독립 실행형
케이블 서버는 일반 애플리케이션 서버와 분리할 수 있습니다. 여전히 Rack 애플리케이션이지만, 독립적인 Rack 애플리케이션입니다. 권장되는 기본 설정은 다음과 같습니다:
# cable/config.ru
require_relative "../config/environment"
Rails.application.eager_load!
run ActionCable.server
서버를 시작하려면 다음 명령어를 실행합니다:
$ bundle exec puma -p 28080 cable/config.ru
이렇게 하면 28080 포트에서 케이블 서버가 시작됩니다. Rails가 이 서버를 사용하도록 하려면 설정을 다음과 같이 업데이트하세요:
# config/environments/development.rb
Rails.application.configure do
config.action_cable.mount_path = nil
config.action_cable.url = "ws://localhost:28080" # 프로덕션에서는 wss:// 사용
end
마지막으로, consumer가 올바르게 설정되어 있는지 확인하세요.
8.3 참고사항
WebSocket 서버는 세션에는 접근할 수 없지만 쿠키에는 접근할 수 있습니다. 이는 인증을 처리해야 할 때 활용할 수 있습니다. Devise를 사용한 구현 방법의 예시는 이 글에서 확인할 수 있습니다.
9 의존성
Action Cable은 내부 발행-구독(pubsub) 처리를 위한 구독 어댑터 인터페이스를 제공합니다. 기본적으로 비동기(asynchronous), 인라인(inline), PostgreSQL, Redis 어댑터가 포함되어 있습니다. 새로운 Rails 애플리케이션의 기본 어댑터는 비동기(async
) 어댑터입니다.
Ruby 측 구현은 websocket-driver, nio4r, concurrent-ruby를 기반으로 구축되었습니다.
10 배포
Action Cable은 WebSocket과 스레드의 조합으로 동작합니다. 프레임워크 내부 구조와 사용자가 지정한 채널 작업은 Ruby의 네이티브 스레드 지원을 활용하여 내부적으로 처리됩니다. 이는 스레드 안전성을 위반하지 않는 한 기존의 모든 Rails 모델을 문제없이 사용할 수 있다는 것을 의미합니다.
Action Cable 서버는 Rack 소켓 하이재킹(hijacking) API를 구현하여, 애플리케이션 서버의 멀티스레드 지원 여부와 관계없이 내부적으로 연결을 관리하기 위한 멀티스레드 패턴을 사용할 수 있습니다.
따라서 Action Cable은 Unicorn, Puma, Passenger와 같은 인기 있는 서버들과 함께 동작합니다.
11 테스팅
Action Cable 기능을 테스트하는 방법에 대한 자세한 설명은 테스팅 가이드에서 확인할 수 있습니다.