실시간 뷰 컴포넌트 빌드 및 배포
GitLab v19.1GitLab은 사용자 입력을 수신하고 상태 변경을 사용자에게 반영하는 개별 뷰 컴포넌트를 통해 대화형 사용자 경험을 제공합니다. 그러나 GitLab은 상태 업데이트를 적시에 반영하지 못하는 경우가 많습니다. 이를 해결하기 위해 GitLab은 뷰 컴포넌트가 WebSocket을 통해 실시간으로 상태 업데이트를 받을 수 있도록 하는 기술과 프로그래밍 API를 도입했습니다.
GitLab은 사용자 입력을 수신하고 상태 변경을 사용자에게 반영하는 개별 뷰 컴포넌트를 통해 대화형 사용자 경험을 제공합니다. 예를 들어, 머지 리퀘스트 페이지에서 사용자는 승인하거나, 댓글을 남기거나, CI/CD 파이프라인과 상호작용하는 등 다양한 작업을 할 수 있습니다.
그러나 GitLab은 상태 업데이트를 적시에 반영하지 못하는 경우가 많습니다. 이는 페이지의 일부가 오래된 데이터를 표시하게 되며, 사용자가 페이지를 새로고침한 후에야 업데이트된다는 것을 의미합니다.
이를 해결하기 위해 GitLab은 뷰 컴포넌트가 WebSocket을 통해 실시간으로 상태 업데이트를 받을 수 있도록 하는 기술과 프로그래밍 API를 도입했습니다.
다음 문서에서는 GitLab Ruby on Rails 서버로부터 실시간으로 업데이트를 수신하는 뷰 컴포넌트를 빌드하고 배포하는 방법을 설명합니다.
Action Cable과 GraphQL 구독은 작업 진행 중이며 현재 활발히 개발되고 있습니다.
개발자는 자신의 사용 사례를 평가하여 이 도구들이 적합한지 확인해야 합니다.
확실하지 않은 경우 #f_real-time 내부 Slack 채널에서 도움을 요청하세요.
WebSocket 안전하게 사용하기#
WebSocket은 GitLab에서 비교적 새로운 기술이므로, WebSocket 연결을 사용할 때는 방어적으로 코딩해야 합니다.
하위 호환성#
연결을 임시(ephemeral)적인 것으로 취급하고, 빌드하는 기능이 하위 호환성을 유지하도록 하세요. WebSocket 연결을 사용할 수 없을 때 중요한 기능이 점진적으로 저하되도록 보장하세요.
필요한 백엔드 코드 없이는 WebSocket을 통한 업데이트를 시뮬레이션하기 어렵기 때문에, 프론트엔드와 백엔드를 동시에 작업할 수 있습니다.
그러나 항상 백엔드 변경 사항을 먼저 배포하세요. 특히 새로운 연결이 도입되는 경우, 백엔드와 프론트엔드 변경 사항을 별도의 릴리즈로 패키징하거나 Feature Flag로 롤아웃을 관리하는 것을 강력히 권장합니다.
이렇게 하면 프론트엔드가 이벤트 구독을 시작할 때 백엔드가 이미 이를 처리할 준비가 된 상태가 됩니다.
대규모에서의 새 연결#
새로운 WebSocket 연결을 도입하는 것은 대규모 환경에서 특히 위험합니다. 사이트의 새 영역에 연결을 설정해야 하는 경우, 더 진행하기 전에 새 WebSocket 연결 도입 섹션에 설명된 단계를 수행하세요.
실시간 뷰 컴포넌트 빌드#
전제 조건:
다음을 먼저 읽으세요:
GitLab에서 실시간 뷰 컴포넌트를 빌드하려면 다음을 수행해야 합니다:
-
GitLab 프론트엔드에서 Vue 컴포넌트와 Apollo 구독을 통합합니다.
-
GitLab Ruby on Rails 백엔드에서 GraphQL 구독을 추가하고 트리거합니다.
Vue 컴포넌트와 Apollo 구독 통합#
현재 실시간 스택은 클라이언트 코드가 렌더링 레이어로 Vue를, 상태 및 네트워킹 레이어로 Apollo를 사용하여 빌드된다고 가정합니다. 아직 Vue + Apollo로 마이그레이션되지 않은 GitLab 프론트엔드 부분을 작업하는 경우, 먼저 해당 작업을 완료하세요.
GitLab Issue 데이터를 관찰하고 렌더링하는 가상의 IssueView Vue 컴포넌트를 생각해 보세요.
간단하게, 여기서는 이슈의 제목과 설명만 렌더링한다고 가정합니다:
import issueQuery from '~/issues/queries/issue_view.query.graqhql';
export default {
props: {
issueId: {
type: Number,
required: false,
default: null,
},
},
apollo: {
// Name of the Apollo query object. Must match the field name bound by `data`.
issue: {
// Query used for the initial fetch.
query: issueQuery,
// Bind arguments used for the initial fetch query.
variables() {
return {
iid: this.issueId,
};
},
// Map response data to view properties.
update(data) {
return data.project?.issue || {};
},
},
},
// Reactive Vue component data. Apollo updates these when queries return or subscriptions fire.
data() {
return {
issue: {}, // It is good practice to return initial state here while the view is loading.
};
},
};
// The <template> code is omitted for brevity as it is not relevant to this discussion.
쿼리는 다음을 충족해야 합니다:
-
app/assets/javascripts/issues/queries/issue_view.query.graqhql에 정의되어 있어야 합니다. -
다음 GraphQL 작업을 포함해야 합니다:
query gitlabIssue($iid: String!) {
# We hard-code the path here only for illustration. Don't do this in practice.
project(fullPath: "gitlab-org/gitlab") {
issue(iid: $iid) {
title
description
}
}
}
지금까지 이 뷰 컴포넌트는 데이터를 채우기 위한 초기 페치 쿼리만 정의했습니다.
이는 뷰에서 시작되어 HTTP POST 요청으로 전송되는 일반적인 GraphQL query 작업입니다.
서버에서 이후 발생하는 업데이트는 이 뷰를 오래된 상태로 만들게 됩니다.
서버로부터 업데이트를 받으려면 다음을 수행해야 합니다:
-
GraphQL 구독 정의를 추가합니다.
-
Apollo 구독 훅을 정의합니다.
GraphQL 구독 정의 추가#
구독도 GraphQL 쿼리를 정의하지만, GraphQL subscription 작업 안에 래핑됩니다.
이 쿼리는 백엔드에서 시작되며 결과는 WebSocket을 통해 뷰 컴포넌트로 푸시됩니다.
초기 페치 쿼리와 유사하게, 다음을 수행해야 합니다:
-
구독 파일을
app/assets/javascripts/issues/queries/issue_updated.subscription.graqhql에 정의합니다. -
파일에 다음 GraphQL 작업을 포함합니다:
subscription issueUpdatedSubscription($iid: String!) {
issueUpdated($issueId: IssueID!) {
issue(issueId: $issueId) {
title
description
}
}
}
새 구독을 추가할 때 다음 명명 가이드라인을 사용하세요:
-
구독의 작업 이름은
Subscription으로 끝내거나, GitLab EE 전용인 경우SubscriptionEE로 끝내세요. 예:issueUpdatedSubscription또는issueUpdatedSubscriptionEE. -
구독의 이벤트 이름에는 "발생했다"는 의미의 동작 동사를 사용하세요. 예:
issueUpdated.
구독 정의는 일반 쿼리와 유사하게 보이지만, 이해해야 할 몇 가지 중요한 차이점이 있습니다:
query:
프론트엔드에서 시작됩니다.
-
내부 ID(
iid, 숫자형)를 사용하는데, 이는 엔티티가 URL에서 일반적으로 참조되는 방식입니다. 내부 ID는 포함하는 네임스페이스(이 예에서는project)에 상대적이므로,fullPath아래에 쿼리를 중첩해야 합니다. -
subscription:
미래 업데이트를 받기 위해 프론트엔드에서 백엔드로 보내는 요청입니다.
- 다음으로 구성됩니다:
구독 자체를 설명하는 작업 이름(이 예에서는 issueUpdatedSubscription).
- 중첩된 이벤트 쿼리(이 예에서는
issueUpdated). 중첩된 이벤트 쿼리는:
같은 이름의 GraphQL 트리거가 실행될 때 실행되므로, 구독에서 사용되는 이벤트 이름은 백엔드에서 사용되는 트리거 필드와 일치해야 합니다.
- 숫자형 내부 ID 대신 전역 ID 문자열을 사용하는데, 이것이 GraphQL에서 리소스를 식별하는 선호되는 방법입니다. 자세한 내용은 GraphQL 전역 ID를 참조하세요.
Apollo 구독 훅 정의#
구독을 정의한 후, Apollo의 subscribeToMore 속성을 사용하여 뷰 컴포넌트에 추가합니다:
import issueQuery from '~/issues/queries/issue_view.query.graqhql';
import issueUpdatedSubscription from '~/issues/queries/issue_updated.subscription.graqhql';
export default {
// As before.
// ...
apollo: {
issue: {
// As before.
// ...
// This Apollo hook enables real-time pushes.
subscribeToMore: {
// Subscription operation that returns future updates.
document: issueUpdatedSubscription,
// Bind arguments used for the subscription operation.
variables() {
return {
iid: this.issueId,
};
},
// Implement this to return true|false if subscriptions should be disabled.
// Useful when using feature-flags.
skip() {
return this.shouldSkipRealTimeUpdates;
},
},
},
},
// As before.
// ...
computed: {
shouldSkipRealTimeUpdates() {
return false; // Might check a feature flag here.
},
},
};
이제 Apollo를 통해 WebSocket 연결로 업데이트를 받을 수 있도록 뷰 컴포넌트를 활성화했습니다. 다음으로, 백엔드에서 이벤트가 트리거되어 프론트엔드로 푸시 업데이트를 시작하는 방법을 살펴봅니다.
GraphQL 구독 트리거#
WebSocket에서 업데이트를 받을 수 있는 뷰 컴포넌트를 작성하는 것은 전체 작업의 절반에 불과합니다. GitLab Rails 애플리케이션에서는 다음 단계를 수행해야 합니다:
GraphQL::Schema::Subscription클래스를 구현합니다. 이 클래스는:
graphql-ruby가 프론트엔드에서 보낸 subscription 작업을 처리하는 데 사용됩니다.
-
구독이 취하는 인수와 호출자에게 반환되는 페이로드(있는 경우)를 정의합니다.
-
호출자가 이 구독을 생성할 권한이 있는지 확인하기 위해 필요한 비즈니스 로직을 실행합니다.
-
Types::SubscriptionType클래스에 새field를 추가합니다. 이 필드는 Vue 컴포넌트 통합 시 사용된 이벤트 이름을GraphQL::Schema::Subscription클래스에 매핑합니다. -
이벤트 이름과 일치하는 메서드를
GraphqlTriggers에 추가하여 해당 GraphQL 트리거를 실행합니다. -
서비스 또는 Active Record 모델 클래스를 사용하여 도메인 로직의 일부로 새 트리거를 실행합니다.
구독 구현#
이미 GraphQL::Schema::Subscription으로 구현된 이벤트를 구독하는 경우, 이 단계는 선택 사항입니다.
그렇지 않은 경우, app/graphql/subscriptions/ 아래에 새 클래스를 만들어 새 구독을 구현합니다.
Issue가 업데이트될 때 issueUpdated 이벤트가 발생하는 예시에서 구독 구현은 다음과 같습니다:
module Subscriptions
class IssueUpdated < BaseSubscription
include Gitlab::Graphql::Laziness
payload_type Types::IssueType
argument :issue_id, Types::GlobalIDType[Issue],
required: true,
description: 'ID of the issue.'
def authorized?(issue_id:)
authorize_object_or_gid!(:read_issue, gid: issue_id)
end
end
end
이 새 클래스를 만들 때:
-
모든 구독 타입이
Subscriptions::BaseSubscription을 상속하는지 확인합니다. -
적절한
payload_type을 사용하여 구독된 쿼리가 접근할 수 있는 데이터를 나타내거나, 노출하려는 개별field를 정의합니다. -
클라이언트가 구독하거나 이벤트가 발생할 때마다 호출되는 커스텀
subscribe및update훅을 정의할 수도 있습니다. 이 메서드 사용 방법은 공식 문서를 참조하세요. -
authorized?를 구현하여 필요한 권한 검사를 수행합니다. 이 검사는subscribe또는update를 호출할 때마다 실행됩니다.
GraphQL 구독 클래스에 대한 자세한 내용은 공식 문서를 참조하세요.
구독 연결하기#
새 구독 클래스를 구현하지 않은 경우 이 단계를 건너뜁니다.
새 구독 클래스를 구현한 후, 실행할 수 있도록 SubscriptionType의 field에 해당 클래스를 매핑해야 합니다.
Types::SubscriptionType 클래스를 열고 새 필드를 추가합니다:
module Types
class SubscriptionType < ::Types::BaseObject
graphql_name 'Subscription'
# Existing fields
# ...
field :issue_updated,
subscription: Subscriptions::IssueUpdated, null: true,
description: 'Triggered when an issue is updated.'
end
end
EE 구독을 연결하는 경우, 대신 EE::Types::SubscriptionType을 업데이트하세요.
:issue_updated 인수가 프론트엔드에서 camelCase로 보낸 subscription 요청에서 사용한 이름(issueUpdated)과 일치하는지 확인하세요.
그렇지 않으면 graphql-ruby가 어떤 구독자에게 알려야 하는지 알 수 없습니다. 이제 이벤트가 트리거될 수 있습니다.
새 트리거 추가#
기존 트리거를 재사용할 수 있는 경우 이 단계를 건너뜁니다.
이벤트를 트리거하기 쉽게 만들기 위해 GitlabSchema.subscriptions.trigger를 래핑한 파사드(facade)를 사용합니다.
GraphqlTriggers에 새 트리거를 추가합니다:
module GraphqlTriggers
# Existing triggers
# ...
def self.issue_updated(issue)
GitlabSchema.subscriptions.trigger(:issue_updated, { issue_id: issue.to_gid }, issue)
end
end
트리거가 EE 구독을 위한 것이라면, 대신 EE::GraphqlTriggers를 업데이트하세요.
-
첫 번째 인수
:issue_updated는 이전 단계에서 사용한field이름과 일치해야 합니다. -
인수 해시는 이벤트를 게시할 이슈를 지정합니다. GraphQL은 이 해시를 사용하여 이벤트를 게시할 토픽을 식별합니다.
마지막 단계는 이 트리거 함수를 호출하는 것입니다.
트리거 실행#
이 단계의 구현은 빌드하는 내용에 따라 다릅니다.
이슈의 필드가 변경되는 예시에서는 Issues::UpdateService를 확장하여 GraphqlTriggers.issue_updated를 호출할 수 있습니다.
실시간 뷰 컴포넌트가 이제 기능합니다. 이슈에 대한 업데이트는 이제 즉시 GitLab UI에 전파됩니다.
실시간 컴포넌트 배포#
기존 WebSocket 연결 재사용#
기존 연결을 재사용하는 기능은 최소한의 위험을 수반합니다. self-hosting 고객에게 더 많은 제어권을 제공하기 위해 Feature Flag 롤아웃을 권장합니다. 그러나 GitLab.com에 대해 비율 기반으로 롤아웃하거나 새 연결을 추정할 필요는 없습니다.
새 WebSocket 연결 도입#
GitLab 애플리케이션의 일부에 WebSocket 연결을 도입하는 변경 사항은 개방 연결을 유지하는 노드와 Redis 및 기본 데이터베이스 같은 다운스트림 서비스 모두에 일부 확장성 위험을 수반합니다.
최대 연결 수 추정#
GitLab.com에서 완전히 활성화된 첫 번째 실시간 기능은 실시간 담당자였습니다. 이슈 페이지의 최대 처리량과 동시 WebSocket 연결의 최댓값을 비교함으로써, 페이지에 대한 초당 요청(RPS) 1개당 약 4200개의 WebSocket 연결이 추가된다는 것을 대략적으로 추정할 수 있습니다.
새 기능이 미칠 영향을 이해하려면, 기능이 시작되는 페이지들의 최대 처리량(RPS)을 합산(n)하고 다음 공식을 적용하세요:
(n * 4200) / peak_active_connections
이 계산은 대략적이며, 새 기능이 배포됨에 따라 수정되어야 합니다. 이는 기존 용량의 비율로서 지원되어야 하는 용량의 대략적인 추정치를 제공합니다.
현재 활성 연결은 이 Grafana 차트에서 확인할 수 있습니다.
점진적 롤아웃#
현재 포화도와 필요한 새 연결의 비율에 따라 변경 사항을 지원하기 위해 새 용량을 프로비저닝해야 할 수 있습니다. Kubernetes가 대부분의 경우 이를 비교적 쉽게 만들어주지만, 다운스트림 서비스에 대한 위험은 여전히 존재합니다.
이를 완화하기 위해, 새 WebSocket 연결을 설정하는 코드가 Feature Flag로 설정되어 기본값이 off인지 확인하세요.
Feature Flag의 신중한 비율 기반 롤아웃을 통해 효과를
WebSocket 대시보드에서 관찰할 수 있습니다.
-
Feature Flag 롤아웃 이슈를 생성합니다.
-
What are we expecting to happen 섹션 아래에 예상되는 새 연결 수를 추가합니다.
-
Plan 팀과 Scalability 팀의 멤버를 참여시켜 비율 기반 롤아웃 계획을 수립합니다.
GitLab.com의 실시간 인프라#
GitLab.com에서 WebSocket 연결은 일반 웹 플릿과 완전히 분리되어 Kubernetes로 배포된 전용 인프라에서 제공됩니다. 이는 요청을 처리하는 노드에 대한 위험을 제한하지만, 공유 서비스에 대한 위험은 제한하지 않습니다. WebSocket Kubernetes 배포에 대한 자세한 내용은 이 에픽을 참조하세요.
GitLab 실시간 스택 심층 분석#
서버에서 시작된 푸시가 사용자 상호작용 없이 네트워크를 통해 전파되어 클라이언트에서 뷰 업데이트를 트리거해야 하기 때문에, 실시간 기능은 프론트엔드와 백엔드를 포함한 전체 스택을 살펴봐야만 이해할 수 있습니다.
역사적인 이유로, 클라이언트가 변경 사항을 폴링하는 응답으로 업데이트를 제공하는 컨트롤러 라우트는
realtime_changes라고 불립니다. 이들은 조건부 GET 요청을 사용하며 이 가이드에서 다루는 실시간 동작과는 무관합니다.
클라이언트로 푸시되는 모든 실시간 업데이트는 GitLab Rails 애플리케이션에서 시작됩니다. 다음 기술을 사용하여 이러한 업데이트를 시작하고 처리합니다:
GitLab Rails 백엔드에서:
-
Redis PubSub - 구독 상태 처리
-
Action Cable - WebSocket 연결 및 데이터 전송 처리
-
graphql-ruby- GraphQL 구독 및 트리거 구현
GitLab 프론트엔드에서:
-
Apollo Client - GraphQL 요청, 라우팅 및 캐싱 처리
-
Vue.js - 실시간으로 업데이트되는 뷰 컴포넌트 정의 및 렌더링
다음 그림은 이러한 레이어 간에 데이터가 전파되는 방식을 보여줍니다.
sequenceDiagram participant V as Vue Component participant AP as Apollo Client participant P as Rails/GraphQL participant AC as Action Cable/GraphQL participant R as Redis PubSub AP-->>V: injected AP->>P: HTTP GET /-/cable AC-->>P: Hijack TCP connection AC->>+R: SUBSCRIBE(client) R-->>-AC: channel subscription AC-->>AP: HTTP 101: Switching Protocols par V->>AP: query(gql) Note over AP,P: Fetch initial data for this view AP->>+P: HTTP POST /api/graphql (initial query) P-->>-AP: initial query response AP->>AP: cache and/or transform response AP->>V: trigger update V->>V: re-render and Note over AP,AC: Subscribe to future updates for this view V->>AP: subscribeToMore(event, gql) AP->>+AC: WS: subscribe(event, query) AC->>+R: SUBSCRIBE(event) R-->>-AC: event subscription AC-->>-AP: confirm_subscription end Note over V,R: time passes P->>+AC: trigger event AC->>+R: PUBLISH(event) R-->>-AC: subscriptions loop For each subscriber AC->>AC: run GQL query AC->>+R: PUBLISH(client, query_result) R-->>-AC: callback AC->>-AP: WS: push query result end AP->>AP: cache and/or transform response AP->>V: trigger update V->>V: re-render
이어지는 섹션에서 이 스택의 각 요소를 자세히 설명합니다.
Action Cable과 WebSocket#
Action Cable은 Ruby on Rails에 WebSocket 지원을 추가하는 라이브러리입니다. WebSocket은 단일 TCP 연결을 통한 양방향 통신으로 기존 HTTP 기반 서버 및 애플리케이션을 향상시키기 위한 HTTP 친화적인 솔루션으로 개발되었습니다. 클라이언트는 먼저 서버에 일반 HTTP 요청을 보내어 연결을 WebSocket으로 업그레이드할 것을 요청합니다. 성공하면 동일한 TCP 연결을 클라이언트와 서버 모두 양방향으로 데이터를 송수신하는 데 사용할 수 있습니다.
WebSocket 프로토콜은 전송된 데이터의 인코딩 또는 구조화 방식을 규정하지 않기 때문에, 이러한 문제를 처리하는 Action Cable 같은 라이브러리가 필요합니다. Action Cable은:
-
HTTP에서 WebSocket 프로토콜로의 초기 연결 업그레이드를 처리합니다. 이후
ws://스킴을 사용하는 요청은 Action Pack이 아닌 Action Cable 서버에서 처리됩니다. -
WebSocket을 통해 전송된 데이터의 인코딩 방식을 정의합니다. Action Cable은 JSON으로 지정합니다. 이를 통해 애플리케이션은 데이터를 Ruby Hash로 제공하고 Action Cable이 JSON으로/에서 (역)직렬화합니다.
-
클라이언트 연결 또는 연결 해제와 클라이언트 인증을 처리하는 콜백 훅을 제공합니다.
-
게시/구독 및 원격 프로시저 호출을 구현하기 위한 개발자 추상화로
ActionCable::Channel을 제공합니다.
Action Cable은 어떤 클라이언트가 어떤 ActionCable::Channel을 구독하는지 추적하기 위한 다양한 구현을 지원합니다.
GitLab에서는 분산 메시지 버스로 Redis PubSub 채널을 사용하는 Redis 어댑터를 사용합니다.
서로 다른 클라이언트가 서로 다른 Puma 인스턴스에서 동일한 Action Cable 채널에 연결할 수 있기 때문에 공유 스토리지가 필요합니다.
Action Cable 채널과 Redis PubSub 채널을 혼동하지 마세요. Action Cable Channel 객체는 WebSocket 연결을 통해 전달되는 다양한 종류의 데이터를 분류하고 처리하는 프로그래밍 추상화입니다.
Action Cable에서 기본 PubSub 채널은 대신 브로드캐스팅(broadcasting)이라고 하며, 클라이언트와 브로드캐스팅 간의 연결은 구독(subscription)이라고 합니다.
특히, 각 Action Cable Channel에 대해 많은 브로드캐스팅(PubSub 채널)과 구독이 있을 수 있습니다.
Action Cable은 Channel API를 통해 다양한 종류의 동작을 표현할 수 있고, 모든 Channel에 대한 업데이트가 동일한 WebSocket 연결을 사용할 수 있기 때문에,
GitLab 페이지의 뷰 컴포넌트에 실시간 동작을 추가하기 위해 각 GitLab 페이지에 대해 단일 WebSocket 연결만 설정하면 됩니다.
GitLab 페이지에서 실시간 업데이트를 구현하기 위해 개별 Channel 구현을 작성하지 않습니다.
대신, 푸시 기반 업데이트가 필요한 모든 GitLab 페이지가 구독하는 GraphqlChannel을 제공합니다.
GraphQL 구독: 백엔드#
GitLab은 클라이언트가 GraphQL 쿼리를 사용하여 서버에서 구조화된 데이터를 요청할 수 있도록 GraphQL을 지원합니다.
GraphQL을 채택한 이유는 GitLab GraphQL 개요를 참조하세요.
GitLab 백엔드의 GraphQL 지원은 graphql-ruby 젬으로 제공됩니다.
일반적으로 GraphQL 쿼리는 표준 요청-응답 사이클을 따르는 클라이언트 시작 HTTP POST 요청입니다.
실시간 기능을 위해 대신 게시/구독 패턴의 구현인 GraphQL 구독을 사용합니다.
이 접근 방식에서 클라이언트는 먼저 다음을 포함하여 GraphqlChannel에 구독 요청을 보냅니다:
-
구독
field의 이름(이벤트 이름) -
이 이벤트가 트리거될 때 실행할 GraphQL 쿼리
이 정보는 서버가 이 이벤트 스트림을 나타내는 topic을 생성하는 데 사용됩니다.
토픽은 구독 인수와 이벤트 이름에서 파생된 고유한 이름으로, 이벤트가 트리거될 때 알려야 하는 모든 구독자를 식별하는 데 사용됩니다.
하나 이상의 클라이언트가 동일한 토픽을 구독할 수 있습니다.
예를 들어, issuableAssigneesUpdated:issuableId:<hashed_id>는 주어진 ID를 가진 이슈의 담당자가 변경될 때마다 업데이트를 받으려는 클라이언트가 구독하는 토픽일 수 있습니다.
백엔드는 일반적으로 "이슈가 에픽에 추가됨" 또는 "사용자가 이슈에 할당됨" 같은 도메인 이벤트에 응답하여 구독을 트리거할 책임이 있습니다.
GitLab에서는 서비스 객체 또는 ActiveRecord 모델 객체가 될 수 있습니다.
트리거는 각각의 이벤트 이름과 인수를 사용하여 GitlabSchema.subscriptions.trigger를 호출함으로써 실행되며,
graphql-ruby는 이를 통해 토픽을 파생합니다. 그런 다음 이 토픽의 모든 구독자를 찾고, 각 구독자에 대해 쿼리를 실행하고, 결과를 모든 토픽 구독자에게 푸시합니다.
GraphQL 구독의 기본 전송으로 Action Cable을 사용하기 때문에, 토픽은 앞서 언급했듯이 Redis PubSub 채널을 나타내는 Action Cable 브로드캐스팅으로 구현됩니다. 이는 각 구독자에 대해 두 개의 PubSub 채널이 사용됨을 의미합니다:
-
토픽당 하나의
graphql-event:<namespace>:<topic>채널. 이 채널은 어떤 클라이언트가 어떤 이벤트를 구독하는지 추적하는 데 사용되며 모든 잠재적 클라이언트 간에 공유됩니다.namespace의 사용은 선택 사항이며 비어 있을 수 있습니다. -
클라이언트당 하나의
graphql-subscription:<subscription-id>채널. 이 채널은 각 클라이언트에게 쿼리 결과를 전송하는 데 사용되므로 서로 다른 클라이언트 간에 공유할 수 없습니다.
다음 섹션에서는 GitLab 프론트엔드가 GraphQL 구독을 사용하여 실시간 업데이트를 구현하는 방법을 설명합니다.
GraphQL 구독: 프론트엔드#
GitLab 프론트엔드는 Ruby가 아닌 JavaScript를 실행하기 때문에, 클라이언트에서 서버로 GraphQL 쿼리, 뮤테이션 및 구독을 보내기 위해 다른 GraphQL 구현이 필요합니다. 이를 위해 Apollo를 사용합니다.
Apollo는 JavaScript에서 GraphQL의 포괄적인 구현이며 apollo-server와 apollo-client 및 추가 유틸리티 모듈로 분리됩니다.
Ruby 백엔드를 실행하기 때문에 apollo-server 대신 apollo-client를 사용합니다.
다음을 단순화합니다:
-
네트워킹, 연결 관리 및 요청 라우팅
-
클라이언트 측 상태 관리 및 응답 캐싱
-
브리지 모듈을 사용하여 GraphQL과 뷰 컴포넌트 통합
Apollo Client 문서를 읽을 때, 뷰 렌더링에 React.js가 사용된다고 가정합니다. GitLab에서는 React.js를 사용하지 않습니다. 우리는 Vue.js 어댑터를 사용하여 Apollo와 통합하는 Vue.js를 사용합니다.
Apollo는 다음을 정의하는 함수와 훅을 제공합니다:
-
뷰가 쿼리, 뮤테이션 또는 구독을 보내는 방법
-
응답을 처리하는 방법
-
응답 데이터가 캐시되는 방법
진입점은 ApolloClient로, 이는 GraphQL 클라이언트 객체입니다:
-
단일 페이지의 모든 뷰 컴포넌트 간에 공유됩니다.
-
모든 뷰 컴포넌트가 서버와 통신하기 위해 내부적으로 사용합니다.
다양한 유형의 요청이 라우팅되는 방식을 결정하기 위해 Apollo는 ApolloLink 추상화를 사용합니다.
구체적으로, ActionCableLink를 사용하여 실시간 서버 구독을 다른 GraphQL 요청에서 분리합니다. 이것은:
-
Action Cable에 대한 WebSocket 연결을 설정합니다.
-
서버 푸시를 클라이언트의
Observable이벤트 스트림에 매핑하여, 뷰가 자신을 업데이트하기 위해 구독할 수 있도록 합니다.
Apollo와 Vue.js에 대한 자세한 내용은 GitLab GraphQL 개발 가이드를 참조하세요.