SQL Connect에서 실시간 업데이트 가져오기

클라이언트 코드는 쿼리를 구독하여 쿼리 결과가 변경될 때 실시간 업데이트를 받을 수 있습니다.

시작하기 전에

  • , Apple 플랫폼, Flutter 문서에 설명된 대로 프로젝트의 SDK 생성을 설정합니다.

    • 생성된 모든 SDK에 대해 클라이언트 측 캐싱을 사용 설정해야 합니다. 특히 모든 SDK 구성에는 다음과 같은 선언이 포함되어야 합니다.
    clientCache:
      maxAge: 5s
      storage: ... # Optional.
    
  • 앱 클라이언트는 최신 버전의 SQL Connect 핵심 SDK를 사용해야 합니다.

    • Apple: Swift용 Firebase SQL Connect SDK 버전 11.12.0 이상
    • 웹: JavaScript SDK 버전 12.12.0 이상
    • Flutter: firebase_data_connect 버전 0.3.0 이상
  • Firebase CLI 버전 15.14.0 이상을 사용하여 클라이언트 SDK를 재생성합니다.

쿼리 결과 구독

쿼리 결과를 변경할 때 응답하도록 쿼리를 구독할 수 있습니다. 예를 들어 프로젝트에 다음과 같은 스키마와 작업이 정의되어 있다고 가정해 보겠습니다.

# dataconnect/schema/schema.gql

type Movie @table(key: "id") {
  id: UUID! @default(expr: "uuidV4()")
  title: String!
  releaseYear: Int
  genre: String
  description: String
  averageRating: Int
}
# dataconnect/connector/operations.gql

query GetMovieById($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    id
    title
    releaseYear
    genre
    description
  }
}

mutation UpdateMovie(
  $id: UUID!,
  $genre: String!,
  $description: String!
) {
  movie_update(id: $id,
    data: {
      genre: $genre
      description: $description
    })
}

GetMovieById 실행 결과의 변경사항을 구독하려면 다음을 실행하세요.

import { subscribe, DataConnectError, QueryResult } from 'firebase/data-connect';
import { getMovieByIdRef, GetMovieByIdData, GetMovieByIdVariables } from '@dataconnect/generated';

const queryRef = getMovieByIdRef({ id: "<MOVIE_ID>" });

// Called when receiving an update.
const onNext = (result: QueryResult<GetMovieByIdData, GetMovieByIdVariables>) => {
  console.log("Movie <MOVIE_ID> updated", result);
}

const onError = (err?: DataConnectError) => {
  console.error("received error", err);
}

// Called when unsubscribing or when the subscription is automatically released.
const onComplete = () => {
  console.log("subscription complete!");
}

const unsubscribe = subscribe(queryRef, onNext, onError, onComplete);

웹 (React)

import { subscribe, QueryResult } from 'firebase/data-connect';
import { getMovieByIdRef, GetMovieByIdData, GetMovieByIdVariables } from '@dataconnect/generated';
import { useState, useEffect } from "react";

export const MovieInfo = ({ id: movieId }: { id: string }) => {
  const [movieInfo, setMovieInfo] = useState<GetMovieByIdData>();
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const queryRef = getMovieByIdRef({ id: movieId });

    function updateUi(result: QueryResult<GetMovieByIdData, GetMovieByIdVariables>): void {
      setMovieInfo(result.data);
      setLoading(false);
    }

    const unsubscribe = subscribe(
      queryRef,
      updateUi,
      (err) => {
        setError(err ?? new Error("Unknown error occurred"));
        setLoading(false);
      }
    );

    return () => unsubscribe();
  }, [movieId]);

  if (loading)
    return <div>Loading movie details...</div>;
  if (error || !movieInfo || !movieInfo.movie)
    return <div>Error loading movie details: {error?.message}</div>;

  return (
    <div>
      <h2>{movieInfo.movie.title} ({movieInfo.movie.releaseYear})</h2>
      <ul>
        <li>Genre: {movieInfo.movie.genre}</li>
        <li>Description: {movieInfo.movie.description}</li>
      </ul>
    </div>
  );
};

SQL Connect는 TanStack을 사용한 캐싱 및 실시간 구독도 지원합니다. connector.yaml 파일에서 react: true 또는 angular: true를 지정하면 SQL Connect에서 TanStack을 사용하여 React 또는 Angular용 바인딩을 생성합니다.

이러한 바인딩은 SQL Connect의 내장 실시간 지원과 함께 작동할 수 있지만 약간의 어려움이 있습니다. TanStack 기반 바인딩 또는 SQL Connect의 내장 실시간 지원 중 하나를 사용하는 것이 좋습니다. 둘 다는 안 됩니다.

SQL Connect 자체 실시간 구현에는 TanStack 바인딩에 비해 몇 가지 장점이 있습니다.

  • 정규화된 캐싱: SQL Connect정규화된 캐싱을 구현하여 쿼리 수준 캐싱에 비해 데이터 일관성과 메모리 및 네트워크 효율성을 개선합니다. 정규화된 캐싱을 사용하면 앱의 한 영역에서 항목이 업데이트될 경우 해당 항목을 사용하는 다른 영역에서도 업데이트됩니다.
  • 원격 무효화: SQL Connect는 구독된 모든 기기에서 캐시된 항목을 원격으로 무효화할 수 있습니다.

TanStack을 사용하지 않으려면 connector.yaml 파일에서 react: trueangular: true 설정을 삭제해야 합니다.

iOS

struct ListMovieView: View {
    // QueryRef has the Observable attribute, so its properties will
    // automatically trigger updates on changes.
    private var queryRef = connector.listMoviesByGenreQuery.ref(genre: "Sci-Fi")

    // Store the handle to unsubscribe from query updates.
    @State private var querySub: AnyCancellable?

    var body: some View {
        VStack {
            // Use the query results in a View.
            ForEach(queryRef.data?.movies ?? [], id: \.self.id) { movie in
                    Text(movie.title)
                }
        }
        .onAppear {
            // Subscribe to the query for updates using the Observable macro.
            Task {
                do {
                    querySub = try await queryRef.subscribe().sink { _ in }
                } catch {
                    print("Error subscribing to query: \(error)")
                }
            }
        }
        .onDisappear {
          querySub?.cancel()
        }
    }
}

Flutter

프로젝트의 생성된 SDK를 가져옵니다.

import 'package:flutter_app/dataconnect_generated/generated.dart';

그런 다음 쿼리 참조에서 subscribe() 메서드를 호출합니다.

final queryRef = MovieConnector.instance.getMovieById(id: "<MOVIE_ID>").ref();
final subscription = queryRef.subscribe().listen((result) {
  final movie = result.data.movie;
  if (movie != null) {
    // Execute your logic to update the UI with the refreshed movie information.
    updateUi(movie.title);
  }
});

업데이트를 중지하려면 subscription.cancel()를 호출하면 됩니다.

앞의 예와 같이 쿼리를 구독하면 특정 쿼리의 결과가 변경될 때마다 업데이트가 제공됩니다. 예를 들어 다른 클라이언트가 내가 구독한 동일한 ID에 UpdateMovie 변이를 실행하면 업데이트가 표시됩니다.

암시적 쿼리 새로고침 신호

위의 예에서는 작업에 추가 수정 없이 쿼리를 구독하고 실시간 업데이트를 받을 수 있었습니다. 특히 UpdateMovie 변이가 GetMovieById 쿼리의 결과에 영향을 줄 수 있다고 지정할 필요가 없었습니다.

이는 GetMovieById 쿼리가 UpdateMovie 뮤테이션에서 갱신 신호를 암시적으로 가져오기 때문에 가능합니다. 암시적 새로고침 신호는 작성할 수 있는 쿼리 및 변이의 하위 집합 간에 전송됩니다.

쿼리기본 키로 단일 항목 조회를 실행하는 경우 기본 키로도 식별되는 동일한 항목에 쓰는 변이는 암시적으로 새로고침 신호를 트리거합니다.

  • _insert, _insertMany
  • _upsert, _upsertMany
  • _update
  • _delete

_deleteMany_updateMany는 새로고침 신호를 전송하지 않습니다.

이전 예에서 GetMovieById 쿼리는 ID(movie(id: $id))로 단일 영화를 조회하고 UpdateMovie 변이는 ID (movie_update(id: $id, ...))로 지정된 단일 영화를 업데이트하므로 쿼리는 암시적 새로고침을 활용할 수 있습니다.

Firebase Authentication 사용자의 UID와 같은 알려진 값을 기반으로 키를 지정하는 경우 삽입 및 upsert 작업으로 인해 암시적 새로고침 신호가 트리거될 수 있습니다.

예를 들어 다음과 같은 쿼리를 생각해 보겠습니다.

query GetExtendedProfileByUser @auth(level: USER) {
  profile(key: { id_expr: "auth.uid" }) {
    id
    status
    photoUrl
    socialLink
  }
}

쿼리는 다음과 같은 변이에서 갱신 신호를 암시적으로 수신합니다.

mutation UpsertExtendedProfile($status: String, $photoUrl: String, $socialLink: String) @auth(level: USER) {
  profile_upsert(
    data: {
      id_expr: "auth.uid"
      status: $status
      photoUrl: $photoUrl
      socialLink: $socialLink
    }
  ) {
    id
    status
    photoUrl
    socialLink
  }
}

쿼리나 변형이 더 복잡한 경우 쿼리 새로고침이 필요한 조건을 지정해야 합니다. 다음 섹션에서 방법을 알아보세요.

명시적 쿼리 새로고침 신호

쿼리에 대한 변형에 의해 암시적으로 전송되는 새로고침 신호 외에도 쿼리가 새로고침 신호를 수신해야 하는 시점을 명시적으로 지정할 수 있습니다. @refresh 지시어로 쿼리에 주석을 달면 됩니다.

쿼리가 자동 새로고침을 위한 특정 기준 (위 참고)을 충족하지 않는 경우 @refresh 지시어를 사용해야 합니다. 이 지시어를 포함해야 하는 쿼리의 예는 다음과 같습니다.

  • 항목 목록을 검색하는 쿼리
  • 다른 테이블에서 조인을 실행하는 쿼리
  • 집계 쿼리
  • 네이티브 SQL을 사용하는 쿼리
  • 맞춤 리졸버를 사용하는 쿼리

다음 두 가지 방법으로 새로고침 정책을 지정할 수 있습니다.

시간 기반 간격

고정된 시간 간격으로 쿼리를 새로고침합니다.

예를 들어 매우 활발한 사용자층으로 인해 영화의 누적 평점이 특히 영화가 개봉된 후 매분 여러 번 업데이트될 수 있다고 가정해 보겠습니다. 평점이 변경될 때마다 쿼리를 새로고침하는 대신 몇 초마다 쿼리를 새로고침하여 여러 변이의 누적 결과를 반영하는 업데이트를 가져올 수 있습니다.

# dataconnect/connector/operations.gql

query GetMovieRating($id: UUID!) @auth(level: PUBLIC) @refresh(every: {seconds: 30}) {
  movie(id: $id) {
    id
    averageRating
  }
}

변이 실행

특정 변이가 실행될 때 쿼리를 새로고침합니다. 이 접근 방식을 사용하면 쿼리 결과를 변경할 수 있는 변형을 명시적으로 알 수 있습니다.

예를 들어 특정 영화가 아닌 여러 영화에 관한 정보를 가져오는 쿼리가 있다고 가정해 보겠습니다. 이 쿼리는 뮤테이션이 영화 레코드를 업데이트할 때마다 새로고침되어야 합니다.

query ListMovies($offset: Int)
    @auth(level: PUBLIC, insecureReason: "Anyone can list all movies.")
    @refresh(onMutationExecuted: { operation: "UpdateMovie" }) {
  movies(limit: 10, offset: $offset) {
    id
    title
    releaseYear
    genre
    description
  }
}

변형이 쿼리 새로고침을 트리거하기 위해 충족해야 하는 CEL 표현식 조건을 지정할 수도 있습니다.

이렇게 하는 것이 좋습니다. 조건을 지정할 때 더 정확할수록 불필요한 데이터베이스 리소스가 적게 소비되고 앱의 응답성이 높아집니다.

예를 들어 지정된 장르의 영화만 나열하는 쿼리가 있다고 가정해 보겠습니다. 이 쿼리는 뮤테이션이 동일한 장르의 영화를 업데이트할 때만 새로고침되어야 합니다.

query ListMoviesByGenre($genre: String, $offset: Int)
    @auth(level: PUBLIC, insecureReason: "Anyone can list movies.")
    @refresh(onMutationExecuted: {
      operation: "UpdateMovie",
      condition: "request.variables.genre == mutation.variables.genre"
    }) {
  movies(
      where: { genre: { eq: $genre } },
      limit: 10,
      offset: $offset) {
    id
    title
    releaseYear
    genre
    description
  }
}

@refresh 조건의 CEL 바인딩

onMutationExecutedcondition 표현식은 다음 두 컨텍스트에 액세스할 수 있습니다.

request

구독 중인 쿼리의 상태입니다.

Binding 설명
request.variables 쿼리에 전달된 변수 (예: request.variables.id)
request.auth.uid Firebase Authentication 쿼리를 실행한 사용자의 UID
request.auth.token 쿼리를 실행한 사용자의 Firebase Authentication 토큰 클레임 사전
mutation

실행된 변이의 상태입니다.

Binding 설명
mutation.variables 변이가 전달된 변수 (예: mutation.variables.movieId)
mutation.auth.uid Firebase Authentication 변이를 실행한 사용자의 UID
mutation.auth.token 변이를 실행한 사용자의 Firebase Authentication 토큰 클레임 사전
일반 패턴
# Refresh only when the mutation targets the same entity
"request.variables.id == mutation.variables.id"

# Refresh only when the same user who subscribed makes a change
"request.auth.uid == mutation.auth.uid"

# Refresh when a specific field value matches a condition
"request.auth.uid == mutation.auth.uid && mutation.variables.status == 'PUBLISHED'"

# Refresh when a specific flag is set in the mutation
"mutation.variables.isPublic == true"

여러 @refresh 지시어

쿼리에서 @refresh 지시어를 여러 번 지정하여 @refresh 지시어 중 하나에서 지정한 기준이 충족될 때마다 새로고침을 트리거할 수 있습니다.

예를 들어 다음 쿼리는 30초마다 새로고침되며 지정된 변이 중 하나가 실행될 때도 새로고침됩니다.

query ListMovies($offset: Int)
    @auth(level: PUBLIC, insecureReason: "Anyone can list all movies.")
    @refresh(every: {seconds: 30})
    @refresh(onMutationExecuted: { operation: "UpdateMovie" })
    @refresh(onMutationExecuted: { operation: "BulkUpdateMovies" }) {
  movies(limit: 10, offset: $offset) {
    id
    title
    releaseYear
    genre
    description
  }
}

참조

자세한 예는 @refresh 지시문 참조를 참고하세요.