푸시 알림은 OS가 전달해 닫힌 앱에도 도달하는 알림이고, 라이브 쿼리는 앱이 열려 있는 동안 데이터 변경을 스트리밍하는 구독입니다. 둘을 경쟁 관계로 보는 것이 전형적인 실수입니다. 두 채널은 서로 겹치지 않는 앱 상태를 다루며, 진짜 설계 질문은 “어느 쪽인가”가 아니라 “사용자가 지금 어디에 있는가”입니다. 제대로 실시간처럼 느껴지는 앱은 대부분 둘 다 운영하고 그 답에 따라 라우팅합니다.
핵심 요약
| 질문 | 답변 |
|---|---|
| 푸시 알림 | OS 게이트웨이(APNs, FCM)를 통한 전달, 닫힌 앱에도 도달, 약 4 KB의 불투명한 페이로드, 최선 노력 |
| 라이브 쿼리 | 웹소켓 구독, 권한이 검사된 전체 객체, 실시간, 앱이 열려 있어야 함 |
| 경쟁 관계 | 사실이 아님: 겹치지 않는 앱 상태를 다루며 경쟁하지 않고 조합됨 |
| 라우팅 규칙 | 앱 열림 → 라이브 쿼리 · 앱 닫힘 → 푸시 · 푸시 탭 → 앱 열기, 재구독, 따라잡기 |
| 실패 양상 | 푸시를 데이터 채널로 쓰거나, 소켓이 프로세스보다 오래 살 것으로 기대하는 것 |
코드로 보는 두 채널
메시징, 주문, 알림 앱이 거의 모두 수렴하는 패턴입니다:
// JavaScript — Back4app JS SDK: one channel per app state
// While the app is OPEN — the live query delivers the data itself
const messages = new Parse.Query('Message');
messages.equalTo('conversation', conversationId);
const sub = await messages.subscribe();
sub.on('create', (m) => appendBubble(m)); // full object, real time
// While it is CLOSED — push owns delivery: register this device
const installation = await Parse.Installation.currentInstallation();
installation.set('channels', [`user-${currentUser.id}`]);
await installation.save();
// A Cloud Code afterSave trigger sends the push to this channel —
// the OS shows the alert; the tap opens the app, which re-subscribes. // Flutter / Dart — Back4app Flutter SDK: one channel per app state
// While the app is OPEN — the live query delivers the data itself
final liveQuery = LiveQuery();
final messages = QueryBuilder<ParseObject>(ParseObject('Message'))
..whereEqualTo('conversation', conversationId);
final sub = await liveQuery.client.subscribe(messages);
sub.on(LiveQueryEvent.create, (m) => appendBubble(m)); // full object
// While it is CLOSED — push owns delivery: register this device
final installation = await ParseInstallation.currentInstallation();
installation.set('channels', ['user-$userId']);
await installation.save();
// A Cloud Code afterSave trigger sends the push to this channel —
// the OS shows the alert; the tap opens the app, which re-subscribes. // iOS / Swift — Back4app Swift SDK: one channel per app state
// While the app is OPEN — the live query delivers the data itself
let messages = Message.query("conversation" == conversationId)
let subscription = messages.subscribeCallback
subscription?.handleEvent { _, event in
if case .created(let m) = event { appendBubble(m) } // full object
}
// While it is CLOSED — push owns delivery: register this device
var installation = Installation.current
installation?.channels = ["user-\(userId)"]
installation?.save { _ in }
// A Cloud Code afterSave trigger sends the push to this channel —
// the OS shows the alert; the tap opens the app, which re-subscribes. // Android / Kotlin — Back4app Android SDK: one channel per app state
// While the app is OPEN — the live query delivers the data itself
val client = ParseLiveQueryClient.Factory.getClient()
val messages = ParseQuery.getQuery<ParseObject>("Message")
messages.whereEqualTo("conversation", conversationId)
val sub = client.subscribe(messages)
sub.handleEvent(SubscriptionHandling.Event.CREATE) { _, m ->
appendBubble(m) // full object
}
// While it is CLOSED — push owns delivery: register this device
val installation = ParseInstallation.getCurrentInstallation()
installation.put("channels", listOf("user-$userId"))
installation.saveInBackground()
// A Cloud Code afterSave trigger sends the push to this channel. 각 절반이 무엇을 다루는지 주목해야 합니다. 구독은 데이터를 전달합니다. 전체 객체와 타입이 있는 이벤트를 실시간으로 받습니다. 설치(installation)는 주의를 위해 등록합니다. 앱이 통제하지 않는 파이프라인을 통해 나중에 사용자를 방해할 권리입니다.
두 가지 전달, 두 명의 소유자
두 경로는 완전히 다르며, 비교표의 모든 속성은 마지막 구간을 누가 소유하느냐에서 나옵니다.
푸시 알림은 OS가 운영하는 게이트웨이에 대한 요청으로 백엔드를 떠납니다. iOS와 Android의 푸시 게이트웨이(APNs, FCM)이거나, 웹에서는 RFC 8030을 구현하는 브라우저 푸시 서비스입니다. 전달은 게이트웨이가 소유합니다. 오프라인 디바이스를 위해 메시지를 보관하고, 배터리 상황에 따라 합치고 제한하며, 앱을 깨우거나 배너를 그립니다. 이 빌린 힘이 핵심입니다. 실행 중이 아닌 프로세스에 도달할 수 있는 것은 오직 OS뿐입니다. 그리고 그것이 곧 제약이기도 합니다. 페이로드는 약 4 KB로 제한되고, 전달은 최선 노력이며, 사일런트 백그라운드 깨우기는 하루 단위로 배급되고, 페이로드는 백엔드의 권한 모델 바깥에 있습니다.
라이브 쿼리는 신뢰 경계를 결코 벗어나지 않습니다. 앱은 백엔드의 구독 서버에 웹소켓을 유지하고, 서버는 모든 데이터베이스 쓰기를 구독된 쿼리와 대조해 타입이 있는 이벤트(create, update, enter, leave, delete)를 구독자별로 ACL을 적용해 푸시합니다. 전체 객체, 실시간 지연 시간, 언급할 만한 페이로드 상한은 없습니다. 그 대가로 의존성은 가혹합니다. 구독은 프로세스 안의 상태이며, OS가 앱을 일시 중단하면(모바일 플랫폼은 이를 적극적으로 합니다) 소켓도 채널도 사라집니다.
푸시 알림 vs. 라이브 쿼리
| 푸시 알림 | 라이브 쿼리 구독 | |
|---|---|---|
| 닫힌 앱에 도달 | 예: 이 채널을 정의하는 능력 | 아니요: 구독은 프로세스와 함께 사라짐 |
| 페이로드 | 약 4 KB, ACL에서 보이지 않음 | 전체 객체, 구독자별 권한 검사 |
| 전달 보장 | 최선 노력, 합쳐지고 제한되며 버려질 수 있음 | 연결 중에는 신뢰 가능, 끊긴 뒤에는 따라잡기 필요 |
| 지연 시간 | 수 초 수준, 게이트웨이에 따라 다름 | 실시간(약 RTT) |
| 전송 소유자 | OS와 그 게이트웨이 | 백엔드의 웹소켓 인프라 |
| 사용자 동의 | 권한 요청, 사용자가 철회 가능 | 필요 없음, 앱 자신의 데이터일 뿐 |
| 오용 비용 | 알림 피로, 앱 삭제 | 과도한 구독 시 배터리와 소켓 부하 |
| 설계 목적 | 주의 끌기와 재참여 | 데이터와 앱 내 상태 |
두 채널이 서로 다른 질문에 답하기 때문에 각 행이 깔끔하게 조합됩니다. 푸시는 “사용자에게 어떻게 도달하는가?”에 답하고, 라이브 쿼리는 “화면이 어떻게 사실을 유지하는가?”에 답합니다. SSE, 웹소켓, 폴링의 전송 수준 비교가 전적으로 두 번째 질문 안에 있는 이유입니다.
인계 지점: 앱이 실제로 깨지는 곳
버그는 두 채널의 이음매에 있습니다. 사용자가 탭한 푸시의 메시지가 앱이 일시 중단되기 전에 라이브 쿼리로 이미 전달된 것일 수 있습니다. 채널이 아니라 객체 ID로 중복을 제거해야 합니다. 사용자가 더 이상 접근할 수 없는 데이터에 대한 푸시가 도착할 수 있습니다. 앱을 열 때 일반 API로 가져와 ACL이 답하게 하고, 페이로드는 절대 신뢰하지 않아야 합니다. 앱이 3일 동안 닫혀 있었다면, 다시 열린 앱은 푸시로 공백을 재생할 수 없습니다(알림은 저널이 아닙니다). 따라서 인계는 언제나 재구독한 뒤 기본 쿼리를 다시 실행해 사실을 재구성하는 것이며, 푸시 토큰 등록은 백그라운드에서 최신으로 유지합니다. 이음매를 한 번 설계하면 두 채널 모두 지루해집니다. 그것이 목표입니다.
일반적인 사용 사례
- 채팅과 메시징: 라이브 쿼리가 열린 대화를 렌더링하고, 푸시가 닫힌 상태에서 “새 메시지”를 전달합니다. 두 채널을 모두 쓰는 대표적인 앱입니다.
- 주문 및 배송 추적: 열린 추적 화면은 구독하고, 앱이 닫혀 있는 동안 “배송 완료”로 바뀐 상태는 푸시로 도착합니다.
- 운영 알림: 온콜 콘솔은 상황판을 위해 구독하고, 호출 경로는 푸시입니다. 새벽 3시에 앱을 열어 두는 사람은 없기 때문입니다.
- 경매와 한정 판매: 앱 안에서는 실시간 가격 변동을 보여 주고, 자리를 비운 사이 상위 입찰이 들어오면 푸시로 알려 실시간 화면으로 딥 링크합니다.
- 소셜 참여: 좋아요와 답글은 재참여를 위한 푸시로 도착하고, 열린 피드는 구독으로 실시간이 됩니다.
어떤 채널을 선택해야 할까? 결정 매트릭스
| 상황 | 선택 |
|---|---|
| 화면이 열려 있고 최신 상태를 유지해야 함 | 라이브 쿼리 |
| 앱이 닫혀 있는 동안 이벤트가 발생하고 사용자가 알아야 함 | 푸시 알림 |
| 페이로드가 민감하거나 권한으로 보호됨 | 라이브 쿼리, 또는 식별자만 푸시하고 열 때 가져오기 |
| 데이터의 보장되고 순서가 있는 전달이 필요함 | 어느 하나만으로는 불가: 열 때 쿼리 기반 따라잡기, 채널은 가속 수단 |
| 앱 내 배지나 카운트를 실시간으로 갱신 | 라이브 쿼리 |
| 며칠 동안 앱을 열지 않은 사용자의 재참여 | 푸시: 이것이 가능한 유일한 채널 |
| 절대 잠들지 않는 키오스크나 상황판 | 라이브 쿼리만, 푸시는 더하는 것이 없음 |
한계와 트레이드오프
- 푸시는 데이터 채널이 아닙니다. 페이로드 상한, ACL 바깥의 불투명한 라우팅, 메시지 합치기 때문에 구조적으로 상태를 실어 나르기에 맞지 않습니다. 포인터를 보내고, 열 때 사실을 가져와야 합니다.
- 푸시 전달은 약속이 아니라 확률입니다. 배터리 최적화, 철회된 권한, 게이트웨이 제한은 모두 메시지를 조용히 버립니다. 놓쳐서는 안 되는 것에는 쿼리 기반 조정 경로가 필요합니다.
- 라이브 쿼리는 프로세스 경계에서 멈춥니다. 일시 중단을 견디는 소켓은 없습니다. 구독을 항상 켜져 있는 채널로 여기는 것이 “이벤트를 놓쳤다”는 버그 대부분의 바탕에 깔린 잘못된 가정입니다.
- 두 채널 모두 클라이언트에 부담을 줍니다. 지나치게 넓은 구독은 배터리와 서버의 매칭 비용을 태우고, 지나치게 잦은 푸시는 신뢰를 태웁니다. 앱 삭제가 사용자의 속도 제한입니다.
- 이음매는 개발자의 책임입니다. 중복 제거, 따라잡기 쿼리, 토큰 갱신은 애플리케이션 로직이며, 어느 채널도 이를 공짜로 제공하지 않습니다.
Back4app에서의 푸시와 라이브 쿼리
Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. 두 채널 모두 기본으로 제공되며 하나의 쓰기 경로를 공유합니다. 같은 데이터베이스 쓰기에 대한 Cloud Code afterSave 트리거가 플랫폼의 게이트웨이 연동을 통해 디바이스와 채널을 지정해 푸시를 보내는 한편, 라이브 쿼리는 권한이 검사된 구독 클라이언트 모두에게 전체 객체를 자동으로 팬아웃합니다. 위의 라우팅 다이어그램은 트리거 하나와 구독 호출 하나로 줄어들며, 이음매 로직(따라잡기 쿼리, Installation 클래스를 통한 토큰 등록)은 코드 탭에 나온 같은 SDK로 처리됩니다.
자주 묻는 질문
푸시 알림과 라이브 쿼리의 차이는 무엇입니까?
전달 경로와 대상이 되는 앱 상태가 다릅니다. 푸시 알림은 운영체제의 푸시 게이트웨이를 거쳐 앱이 닫혀 있어도 디바이스에 도달하지만, 작고 불투명한 페이로드와 최선 노력(best-effort) 수준의 보장만 제공합니다. 라이브 쿼리는 실행 중인 앱이 열어 두는 웹소켓 구독입니다. 권한이 검사된 전체 데이터 객체를 실시간으로 받지만, 앱이 종료되는 순간 함께 사라집니다.
앱이 닫혀 있어도 라이브 쿼리가 동작합니까?
아니요. 클라이언트 측에서 어떤 기교를 부려도 달라지지 않습니다. 라이브 쿼리는 실행 중인 프로세스 안의 상태, 즉 앱이 열어 두는 웹소켓입니다. OS가 앱을 일시 중단하거나 종료하면 소켓도 함께 사라지며, 모바일 플랫폼은 배터리를 아끼기 위해 백그라운드 앱을 적극적으로 일시 중단합니다. 닫힌 앱에 도달하는 일은 OS가 자체 푸시 게이트웨이에 맡겨 둔 바로 그 역할입니다.
채팅 앱은 푸시 알림과 라이브 쿼리 중 무엇을 사용해야 합니까?
둘 다 사용하되 앱 상태로 나눕니다. 열린 대화는 라이브 쿼리를 구독하므로 메시지가 입력 중 표시와 읽음 확인까지 포함한 전체 데이터로 즉시 렌더링됩니다. 닫힌 앱은 푸시에 의존해 수신자에게 알리며, 배너를 그리는 데 필요한 만큼의 페이로드만 싣습니다. 탭하면 앱이 열리고, 앱은 다시 구독하고 다시 쿼리해 밀린 내용을 따라잡습니다. 주요 메신저는 모두 이렇게 동작합니다.
푸시 알림은 반드시 도착합니까?
아니요. 전달은 설계상 최선 노력입니다. OS 게이트웨이는 배터리 상황에 따라 메시지를 합치고, 제한하고, 버리며, 사용자는 권한을 아예 꺼 버리고, 디바이스는 연결이 끊깁니다. 사일런트 백그라운드 푸시는 보이는 알림보다 더 강하게 제한됩니다. 푸시는 어깨를 두드려 깨우는 신호로 다뤄야 하며 데이터 전송 계약으로 여겨서는 안 됩니다. 앱은 열린 뒤 쿼리로 상태를 맞춰야 합니다.
푸시 알림 페이로드는 얼마나 클 수 있습니까?
데이터가 아니라 킬로바이트 단위입니다. OS 게이트웨이는 페이로드를 약 4 KB로 제한하며, 페이로드는 백엔드의 권한 모델에서 보이지 않습니다. 그 안에 넣은 것은 무엇이든 ACL 바깥의 알림 파이프라인에 놓입니다. 견고한 패턴은 식별자와 표시용 문자열만 보내고, 열린 앱이 권한 검사를 거치는 일반 API로 실제 객체를 가져오게 하는 것입니다.
사일런트 푸시 알림이란 무엇입니까?
보이는 알림 없이 OS에 앱을 백그라운드에서 잠깐 깨워 달라고 요청하는 푸시입니다. 보통 다음에 앱을 열 때 즉시 표시되도록 데이터를 미리 가져오는 데 씁니다. 푸시 시스템에서 가장 엄격하게 배급되는 자원으로, OS는 앱별로 하루에 앱을 깨울 수 있는 횟수에 예산을 두고 초과분은 무시합니다. 따라서 사일런트 푸시는 최적화 계층이지 신뢰할 수 있는 동기화 채널이 아닙니다.
푸시 알림과 라이브 쿼리가 모두 필요합니까?
사용자가 앱이 닫혀 있는 동안 일어나는 이벤트(메시지, 주문, 알림)에 관심이 있다면 거의 필연적으로 필요합니다. 두 채널은 서로 겹치지 않는 앱 상태를 다룹니다. 라이브 쿼리는 열린 앱의 경험을, 푸시는 닫힌 상태에서의 재참여를 맡습니다. 두 채널을 모두 내장한 백엔드에서는 데이터베이스 쓰기 한 번이 각 채널로 팬아웃되므로, 둘 다 필요하다는 것이 두 번 구축한다는 뜻이 되지 않습니다.