사이드프로젝트를 하면서 앱의 완성도를 어떻게 높일 수 있을까🤔❓ 고민을 하다가 평소에 접하던 앱의 PUSH와 우리 앱의 PUSH가 많이 다르다는 걸 깨닫게 되었어요.
그래서 PUSH를 이쁘게 바꿔보자 결심했습니다.
FCM Json 구조
FCM 공홈에서 기본적으로 지원하는 Json 형식은 아래와 같습니다.
{
"message":{
"token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
"notification":{
"title":"Portugal vs. Denmark",
"body":"great match!"
}
}
}
token으로 유저(기기)를 판단하고 정해진 title, body로 푸쉬를 보내는거죠
간단하고 편하게 보낼 수 있지만, 이쁘지 않다는 정말 큰 문제가 있습니다😂
저는 공홈에서 조금만 더 스크롤을 내리면 보이는 [플랫폼별 전송 옵션이 있는 알림 메시지] 에서 android를 통해 해결하려 했습니다만, 여러 문제로 인해 결국 다른 방법을 통해 구현했어요.
하지만 꽤 좋은 옵션이라고 판단이 되어서 시행착오를 기록할 겸 소개해보도록 하겠습니다. (Android 코드 수정 없이 Json Option 만으로 이쁜 Push 구현이 가능해요😁)
{
"message":{
"token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
"notification":{
"title":"Match update",
"body":"Arsenal goal in added time, score is now 3-0"
},
"android":{
"ttl":"86400s",
"notification"{
"click_action":"OPEN_ACTIVITY_1"
}
},
"apns": {
"headers": {
"apns-priority": "5",
},
"payload": {
"aps": {
"category": "NEW_MESSAGE_CATEGORY"
}
}
},
"webpush":{
"headers":{
"TTL":"86400"
}
}
}
}
위 간편한 Json과 비교하면 여러가지 Option이 생긴 걸 볼 수 있고 android 부분을 뚝 떼어서 보게되면 notification이 설정이 있다는 걸 확인할 수 있죠.
"android":{
"ttl":"86400s",
"notification"{
"click_action":"OPEN_ACTIVITY_1"
}
},
해당 notification에는 다양한 option이 있지만 제가 시행착오를 겪으면서 사용한 option만 간단히 설명하고 실 구현으로 넘어갈게요.
{
"title": string, // 제목
"body": string, // 본문
"icon": string, // Android 프로젝트에 있는 Image만 설정 가능!
"color": string, // icon 색상 설정
"tag": string, // tag 설정 (알림 창에서 기존 알림을 대체할 수 있게 해줍니다.)
"image": string, // image URL 설정
}
더 많은 내용은 [공식 문서📋] 를 확인해 주세요
Fcm 초기 연동은 되어 있다고 가정할게요.❗
구현은 간단히 Fcm서버에 주어진 형식에 맞는 Json을 커스텀해서 보내기만 하면 되요. (아래처럼요)
{
"message": {
"token": "eTMgJ7JWR-qDppk3G4BB4q:APA91bGwShYspZ3Gci .....",
"android": {
"notification": {
"title": "댓글을 받았어요",
"body": "[아기맹수] 님이 댓글을 남겼어요",
"image": "https://imageurl ~~~",
"tag": "0"
}
}
},
"validateOnly": false
}
저는 Json을 사용할 때 DTO Class를 정의해준 다음 ObjectMapper로 변환해주는 걸 선호해요.
하지만 위 Json의 경우 Depth가 너무 많기 때문에 모두 DTO로 만들 경우 너무 복잡했어요. 그래서 큰 틀의 FcmMessage와 FcmAOS 2개의 DTO만 정의해서 사용했습니다.
(FcmMessage를 굳이 만든 이유는 제가 IOS로의 확장성도 생각해서였어요.)
FcmMessage
@Getter
@Builder
@AllArgsConstructor
public class FcmMessage {
private String token;
private FcmAOS android; // android
public static FcmMessage from(String token, String title, String body, String image, String tag, String url) {
return FcmMessage.builder()
.token(token)
.android(FcmAOS.from(title, body, image, tag, url))
.build();
}
}
FcmMessage에서는 token과 android를 정의했고 from() 메서드를 통해 title, body, image, tag, url 값을 FcmAOS에 넘겨주도록 구현했어요.
📌 주의할 점: Android DTO의 Class명은 뭐로 하든 상관없지만 필드명은 꼭 android로 지정해줘야 합니다.
FcmAOS
@Getter
@Builder
public class FcmAOS {
Notification notification;
Data data;
@Getter
@Builder
public static class Notification {
private String title;
private String body;
private String image;
private String tag;
}
@Getter
@Builder
public static class Data {
private String url;
}
public static FcmAOS from(String title, String body, String image, String tag, String url) {
return FcmAOS.builder()
.notification(Notification.builder()
.title(title)
.body(body)
.image(image)
.tag(tag)
.build())
.data(Data.builder()
.url(url)
.build())
.build();
}
}
Json의 Depth가 깊어서 Class 안에 Class를 지정하는 식으로 구현했어요.
Data의 경우는 위에서 다루지 못했는데 Fcm에서 제공하지 않는 기능을 커스텀하기 위해 안드로이드에 전달할 때 사용하는 필드에요. [공식 문서📋]
댓글 알림의 경우 Push 클릭 시 해당 게시글로 이동하는 기능 구현을 하고 싶었는데 웹앱이었기 때문에 특정 화면으로 이동하기 위한 URL을 Web Server로부터 전달받아야 했어요.
그래서 Data에 url을 담아서 Android에 전달했습니다.😁
public void sendMessageTo(FcmMessage fcmMessage) {
try {
String API_URL = "https://fcm.googleapis.com/v1/projects/~~";
String requestJson = new ObjectMapper().writeValueAsString(
Map.of("validateOnly", false,
"message", fcmMessage));
System.out.println(requestJson);
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = RequestBody.create(MediaType.get("application/json; charset=utf-8"), requestJson);
Request request = new Request.Builder()
.url(API_URL)
.post(requestBody)
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken())
.addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8")
.build();
Response response = client.newCall(request).execute();
assert response.body() != null;
System.out.println("response = " + response.body().string());
response.close();
} catch (Exception e) {
log.error("[FCM] ERROR : {}", e.getMessage());
throw new RuntimeException("PUSH ERROR");
}
}
private String getAccessToken() throws IOException {
String firebaseConfigPath = "firebase/firebase-adminsdk-~~~~.json";
GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())
.createScoped(List.of("https://www.googleapis.com/auth/cloud-platform"));
googleCredentials.refreshIfExpired();
return googleCredentials.getAccessToken().getTokenValue();
}
FcmMessage 만으로는 Json이 완성이 안되서 Map.of()를 통해 Json을 완성해주었어요.
그리고 정해진 형식에 맞춰서 Fcm Server API를 호출하면 Push 이쁘게 만들기 완성입니다.
📌 Map.of() 에서 설정한 validateOnly Option은 test 할 때 사용하는 option이에요. 실제로 연결이 잘 되었는지 확인할 때 사용합니다. true로 설정하면 실제로 Push를 날리지 않아요. 저는 실제로 Push를 보낼거기 때문에 false로 설정했습니다.
@SpringBootTest
@ExtendWith(SpringExtension.class)
class FcmServiceTest {
@Autowired
FcmService fcmService;
@Test
void push(){
// given
String token = "eTMgJ7JWR-qDppk3G4BB4q:APA91bGwShYspZ3GciSCTSWOh6rCmPv-XwQVwMiRs_8Nmhy9JQSVMjPVORoHjgd8x704Nb9aAaoDCIEi52aD7TIQPhkc_9pmwQHR1lyeLrsLFXuwjktNYnamXDssKo4wKuo0sMZ18d77";
String title = "댓글을 받았어요😁";
String body = "[아기맹수]님이 댓글을 남겼어요";
String image = "https://kr.object.ncloudstorage.com/reward-certification/2017_cert.webp?2309020907110842";
String tag = "comment";
String url = "~~~";
// when
FcmMessage fcmMessage = FcmMessage.from(token, title, body, image, tag, url);
fcmService.sendMessageTo(fcmMessage);
}
간단하게 Push Test Code를 작성했습니다. 실제 구현은 DB에서 sender, receiver Data를 조회해서 title, body를 구성해주엇지만 Push가 되는지 테스트이니 테스트용 문구를 직접 넣고 실행시켜 볼게요!
결과

테스트 결과를 확인해보면 Json이 우리가 생각한대로 잘 구현되었고 response를 통해 Push가 제대로 갔다는걸 확인할 수 있습니다.
아래 화면을 보면 Push가 잘 온 걸 확인할 수 있어요.

참고로 token값이 유효하지 않을 경우 아래와 같이 Error Response를 확인할 수 있습니다.😂

해당 기능은 정말 간편하게 Push를 Custom 할 수 있지만 아쉽게도 몇 가지 한계가 있었어요.
First. 헤드업 디스플레이 미지원
Second. 포그라운드, 백그라운드에서 다르게 동작하는 Push
Third. Url을 통한 WebView 이동 설정 어려움
Fourth. Grouping 설정 불가
Push를 이쁘게 하겠다는 처음의 의도는 충분히 만족했지만, 만들면서 추가된 요구사항을 전부 구현하기에는 부족한 기능이었다!
그래서 결국 저는 Fcm.Data()를 사용해서 Android에 필요한 Data를 전달하고 Android Custom을 통해 제가 원하는 Push를 구현했습니다.😂(이게 인생인가..?) 하지만 이쁘죠?

해당 내용은 2편에서 다루겠습니다! 끝 😁❗
ps. 혹시 틀린 내용이 있거나 제가 생각하지 못한 해결방법이 있다면 댓글 남겨주시면 개선하겠습니다. 감사합니다