실시간 뷰 컴포넌트 구축 및 배포

GitLab은 사용자 입력을 수용하고 상태 변화를 사용자에게 반영하는 개별 뷰 컴포넌트를 통해 인터랙티브한 사용자 경험을 제공합니다. 예를 들어, Merge Request 페이지에서는 사용자가 승인, 댓글 작성, CI/CD 파이프라인 상호작용 등을 할 수 있습니다.

그러나 GitLab은 종종 상태 업데이트를 적시에 반영하지 않습니다.

이로 인해 페이지 일부는 사용자가 페이지를 새로 고칠 때까지 업데이트되지 않는 오래된 데이터를 표시합니다.

이를 해결하기 위해 GitLab은 웹소켓을 통해 실시간으로 상태 업데이트를 수신할 수 있는 기술 및 프로그래밍 API를 도입하였습니다.

다음 문서는 GitLab Ruby on Rails 서버에서 실시간으로 업데이트를 수신하는 뷰 컴포넌트를 구축하고 배포하는 방법을 알려줍니다.

참고: Action Cable과 GraphQL 구독은 진행 중이며 활성 개발 중에 있습니다.

개발자는 사용 사례를 평가하여 이 도구들이 적합한지 확인해야 합니다.

확신이 없는 경우, #f_real-time 내부 Slack 채널에서 도움을 요청하세요.

실시간 뷰 컴포넌트 구축

전제 조건:

다음을 읽어보세요:

GitLab에서 실시간 뷰 컴포넌트를 구축하려면:

  • GitLab 프론트엔드에서 Apollo 구독과 통합된 Vue 컴포넌트를 만들어야 합니다.
  • GitLab Ruby on Rails 백엔드에서 GraphQL 구독을 추가하고 트리거해야 합니다.

Apollo 구독과 Vue 컴포넌트 통합

참고: 현재 우리의 실시간 스택은 클라이언트 코드가 Vue를 렌더링 계층으로 사용하고 Apollo를 상태 및 네트워킹 계층으로 사용한다고 가정합니다.

아직 Vue + Apollo로 마이그레이션되지 않은 GitLab 프론트엔드의 일부를 작업 중이라면, 먼저 해당 작업을 완료하세요.

가상의 IssueView Vue 컴포넌트를 고려해 보겠습니다. 이 컴포넌트는 GitLab의 Issue 데이터를 관찰하고 렌더링합니다.

단순화를 위해, 우리는 이 컴포넌트가 이슈의 제목과 설명을 렌더링하는 것만 한다고 가정합니다:

import issueQuery from '~/issues/queries/issue_view.query.graqhql';

export default {
  props: {
    issueId: {
      type: Number,
      required: false,
      default: null,
    },
  },
  apollo: {
    // Apollo 쿼리 객체의 이름. `data`에 바인딩된 필드 이름과 일치해야 합니다.
    issue: {
      // 초기 페치에 사용되는 쿼리.
      query: issueQuery,
      // 초기 페치 쿼리에 사용되는 바인딩 인수.
      variables() {
        return {
          iid: this.issueId,
        };
      },
      // 응답 데이터를 뷰 속성에 매핑합니다.
      update(data) {
        return data.project?.issue || {};
      },
    },
  },
  // 반응형 Vue 컴포넌트 데이터. Apollo는 쿼리가 반환되거나 구독이 작동할 때 이를 업데이트합니다.
  data() {
    return {
      issue: {}, // 뷰가 로드되는 동안 초기 상태를 반환하는 것이 좋습니다.
    };
  },
};

// <template> 코드는 이 논의와 관련이 없어 간략히 생략되었습니다.

쿼리는 다음과 같이 정의되어야 합니다:

  • app/assets/javascripts/issues/queries/issue_view.query.graqhql에 정의되어야 합니다.
  • 다음 GraphQL 작업을 포함해야 합니다:
query gitlabIssue($iid: String!) {
  # 여기에서 경로를 하드코딩하는 것은 단지 설명을 위한 것입니다. 실제로는 하지 마세요.
  project(fullPath: "gitlab-org/gitlab") {
    issue(iid: $iid) {
      title
      description
    }
  }
}

현재까지 이 뷰 컴포넌트는 데이터로 자신을 채우기 위한 초기 페치 쿼리만 정의합니다.

이것은 뷰에 의해 시작된 HTTP POST 요청으로 전송되는 일반적인 GraphQL query 작업입니다.

서버에서의 후속 업데이트는 이 뷰를 오래된 상태로 만들 수 있습니다.

서버에서 업데이트를 받으려면 다음을 수행해야 합니다:

  1. GraphQL 구독 정의를 추가합니다.
  2. 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:
    • 프론트엔드에서 발생합니다.
    • 엔티티가 일반적으로 URL에서 참조되는 방식인 내부 ID(iid, 숫자)를 사용합니다. 내부 ID는 포함된 네임스페이스에 상대적이므로(이 예제에서 project), 쿼리를 fullPath 아래에 중첩해야 합니다.
  • subscription:
    • 프론트엔드에서 백엔드로 향하는 요청으로, 향후 업데이트를 수신합니다.
    • 구성 요소는:
      • 구독 자체를 설명하는 연산 이름(issueUpdatedSubscription 이 예시에서).
      • 중첩된 이벤트 쿼리(issueUpdated 이 예시에서). 중첩된 이벤트 쿼리는:
        • 동일한 이름의 GraphQL 트리거를 실행할 때, 구독에서 사용된 이벤트 이름은 백엔드에서 사용되는 트리거 필드와 일치해야 합니다.
        • 숫자 내부 ID 대신 리소스를 청정하게 식별하는 방법인 글로벌 ID 문자열을 사용합니다. 더 많은 정보는 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 {
  // 이전과 같습니다.
  // ...
  apollo: {
    issue: {
      // 이전과 같습니다.
      // ...
      // 이 Apollo 훅은 실시간 푸시를 활성화합니다.
      subscribeToMore: {
        // 향후 업데이트를 반환하는 구독 연산.
        document: issueUpdatedSubscription,
        // 구독 연산에 사용되는 인수 바인딩.
        variables() {
          return {
            iid: this.issueId,
          };
        },
        // 구독이 비활성화되어야 하는지 true|false를 반환하는 구현.
        // 기능 플래그를 사용할 때 유용합니다.
        skip() {
          return this.shouldSkipRealTimeUpdates;
        },
      },
    },
  },
  // 이전과 같습니다.
  // ...
  computed: {
    shouldSkipRealTimeUpdates() {
      return false; // 여기서 기능 플래그를 확인할 수 있습니다.
    },
  },
};

이제 Apollo를 통해 WebSocket 연결을 통해 업데이트를 수신하도록 뷰 컴포넌트를 활성화할 수 있습니다.

다음으로는 백엔드에서 이벤트가 트리거되어 프론트엔드로 푸시 업데이트를 시작하는 방법을 다룹니다.

GraphQL 구독 트리거

WebSocket에서 업데이트를 수신할 수 있는 보기 구성 요소를 작성하는 것은 이야기의 반에 불과합니다.

GitLab Rails 애플리케이션에서는 다음 단계를 수행해야 합니다:

  1. GraphQL::Schema::Subscription 클래스를 구현합니다. 이 클래스는:
    • 프론트엔드에서 전송된 subscription 작업을 해결하기 위해 graphql-ruby에 의해 사용됩니다.
    • 구독이 필요로 하는 인수와 호출자에게 반환되는 페이로드를 정의합니다.
    • 호출자가 이 구독을 생성할 수 있는 권한이 있는지 확인하기 위해 필요한 비즈니스 논리를 실행합니다.
  2. Types::SubscriptionType 클래스에 새로운 field를 추가합니다. 이 필드는 Vue 구성 요소와 통합할 때 사용되는 이벤트 이름을 GraphQL::Schema::Subscription 클래스에 매핑합니다.

  3. 해당 GraphQL 트리거를 실행하는 이벤트 이름과 일치하는 메서드를 GraphqlTriggers에 추가합니다.

  4. 서비스 또는 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입니다.'

    def authorized?(issue_id:)
      issue = force(GitlabSchema.find_by_gid(issue_id))

      unauthorized! unless issue && Ability.allowed?(current_user, :read_issue, issue)

      true
    end
  end
end

이 새로운 클래스를 생성할 때:

  • 모든 구독 유형이 Subscriptions::BaseSubscription에서 상속되도록 합니다.
  • 구독 쿼리가 액세스할 수 있는 데이터를 나타내기 위해 적절한 payload_type을 사용하거나 노출할 개별 field를 정의합니다.
  • 클라이언트가 구독할 때마다 호출되는 subscribeupdate 후크를 정의할 수도 있습니다. 이러한 방법을 사용하는 방법은 공식 문서를 참조하세요.
  • 필요한 권한 검사를 수행하기 위해 authorized?를 구현합니다. 이러한 검사는 subscribe 또는 update 호출 시마다 실행됩니다.

GraphQL 구독 클래스에 대해 자세히 알아보려면 공식 문서를 참조하십시오.

구독 연결

새 구독 클래스를 구현하지 않은 경우 이 단계를 건너뛰세요.

새 구독 클래스를 구현한 후에는 이를 SubscriptionTypefield에 매핑해야 실행할 수 있습니다. Types::SubscriptionType 클래스를 열고 새로운 필드를 추가합니다:

module Types
  class SubscriptionType < ::Types::BaseObject
    graphql_name 'Subscription'

    # 기존 필드
    # ...

    field :issue_updated,
      subscription: Subscriptions::IssueUpdated, null: true,
      description: '문제가 업데이트될 때 트리거됩니다.'
  end
end
note
EE 구독을 연결하는 경우 EE::Types::SubscriptionType를 업데이트하십시오.

:issue_updated 인수가 프론트엔드에서 전송된 subscription 요청에서 사용되는 이름(카멜 케이스로 issueUpdated)과 일치하는지 확인해야 graphql-ruby가 어떤 구독자에게 알릴지 알 수 있습니다. 이제 이벤트를 트리거할 수 있습니다.

새로운 트리거 추가

기존 트리거를 재사용할 수 있다면 이 단계를 건너뛰세요.

우리는 이벤트를 트리거하기 간단하게 만들기 위해 GitlabSchema.subscriptions.trigger 둘레에 파사드를 사용합니다.
GraphqlTriggers에 새로운 트리거를 추가하세요:

module GraphqlTriggers
  # 기존 트리거
  # ...

  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에 즉시 전파되어야 합니다.

실시간 뷰 컴포넌트 배포

WebSockets는 GitLab에서 비교적 새로운 기술이며, 이를 대규모로 지원하는 것은 몇 가지 도전 과제를 제공합니다.
그 이유로 인해, 새로운 기능은 아래의 지침을 사용하여 롤아웃되어야 합니다.

실시간 컴포넌트 배송

당신은 프론트엔드와 백엔드에서 동시에 작업할 수 있습니다.
WebSockets를 통한 업데이트는 필요한 백엔드 코드 없이는 시뮬레이트하기 어렵기 때문입니다.

그러나, 변경 사항을 개별 병합 요청(merge request)에서 전송하고 백엔드 변경을 먼저 배포하는 것이 더 안전합니다.
이로 인해 프론트엔드가 이벤트를 구독하기 시작할 때 백엔드가 이미 이를 서비스할 준비가 되어 있습니다.

기존 WebSocket 연결 재사용

기존 연결을 재사용하는 기능은 위험이 최소화됩니다.
기능 플래그 롤아웃을 권장하여 셀프 호스팅 고객에게 더 많은 제어권을 제공합니다.
그러나, GitLab.com을 위한 새로운 연결을 백분율로 롤아웃할 필요는 없습니다.

새로운 WebSocket 연결 도입

GitLab 애플리케이션의 일부에 WebSocket 연결을 도입하는 모든 변경은
열려 있는 연결을 유지보수하는 노드와 Redis 및 기본 데이터베이스와 같은 다운스트림 서비스에
일부 확장성 위험이 발생합니다.

피크 연결 수 추정

GitLab.com에서 완전히 활성화된 첫 번째 실시간 기능은
실시간 할당자였습니다.
문제 페이지에 대한 피크 처리량을 피크 동시 WebSocket 연결과 비교하여
초당 1 요청이 약 4200 WebSocket 연결을 추가한다는 것을 대략적으로 추정할 수 있습니다.

새로운 기능이 미칠 영향을 이해하기 위해
원천 페이지로부터의 피크 처리량(RPS)을 합산(n)하고 아래의 공식을 적용하세요:

(n * 4200) / peak_active_connections

현재 활성 연결은
이 Grafana 차트에서 볼 수 있습니다.

이 계산은 대략적이며, 새로운 기능이 배포됨에 따라 수정되어야 합니다.
기존 용량의 비율로서 지원해야 하는 용량의 대략적인 추정을 제공합니다.

단계적 롤아웃

현재 포화 상태와 필요한 새로운 연결 비율에 따라
변경 사항을 지원하기 위해 새로운 용량을 제공해야 할 수 있습니다.
Kubernetes는 대부분의 경우 이 일을 상대적으로 쉽게 만들지만, 다운스트림 서비스에 대한 위험은 여전히 존재합니다.

이를 완화하기 위해, 새로운 WebSocket 연결을 설정하는 코드는
기능 플래그가 설정되고 기본값이 off로 설정되도록 해야 합니다.
기능 플래그의 신중하고 백분율 기반의 롤아웃은
WebSocket 대시보드에서 영향을 관찰할 수 있도록 보장합니다.

  1. 기능 플래그 롤아웃
    이슈를 생성하세요.

  2. 우리가 기대하는 일이 무엇인지 섹션에 필요한 새로운 연결 수를 추가하세요.

  3. 백분율 기반의 롤아웃 계획을 추정하기 위해 계획 및 확장성 팀의 구성원을 추가하세요.

이전 호환성

기능 플래그 롤아웃 기간 동안과 그 이후로도,

실시간 기능은 이전 호환성이 있어야 하며, 최소한 우아하게 축소되어야 합니다.

모든 고객이 Action Cable을 활성화하고 있는 것은 아니며, Action Cable을 기본으로 활성화하기 전에 추가 작업이 필요합니다.

실시간을 요구사항으로 만드는 것은 중단된 변경을 나타내므로, 이를 수행할 다음 기회는 버전 15.0입니다.

GitLab.com의 실시간 인프라

GitLab.com에서는 WebSocket 연결이 전용 인프라에서 제공되며,

정기적인 웹 플릿과 완전히 분리되어 Kubernetes와 함께 배포됩니다.

이로 인해 요청을 처리하는 노드에 대한 위험은 제한되지만 공유 서비스에는 해당되지 않습니다.

WebSockets Kubernetes 배포에 대한 자세한 내용은

이 에픽을 참조하세요.

GitLab 실시간 스택 심층 분석

서버에서 시작된 푸시가 네트워크를 통해 전파되고

사용자 상호작용 없이 클라이언트에서 뷰 업데이트를 트리거해야 하므로,

실시간 기능은 프론트엔드와 백엔드를 포함한 전체 스택을 살펴보아야만 이해할 수 있습니다.

참고:

역사적 이유로, 클라이언트가 변경 사항을 폴링하는 데 대한 응답으로 서비스 업데이트를 제공하는 컨트롤러 라우트는

realtime_changes라고 불립니다. 이들은 조건부 GET 요청을 사용하며,

이 가이드에서 다루는 실시간 동작과는 관련이 없습니다.

클라이언트에 푸시된 모든 실시간 업데이트는 GitLab Rails 애플리케이션에서 시작됩니다.

이러한 업데이트를 시작하고 서비스하는 데 사용되는 기술은 다음과 같습니다:

GitLab Rails 백엔드에서:

  • Redis PubSub로 구독 상태를 처리합니다.
  • Action Cable로 WebSocket 연결과 데이터 전송을 처리합니다.
  • graphql-ruby로 GraphQL 구독 및 트리거를 구현합니다.

GitLab 프론트엔드에서:

  • Apollo Client로 GraphQL 요청, 라우팅 및 캐싱을 처리합니다.
  • Vue.js로 실시간으로 업데이트되는 뷰 컴포넌트를 정의하고 렌더링합니다.

다음 그림은 이러한 계층 간 데이터가 어떻게 전파되는지를 설명합니다.

Redis PubSubAction Cable/GraphQLRails/GraphQLApollo ClientVue ComponentRedis PubSubAction Cable/GraphQLRails/GraphQLApollo ClientVue ComponentFetch initial data for this viewSubscribe to future updates for this viewpartime passesloop[For each subscriber]injectedHTTP GET /-/cableHijack TCP connectionSUBSCRIBE(client)channel subscriptionHTTP 101: Switching Protocolsquery(gql)HTTP POST /api/graphql (initial query)initial query responsecache and/or transform responsetrigger updatere-rendersubscribeToMore(event, gql)WS: subscribe(event, query)SUBSCRIBE(event)event subscriptionconfirm_subscriptiontrigger eventPUBLISH(event)subscriptionsrun GQL queryPUBLISH(client, query_result)callbackWS: push query resultcache and/or transform responsetrigger updatere-render

이후 섹션에서는 이 스택의 각 요소를 자세히 설명합니다.

액션 케이블과 웹소켓

Action Cable은 Ruby on Rails에 WebSocket 지원을 추가하는 라이브러리입니다.

웹소켓은 단일 TCP 연결을 통해 양방향 통신을 수행함으로써 기존의 HTTP 기반 서버와 애플리케이션을 향상시키기 위한 HTTP 친화적인 솔루션으로 개발되었습니다.

클라이언트는 먼저 서버에 보통의 HTTP 요청을 보내 연결을 웹소켓으로 업그레이드해 달라고 요청합니다.

성공하면 클라이언트와 서버 모두 동일한 TCP 연결을 사용하여 데이터를 양방향으로 보내고 받을 수 있습니다.

웹소켓 프로토콜은 전송되는 데이터의 인코딩이나 구조를 규정하지 않기 때문에, 이러한 문제를 처리하는 Action Cable과 같은 라이브러리가 필요합니다. Action Cable:

  • HTTP에서 웹소켓 프로토콜로의 초기 연결 업그레이드를 처리합니다. 이후 ws:// 스킴을 사용하는 요청은 Action Pack이 아닌 Action Cable 서버에서 처리됩니다.

  • 웹소켓을 통해 전송되는 데이터가 인코딩되는 방식을 정의합니다. Action Cable은 이를 JSON으로 지정합니다. 이는 애플리케이션이 데이터를 Ruby 해시로 제공할 수 있게 하며 Action Cable은 이를 JSON에서 직렬화 및 역직렬화합니다.

  • 클라이언트의 연결 및 연결 해제, 클라이언트 인증을 처리하는 콜백 후크를 제공합니다.

  • 게시/구독 및 원격 프로시저 호출을 구현할 수 있는 개발자 추상화로서 ActionCable::Channel을 제공합니다.

Action Cable은 어떤 클라이언트가 어떤 ActionCable::Channel에 구독하고 있는지를 추적하기 위해 서로 다른 구현을 지원합니다. GitLab에서는 Redis 어댑터를 사용하며, 이는 분산 메시지 버스로서 Redis PubSub 채널을 사용합니다.

서로 다른 Puma 인스턴스에서 동일한 Action Cable 채널에 다른 클라이언트가 연결할 수 있기 때문에 공유 스토리지가 필요합니다.

note
Action Cable 채널과 Redis PubSub 채널을 혼동하지 마세요. Action Cable의 Channel 객체는 웹소켓 연결을 통해 오가는 다양한 종류의 데이터를 분류하고 처리하기 위한 프로그래밍 추상화입니다.

Action Cable에서 기본 PubSub 채널은 방송(broadcasting)이라고 하며, 클라이언트와 방송 간의 연관은 구독(subscription)이라고 합니다. 특히 각 Action Cable Channel에 대해 많은 방송(PubSub 채널)과 구독이 있을 수 있습니다.

Action Cable은 Channel API를 통해 다양한 종류의 동작을 표현할 수 있게 해주며, 모든 Channel에 대한 업데이트가 동일한 웹소켓 연결을 사용할 수 있기 때문에 각 GitLab 페이지에 대해 실시간 동작을 부여하기 위해 단일 웹소켓 연결만을 설정해야 합니다.

GitLab 페이지에서 실시간 업데이트를 구현하기 위해, 우리는 개별 Channel 구현을 작성하지 않습니다. 대신 모든 GitLab 페이지가 구독하는 GraphqlChannel을 제공합니다.

GraphQL 구독: 백엔드

GitLab은 클라이언트가 GraphQL 쿼리를 사용하여 서버에서 구조화된 데이터를 요청할 수 있도록 GraphQL을 지원합니다. 우리가 GraphQL을 채택한 이유에 대해서는 GitLab GraphQL 개요를 참조하세요.

GitLab 백엔드에서의 GraphQL 지원은 graphql-ruby 젬에 의해 제공됩니다.

일반적으로, GraphQL 쿼리는 표준 요청-응답 주기를 따르는 클라이언트에서 시작되는 HTTP POST 요청입니다.

실시간 기능을 위해 우리는 대신 publish/subscribe 패턴의 구현인 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 프론트엔드가 JavaScript를 실행하고 Ruby가 아니라서, 클라이언트에서 서버로 GraphQL 쿼리, 변형 및 구독을 보내기 위해 다른 GraphQL 구현이 필요합니다.

이를 위해 Apollo를 사용합니다.

Apollo는 JavaScript에서 GraphQL의 포괄적인 구현이며 apollo-serverapollo-client 뿐만 아니라 추가 유틸리티 모듈로 나뉩니다. Ruby 백엔드를 실행하기 때문에 apollo-server 대신 apollo-client를 사용합니다.

이것은 다음을 간소화합니다:

  • 네트워킹, 연결 관리 및 요청 라우팅.
  • 클라이언트 측 상태 관리 및 응답 캐싱.
  • 브리지 모듈을 사용하여 보기 컴포넌트와 GraphQL 통합.

참고: Apollo Client 문서를 읽을 때, React.js가 뷰 렌더링에 사용된다고 가정합니다. GitLab에서는 React.js를 사용하지 않습니다. 우리는 Vue.js를 사용하며, Vue.js 어댑터를 통해 Apollo와 통합합니다.

Apollo는 다음을 정의하는 함수와 후크를 제공합니다:

  • 뷰가 쿼리, 변형 또는 구독을 보내는 방법.
  • 응답이 어떻게 처리되어야 하는지.
  • 응답 데이터가 어떻게 캐시되는지.

진입 지점은 ApolloClient로, 이는 GraphQL 클라이언트 객체입니다:

  • 단일 페이지의 모든 뷰 컴포넌트 간에 공유됩니다.
  • 모든 뷰 컴포넌트가 내부적으로 서버와 통신하는 데 사용합니다.

다양한 유형의 요청이 어떻게 라우팅될지 결정하기 위해, Apollo는 ApolloLink 추상화를 사용합니다. 구체적으로,

실시간 서버 구독과 다른 GraphQL 요청을 ActionCableLink를 사용하여 분리합니다. 이것은:

  • Action Cable에 대한 WebSocket 연결을 설정합니다.
  • 서버 푸시를 클라이언트의 Observable 이벤트 스트림에 매핑하여 뷰가 자신을 업데이트할 수 있도록 구독하게 합니다.

Apollo와 Vue.js에 대한 자세한 내용은 GitLab GraphQL 개발 가이드를 참조하세요.