사이드 프로젝트로 웹앱 서비스를 운영하고 있다. 해당 서비스에 웹 푸시 알림을 도입한다면 서비스 리텐션을 높일 수 있지 않을까 생각했고, 팀원들을 설득하기 전에 웹 푸시 알림에 대해 공부하려고 한다.
알림과 푸시는 다른 의미를 가진다. 알림은 클라이언트가 사용자에게 새로운 정보를 표시하거나, 업데이트 된 내용 등을 알릴 수 있고, 푸시는 서버에서 클라이언트 방향으로 메시지를 전송하는 것이다.
푸시 기능이 가능하려면 하나의 스레드가 서버의 응답을 계속 기다려야 하는데 웹에서는 서비스 워커가 그 역할을 한다.
웹 푸시는 보안을 위해서 https
와 localhost
에서만 동작한다.
FCM을 통해서 푸시 기능을 쉽게 구현할 수 있다.
최종적으로 아래와 같은 흐름으로 푸시 알림이 진행된다.
클라이언트는 서비스 보안을 위해서 vapid 키가 꼭 필요하고, FCM에서 이를 지원해준다.
(프로젝트 설정 → 클라우드 메시징 → 하단 웹 구성 → 웹 푸시 인증서 클릭 후 Generate key pair)
위 경로를 통해서 다음과 같이 vapid 키를 발급 받을 수 있다.
Firebase로부터 토큰 값을 받기 전에 사용자에게 알림 권한 요청이 선행되어야 한다.
사용자가 알림 권한이 없는 상태에서 subscribe 요청을 보내면 에러가 발생하니 꼭 알림 권한이 있는 상태에서 subscribe 요청을 보내자.
알림 허용을 받은 후, vapid키와 함께 subscribe 요청을 FCM으로 보내서 토큰 값을 받을 수 있다. 문서의 스니펫에 나오는 getToken
메서드를 통해서 토큰을 얻을 수 있고, 이후 해당 토큰 값을 서버로 전송하면 된다.
토큰 값을 받는 자세한 과정은 Firebase 문서를 참고하면 된다.
서비스 워커는 브라우저와 네트워크 사이의 가상 프록시이다. 웹 페이지와 독립된 스레드에서 실행되며 오프라인 기능, 알림 처리, 독립된 스레드에서의 복잡한 계산 등 많은 것을 할 수 있다.
FCM의 서비스 워커는 /public/firebase-messaging-sw.js
파일에서 서비스 워커 기능을 구현해야 한다. 이때, 경로와 파일 명을 준수해야 한다.
서버는 클라이언트로부터 토큰 값을 받고, 언제든 해당 클라이언트에게 푸시 이벤트를 보낼 수 있도록 이를 DB에 잘 저장해야 한다.
서버에서의 FCM 구현 방식으로 Firebase Admin SDK 사용
, FCM HTTP v1 API 사용
, 기존 HTTP 프로토콜
사용 총 3가지가 있다.
이중 HTTP v1 API 방식이 가장 최신의 프로토콜로서 보다 안전한 승인과 유연한 크로스 플랫폼 메시징 기능을 제공한다. 또한, 이제 새로운 기능은 HTTP v1 API 방식에만 추가되므로 해당 방식을 사용하는 것을 권장한다.
Firebase Admin SDK 사용 방식과 HTTP v1 API 방식, 총 2개의 메시지를 전송하는 방식을 실습 해볼 것이다.
애플리케이션을 시작할 때 Firebase를 초기화 한다.
메시지를 전송할 때, Builder 패턴을 이용해서 간단하게 푸시 메시지를 전송할 수 있다.
fun main(args: Array<String>) {
val resource = ClassPathResource("firebase/fcm.json")
val serviceAccount = FileInputStream(resource.file)
val options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build()
FirebaseApp.initializeApp(options)
runApplication<ServerApplication>(*args)
}
fun push(targetToken: String) {
val notification: Notification = Notification.builder()
.setTitle("타이틀")
.setBody("바디")
.build()
val message: Message = Message.builder()
.setToken(targetToken)
.setNotification(notification)
.build()
FirebaseMessaging.getInstance().send(message)
}
서버에 Admin SDK를 추가하는 자세한 내용은 해당 Firebase 문서를 참고하면 된다.
Admin SDK를 통해 메시지를 전송하는 자세한 내용은 해당 Firebase 문서를 참고하면 된다.
애플리케이션을 시작할 때 따로 Firebase를 초기화 하지 않아도 된다.
JSON 형식으로 된 메시지를 통해서 사용자에게 메시지를 보낸다.
메시지를 전송할 target을 나타내는 token
또는 topic
을 무조건 설정해줘야 한다.
data class FCMSendDto(
@JsonProperty("validate_only")
val validateOnly: Boolean,
val message: MessageDto
)
data class MessageDto(
val token: String,
val notification: NotificationDto
)
data class NotificationDto(
val title: String,
val body: String
)
JSON에 형식의 메시지는 해당 Firebase 문서를 참고하면 된다.
private fun getAccessToken(): String {
val scopes = listOf("https://www.googleapis.com/auth/cloud-platform")
val resource = ClassPathResource("firebase/fcm.json")
val serviceAccount = resource.inputStream
val googleCredentials: GoogleCredentials = GoogleCredentials
.fromStream(serviceAccount)
.createScoped(scopes)
googleCredentials.refreshIfExpired()
return googleCredentials.accessToken.tokenValue
}
공식 문서에서 보면 refreshAccessToken()
함수를 사용하지만 accessToken을 null로 가져오기 때문에 꼭 refreshIfExpired()
함수를 사용하자
val restTemplate = RestTemplate()
val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_JSON
setBearerAuth(getAccessToken())
};
val objectMapper = ObjectMapper()
val entity = HttpEntity(objectMapper.writeValueAsString(body), headers)
val response = restTemplate.exchange(
"https://fcm.googleapis.com/v1/projects/{projectId}/messages:send",
HttpMethod.POST,
entity,
String::class.java
)
println(response)
HTTP 요청 주소에 있는 {parent=projects/*}
는 본인의 프로젝트 id를 뜻한다. 프로젝트 id는 프로젝트 설정란에서 확인할 수 있다.
메시지 전송에 관한 자세한 내용은 해당 Firebase 문서를 참고하면 된다.
웹 푸시에 관해 학습할 때, 우선 How to make PWAs re-engageable using Notifications and Push 문서를 한번 읽는 것을 추천한다. 프로세스를 빠르게 파악할 수 있고, FCM을 도입할 때 ‘이게 이 내용이구나’를 알 수 있어서 FCM 흐름을 이해하는데 도움을 준다.
HTTP v1 API 방식을 바로 도입하는 것이 베스트긴 하지만 시간이 부족하고, 아직 FCM이 잘 이해가 되지 않는다면 우선 Admin SDK를 사용하는 방식으로 구현하는 것을 추천한다. 먼저 쉽고 간단하게 Admin SDK를 사용하는 방식으로 기능을 구현한 후에 HTTP v1 API 방식으로 마이그레이션을 하면 좋을 것 같다.
참고
How to make PWAs re-engageable using Notifications and Push
Making PWAs work offline with Service workers
중간 중간 있는 Firebase 문서