라이브 쿼리는 일치하는 행의 생성, 수정, 삭제 이벤트를 서버가 발생 즉시 푸시해 주는 데이터베이스 쿼리 구독입니다. 데이터베이스 접근에서 빠져 있던 시제입니다. 일반 쿼리는 과거형으로 말하고(“물었을 때 일치했던 것”), 라이브 쿼리는 현재 진행형으로 말합니다(“지금 일치하는 것, 바뀌는 대로”). 화면은 묻기를 멈추고 계속 정확한 상태를 유지합니다.
핵심 요약
| 질문 | 답변 |
|---|---|
| 정의 | 상시 쿼리 – 한 번 구독하면 결과에 대한 모든 변경을 받음 |
| 다섯 가지 이벤트 | create · update · delete · enter · leave |
| 내부 구조 | 데이터베이스 변경 스트림 → 조건 대조 → 웹소켓 푸시 |
| vs. 폴링 | 실시간 지연 시간, 변경분 크기의 페이로드, 주기별 쿼리 부하 없음 |
| vs. 원시 소켓 | 쿼리 계층: 대조, 타입이 지정된 이벤트, 권한, 재연결 |
코드로 보는 구독
핵심 동작은 이미 갖고 있던 쿼리를 가져와 구독하는 것입니다:
// JavaScript / Node.js — Back4app JS SDK
// A live query: subscribe to a QUERY and receive its changes as events
const query = new Parse.Query('Task');
query.equalTo('assignee', currentUser);
const sub = await query.subscribe();
sub.on('create', (t) => addRow(t)); // new match appeared
sub.on('update', (t) => refreshRow(t)); // a match changed
sub.on('enter', (t) => addRow(t)); // edited INTO the result set
sub.on('leave', (t) => removeRow(t)); // edited OUT of the result set
sub.on('delete', (t) => removeRow(t)); // a match was deleted // Flutter / Dart — Back4app Flutter SDK
// A live query: subscribe to a QUERY and receive its changes as events
final liveQuery = LiveQuery();
final query = QueryBuilder<ParseObject>(ParseObject('Task'))
..whereEqualTo('assignee', currentUser);
final sub = await liveQuery.client.subscribe(query);
sub.on(LiveQueryEvent.create, (t) => addRow(t)); // new match
sub.on(LiveQueryEvent.update, (t) => refreshRow(t)); // match changed
sub.on(LiveQueryEvent.enter, (t) => addRow(t)); // edited into the set
sub.on(LiveQueryEvent.leave, (t) => removeRow(t)); // edited out of the set // iOS / Swift — Back4app Swift SDK
// A live query: subscribe to a QUERY and receive its changes as events
let query = Task.query("assignee" == currentUser)
let subscription = query.subscribeCallback
subscription?.handleEvent { _, event in
switch event {
case .created(let t): addRow(t) // new match appeared
case .updated(let t): refreshRow(t) // a match changed
case .entered(let t): addRow(t) // edited INTO the result set
case .left(let t): removeRow(t) // edited OUT of the result set
case .deleted(let t): removeRow(t)
}
} // Android / Kotlin — Back4app Android SDK
// A live query: subscribe to a QUERY and receive its changes as events
val client = ParseLiveQueryClient.Factory.getClient()
val query = ParseQuery.getQuery<ParseObject>("Task")
query.whereEqualTo("assignee", currentUser)
val sub = client.subscribe(query)
sub.handleEvent(SubscriptionHandling.Event.CREATE) { _, t -> addRow(t) }
sub.handleEvent(SubscriptionHandling.Event.UPDATE) { _, t -> refreshRow(t) }
sub.handleEvent(SubscriptionHandling.Event.ENTER) { _, t -> addRow(t) }
sub.handleEvent(SubscriptionHandling.Event.LEAVE) { _, t -> removeRow(t) } 이벤트 어휘는 표로 정리할 가치가 있습니다. enter와 leave가 있기에 이것은 테이블 알림이 아니라 쿼리 구독이 됩니다:
| 이벤트 | 발생 시점… | 예시(“내게 배정된 작업”을 구독한 경우) |
|---|---|---|
| create | 새 레코드가 일치함 | 자신에게 작업이 생성됨 |
| update | 일치하는 레코드가 변경됨 | 자신의 작업 상태가 바뀜 |
| enter | 수정으로 기존 레코드가 일치하게 됨 | 작업이 자신에게 재배정됨 |
| leave | 수정으로 일치하던 레코드가 더 이상 일치하지 않음 | 자신의 작업이 다른 사람에게 재배정됨 |
| delete | 일치하는 레코드가 삭제됨 | 작업이 제거됨 |
푸시 뒤의 파이프라인
두 절반은 깔끔하게 분리됩니다. 하나는 변경 소스로, 데이터베이스 자체의 쓰기 피드입니다(문서 저장소에서는 변경 스트림, 그 밖에서는 복제 로그). 다른 하나는 대조 계층으로, 각 변경을 등록된 모든 조건과 비교해 결과가 바뀐 구독자에게만 정확히 푸시하며, 권한은 구독자별로 강제됩니다. 공개 LiveQuery 프로토콜은 이 전체 구조를 읽기 쉽게 보여 주는 참조 구현입니다.
라이브 쿼리 vs. 폴링 vs. SSE vs. 웹소켓
| 방식 | 지연 시간 | 페이로드 | 서버 비용 | 직접 구축할 것 |
|---|---|---|---|---|
| 폴링 | 폴링 주기 | 매번 전체 재조회 | 쿼리 × 클라이언트 × 빈도 | 타이머, 비교(diff) |
| 롱 폴링 | 거의 실시간 | 이벤트마다 전체 응답 | 유지되는 연결 | 폴백 연결 작업 |
| SSE | 실시간 | 변경분 | 스트림, 단방향 | 이벤트 설계 |
| 원시 웹소켓 | 실시간 | 정의하기 나름 | 연결 + 직접 만든 팬아웃 | 전부 |
| 라이브 쿼리 | 실시간 | 변경별 이벤트 | 대조 + 연결(관리형) | 쿼리 |
마지막 열이 논거입니다. 원시 소켓에서는 메시지 프로토콜, 라우팅, 권한 확인, 재연결 후 따라잡기를 모두 직접 만들어야 합니다. 라이브 쿼리에서는 조건이 곧 프로토콜이고 나머지는 플랫폼이 맡습니다.
일반적인 사용 사례
- 채팅과 받은 편지함 – 대화의 메시지를 구독합니다. 도착은 새로 고침이 아니라 이벤트입니다.
- 라이브 대시보드 – 주문, 지표, 차량 현황. 쿼리가 화면을 정의하고, 이벤트가 그 화면을 최신 상태로 유지합니다.
- 협업 앱 – 다섯 명의 편집이 수 초 안에 뒤섞이는 공유 작업 보드와 문서.
- 프레즌스와 진행 상태 – 누가 온라인인지, 무엇이 진행 중인지. enter/leave가 정밀하게 제 몫을 합니다.
- 운영 화면 – 마지막 새로 고침 시점이 아니라 지금의 프로덕션을 반영해야 하는 지원 콘솔과 관리자 화면.
폴링, 푸시, 구독 중 무엇을 선택해야 할까? 결정 매트릭스
| 라이브 쿼리를 선택해야 할 때… | 더 단순한 도구로 충분할 때… |
|---|---|
| 사용자가 공유되고 변하는 데이터를 지켜봄 | 데이터가 드물게 바뀜 – 느슨하게 폴링하거나 포커스 시 재조회 |
| 변경이 여러 작성자와 경로에서 옴 | 작성자가 하나라면 SSE로 푸시하면 됨 |
| 화면이 자연스럽게 쿼리로 표현됨 | 메시지가 데이터 형태가 아님(소켓을 직접 사용) |
| 권한이 각 사용자가 보는 것을 제한해야 함 | 모든 것이 공개 브로드캐스트임 |
| 팬아웃 인프라를 직접 소유하고 싶지 않음 | 이미 실시간 플랫폼을 운영 중임 |
무엇을 선택하든 구독을 건강하게 유지하는 클라이언트 측 규율이 있습니다. 좁게 구독하고(인덱스된 필드에 대한 선택적인 조건), 화면이 닫히면 구독을 해제하는 것입니다. 상시 쿼리는 서버 상태이며, 구독을 해제하지 않고 남겨 두는 화면은 보이지 않게 비용을 쌓습니다.
한계와 트레이드오프
- 대조는 실제 작업 부하입니다. 모든 쓰기가 등록된 조건과 비교됩니다. 자주 쓰이는 클래스에 대한 수천 개의 넓은 구독은 이를 몇 배로 늘리며, 선택도가 조절 레버입니다.
- 연결은 상태입니다. 소켓 인프라에는 어피니티, 하트비트, 팬아웃 백플레인이 필요합니다. 이는 차별화와 무관한 고된 작업이며, 관리형 플랫폼이 존재하는 이유가 바로 여기에 있습니다.
- 재연결에는 따라잡기가 필요합니다. 오프라인이었던 클라이언트는 이벤트를 놓쳤습니다. 견고한 흐름은 공백 기간이 조용했다고 믿지 않고 다시 구독할 때 기본 쿼리를 재실행합니다.
- 순서와 전달은 최소 한 번(at-least-once) 형태입니다. 멱등한 이벤트 핸들러(추가가 아니라 객체 ID 기준으로 적용)가 가끔 생기는 중복을 깔끔하게 흡수합니다.
- 모든 것이 구독을 원하지는 않습니다. 거의 조회되지 않고 거의 바뀌지 않는 데이터는 폴링의 영역입니다. 라이브 쿼리는 보는 눈과 쓰기가 모두 잦은 곳에서 비용만큼의 값을 합니다.
Back4app에서의 라이브 쿼리
Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. Live Query가 실시간 계층이며, 위의 코드 탭이 클라이언트 측의 전부입니다. 이미 사용하는 쿼리 빌더, 구독 호출 한 번, 타입이 지정된 다섯 가지 이벤트입니다. ACL은 구독자별로 확인되므로 실시간 기능이 권한 모델을 우회하지 않으며, 웹소켓 인프라, 변경 스트림, 팬아웃은 관리형 인프라로 실행됩니다. 대시보드에서 클래스에 Live Query를 활성화하고 어느 SDK에서든 구독하면, 현재 진행형 데이터 접근은 프로젝트가 아니라 하나의 기능이 됩니다.
자주 묻는 질문
라이브 쿼리란 무엇입니까?
데이터베이스 쿼리에 대한 상시 구독입니다. 한 번 묻고 스냅샷을 받는 대신 쿼리를 구독하면, 결과 집합이 바뀔 때마다 서버가 이벤트를 푸시합니다. 일치하는 레코드가 생성, 수정, 삭제되거나, 수정으로 결과에 들어오거나 나갈 때입니다. 화면은 폴링을 멈추고 듣기 시작합니다.
라이브 쿼리는 일반 쿼리와 어떻게 다릅니까?
수명이 다릅니다. 일반 쿼리는 실행되고, 반환되고, 끝납니다. 그 답은 즉시 낡기 시작합니다. 라이브 쿼리는 서버에 등록된 채로 남습니다. 초기 결과는 똑같이 도착하고, 그 뒤로는 구독을 해제할 때까지 대상이 정해진 변경 이벤트가 결과를 최신으로 유지합니다. 조건 표현 방식은 같고, 시간과의 관계는 정반대입니다.
라이브 쿼리는 내부적으로 어떻게 작동합니까?
두 절반이 결합되어 있습니다. 변경 소스와 전송 수단입니다. 데이터베이스는 변경 스트림, 복제 로그, 트리거를 통해 쓰기 스트림을 내보내고, 구독 서버는 각 변경을 등록된 쿼리 조건과 대조해 관련 이벤트를 웹소켓으로 구독자에게 푸시합니다. 클라이언트 SDK가 소켓 수명 주기를 감싸므로 코드에는 타입이 지정된 이벤트만 보입니다.
enter와 leave 이벤트는 무엇입니까?
쿼리 구독을 정밀하게 만드는 미묘한 한 쌍입니다. enter는 기존 레코드가 수정되어 조건에 일치하기 시작할 때 발생하고, leave는 수정으로 더 이상 일치하지 않게 될 때 발생합니다. 자신에게 재배정된 작업은 생성되지 않고도 "내게 배정된 작업" 구독에 들어오고(enter), 다른 사람에게 재배정되면 삭제되지 않고도 나갑니다(leave). 나머지는 create, update, delete가 맡습니다.
라이브 쿼리가 폴링보다 나은 이유는 무엇입니까?
세 가지가 동시에 좋아집니다. 지연 시간 면에서는 변경이 다음 폴링 주기가 아니라 실시간으로 도착합니다. 대역폭 면에서는 이벤트가 전체를 다시 가져오는 대신 바뀐 것만 싣습니다. 부하 면에서는 서버가 클라이언트별, 주기별로 전체 쿼리를 실행하는 대신 변경 단위로 대조합니다. 몇 초 간격의 폴링은 실시간을 흉내 낼 뿐이고, 구독이야말로 진짜 실시간입니다.
라이브 쿼리는 원시 웹소켓에 무엇을 더합니까?
쿼리라는 두뇌를 더합니다. 원시 소켓은 메시지를 옮길 뿐이고, 어떤 클라이언트가 어떤 변경에 관심이 있는지, 이벤트가 무엇을 의미하는지, 누가 무엇을 볼 수 있는지, 재연결 후 무엇을 따라잡아야 하는지는 모두 직접 작성해야 하는 코드입니다. 라이브 쿼리 계층은 서버 측에서 조건을 대조하고, 이벤트에 타입을 부여하고, 구독자별로 권한을 강제하고, 소켓 수명 주기를 관리합니다. 전송 수단과 기능의 차이입니다.
GraphQL 구독은 라이브 쿼리와 같습니까?
같은 계열이지만 트리거가 다릅니다. GraphQL 구독은 직접 정의한 이름 있는 이벤트에서 발생합니다. 뮤테이션이 발행하면 구독자가 받습니다. 라이브 쿼리는 쿼리의 결과 집합 변경에서 발생합니다. 이벤트 연결 작업이 없고, 조건 자체가 구독입니다. 둘 다 웹소켓을 사용하며, 라이브 쿼리는 명시적인 이벤트 설계를 포기하는 대신 모든 쓰기 경로를 자동으로 포괄합니다.
라이브 쿼리는 어떻게 확장됩니까?
두 축으로 확장됩니다. 하나는 연결로, 수천 개의 열린 소켓이 pub/sub 백플레인 뒤의 구독 서버들에 분산됩니다. 다른 하나는 대조로, 모든 쓰기가 등록된 조건과 비교되므로 인덱스된 필드에 대한 선택적인 구독이 중요합니다. 관리형 플랫폼은 두 축을 모두 대신 운영하며, 클라이언트 측의 규율은 좁게 구독하고 화면이 닫히면 구독을 해제하는 것입니다.