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

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

시작하기 전에

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

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

    • Apple: Swift용 Firebase SQL Connect SDK 버전 11.12.0 이상
    • Android: Firebase SQL Connect SDK 버전 17.3.0 이상 (BoM 버전 34.14.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을 사용하여 캐싱 및 실시간 구독도 지원합니다. 또는 angular: trueconnector.yaml 파일에 지정하면 SQL Connect은 TanStack을 사용하여 React 또는 Angular용 바인딩을 생성합니다.react: true

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

SQL Connect 자체의 실시간 구현은 TanStack 바인딩보다 몇 가지 이점이 있습니다.

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

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

iOS

struct MovieDetailsView: View {
    // QueryRef has the @Observable annotation, so its properties will
    // automatically trigger updates on changes.
    // Realtime subscriptions will keep the query results updated with changes.

    // Define the ref variable.
    // If parameters are known before hand, refs can be initialized here directly
    // else they can be initialized in the init for the view, like here
    @State private var queryRef: GetMovieByIdQuery.Ref

    // Store the handle to unsubscribe from query updates.
    // QueryRef can be used in multiple views.
    // Each view can separately subscribe / unsubscribe to updates
    // When there are no more subscribers to a QueryRef,
    // it will cancel automatic updates for that QueryRef.
    @State private var querySub: AnyCancellable?

    init(movieId: String) {
      // initialize the ref with the movieId
      queryRef = DataConnect.moviesConnector.getMovieByIdQuery.ref(movieId: movieId)
    }

    var body: some View {
        VStack {
            // Use the query results in a View.
            if let movie = queryRef.data?.movie {
              Text(movie.title)
              Text(mpvie.description)
              // other details
            } else {
              // if last fetch/update resulted in an error
              if error = queryRef.lastError {
                Text("Error loading movie")
              } else {
                Text("Loading movie ...")
              }
            }
        }
        .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 {
          // Calling cancel will unsubscribe from receiving updates.
          querySub?.cancel()
        }
    }
}

Android

class ExampleViewModel(
  private val movieId: UUID
) : ViewModel() {
  private val _uiState = MutableStateFlow<GetMovieByIdQuery.Data.Movie?>(null)
  val uiState = _uiState.asStateFlow()

  // Subscribe to the query.
  private val movieInfoSub = ExampleConnector.instance
      .getMovieById.ref(GetMovieByIdQuery.Variables(id = movieId))
      .subscribe()

  init {
    viewModelScope.launch {
      movieInfoSub.flow.collect {
        // As query results are collected, update the UI state.
        val result = it.result.getOrElse { return@collect }
        _uiState.update{ result.data.movie }
      }
    }
  }

  companion object {
    fun provideFactory(movieId: UUID): ViewModelProvider.Factory =
      viewModelFactory {
        initializer {
          ExampleViewModel(movieId = movieId)
        }
      }
  }
}

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, ...))로 지정된 단일 영화를 업데이트하므로 쿼리는 암시적 새로고침을 활용할 수 있습니다.

삽입 및 upsert 작업은 Firebase Authentication 사용자의 UID와 같은 알려진 값을 키로 사용할 때 암시적 새로고침 신호를 트리거할 수 있습니다.

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

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

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

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

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

바인딩 설명
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 디렉티브 참조를 확인하세요.