자바

구글 푸시알림 기능 구현하기 (FCM, Java, Vue )

류창 2024. 11. 19. 16:52
반응형

 

 

서론 :

웹 브라우저에 

푸시알림 기능을 구현해달라는 요청을 받았다.

 

 

실무자분들이 고객분들에게 들어오는 실제 접수요청을

다른 업무하느라 잠시 놓쳐 지연되는걸 방지하기 위함이였다.

 

구글에 검색해 보니,  백엔드 : JAVA,  프론트엔드 Vue.js 환경을 기준으로,

프로젝트를 구성한 케이스가없어서 

열심히 삽질한거 다른 개발자분들도 고생하지말라고 남기기로 했다.

 

 

푸시알림 기능은 기본적으로 PUB-SUB 구조다.

즉, 실사용자가 구독하고 서버가 발행하는 형식이다. 

 

많은 기술이있지만 그중에  FCM (Firebase Cloud Messege)를 사용하려고한다.

 

 

 

 

1. FCM 프로젝트 만들기

 

 

https://console.firebase.google.com/

 

로그인 - Google 계정

이메일 또는 휴대전화

accounts.google.com

 

 

구글로그인을 준비해서 firebase console사이트에 로그인을 한다.

 

 

로그인을 한다면 여기서 프로젝트를 만들기 버튼을 눌러, 

프로젝트를 하나 생성한다.

 

 

 

 

프로젝트 개요 -> 프로젝트 설정 -> 일반 -> SDK 설정 및 구성을 보면,

 

firebaseConfig란 설정값이 있다. 

 

이 설정값은 프로젝트에 그대로 쓸거니깐 복사하고 잘 기억해두자.

 

 

 

 

프로젝트 개요 -> 프로젝트 설정 -> 클라우드 메시징 -> 웹 푸시 인증서를 보면,

 

키 쌍 값을 하나 준다.

 

이 값은 메시지를 보내기 위한 토큰을 발급받기위한 인증서기 때문에 이것도 복사 붙이기를 해두고

기억 해두자.

 

이것 역시 프로젝트에 그대로 복붙 할 예정이다.

 

 

2. 클라이언트  (Vue.JS) 설정

FCM 도 firebase이니,  npm 설치를 해줘야한다.

 

 

npm i  firebase  필수!

 

 

Vue.js 프로젝트에 파일을 딱 2개 만들거다.

 

1. firebase-messaging-sw.js

2. fcm.js

 

firebase-messaging-sw는  서비스 워커라는 메세지 발행 시스템이 만들어준다.

 

 

2 - 1. firebase-messaging-sw.js 작성

 

해당 파일은 위치가 중요하다.

 

본인프로젝트 / public /firebase-messagin-sw.js 에 저장한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
importScripts("https://www.gstatic.com/firebasejs/8.0.0/firebase-app.js");
importScripts(
    "https://www.gstatic.com/firebasejs/8.0.0/firebase-messaging.js"
);
 
firebase.initializeApp({
  // 1번에서 복사했던 firebaseConfig를 여기다 집어넣자
});
 
const messaging = firebase.messaging();
 
messaging.onBackgroundMessage((payload) => {
    console.log(
        "[firebase-messaging-sw.js] Received background message ",
        payload
    );
    // Customize notification here
    const notificationTitle = payload.notification.title;
    const notificationOptions = {
        body: payload.notification.body,
        // icon: "/icon.png",
    };
 
    self.registration.showNotification(notificationTitle, notificationOptions);
});
cs

 

15번줄에 onBackgroundMessage가 

백그라운드에서 메시지 수신을 하는 메소드다.

 

notificationTitle과  Options을 사용해,  푸시 알림의 제목과 내용을 작성할수있다.

 

 

이렇게 말이다.

 

 

 

2 - 2. fcm.js 작성

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import { initializeApp } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import axios from "axios";
 
const firebaseConfig = {
// 복사한 firegbaseConfig 입력
};
const app = initializeApp(firebaseConfig);
const messaging = getMessaging();
 
//토큰값 얻기
getToken(messaging, {
   vapidKey: // 복사한 인증서 키 쌍 입력!
       
})
    .then((currentToken) => {
        if (currentToken) {
            // Send the token to your server and update the UI if necessary
            // ...
            console.log(currentToken);
 
 
           axios.post(`본인이 보내고 싶은 주소지`, currentToken,{
                headers: { 'Content-Type''application/json' }
            })
                .then((response) => {
                    // Handle successful response from the server (e.g., update UI)
                    console.log("Token sent to server:", response.data);
                })
                .catch((error) => {
                    console.error("Error sending token to server:", error);
                    // Handle error (e.g., display error message to user)
                });
 
        } else {
            // Show permission request UI
            console.log(
                "No registration token available. Request permission to generate one."
            );
            // ...
        }
    })
    .catch((err) => {
        console.log("An error occurred while retrieving token. ", err);
        // ...
    });
 
//포그라운드 메시지 수신
onMessage(messaging, (payload) => {
    console.log("Message received. ", payload);
    new Notification(payload.notification.title, {body: payload.notification.body});
});
cs

 

이 파일은  src폴더 안에 넣으면 된다.

 

넣은 뒤, 이  js 폴더를 import를 시키면 작동한다.

 

이 파일은 메시지를 수신받기위한 토큰을 생성하는데, 

토큰을 받기위해 아까 복사해둔 인증서키를  vapidKey에 복사한다.

 

그 후, 복사한 token을  백엔드에 요청을보낸다.

 

이 요청은 이른바 Subscribe(등록)하는 행위로써,  백엔드가 기억해뒀다가 이벤트가 발생하면 등록된 클라이언트들에게 메시지를 Publish(발행) 하기위한 첫 스텝이다.

 

 

 

3. 백엔드 설정

 

백엔드 설정은 필자는 4개의 클래스를 설정했다.

 

 

1. 요청 수신받는 Controller

 

2. 서비스를 수행하는 Service

 

3. FCM 메시지 JSON과  FCM 발송 JSON 그릇(Dto)이다.

 

 

NotificationController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequiredArgsConstructor
@RequestMapping("")
public class NotificationController {
 
    private final NotificationService notificationService;
 
 
 
    @PostMapping("/api/rvi/usr/getToken")
    public void initToken(
            @RequestBody String token) {
 
        notificationService.initToken(token);
 
    }
    
}
cs

 


Controller는  Spring을 쓰시는 분이라면 그냥 넘어가도 될만큼 간단하다.

수신받는 Token은 우리 메시지 발송시스템을 구독할 유저의 정보가 담겨있다.

 

 

 

NotificationService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@Service
@RequiredArgsConstructor
public class NotificationService {
 
    Set<String> registrationTokens = new HashSet<>();
 
    public void initToken(String token){
        if(registrationTokens.contains(token)){
            return;
        }
        registrationTokens.add(token);
        System.out.println("FCM TOKEN INSERT");
    }
 
    @Transactional
    public boolean sendMail(String name)  {
 
        System.out.println("알림 발송");
 
        try{
 
            for (String registrationToken : registrationTokens) {
                System.out.println(registrationToken);
                sendMessageTo(new FcmSendDto(registrationToken, "새로운 접수건 발생","새로운 접수건 고객명 " + name));
            }
 
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
        return true;
    }
 
    public int sendMessageTo(FcmSendDto fcmSendDto) throws IOException {
 
        String message = makeMessage(fcmSendDto);
        RestTemplate restTemplate = new RestTemplate();
        /**
         * 추가된 사항 : RestTemplate 이용중 클라이언트의 한글 깨짐 증상에 대한 수정
         * @refernece : https://stackoverflow.com/questions/29392422/how-can-i-tell-resttemplate-to-post-with-utf-8-encoding
         */
        restTemplate.getMessageConverters()
                .add(0new StringHttpMessageConverter(StandardCharsets.UTF_8));
 
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("Authorization""Bearer " + getAccessToken());
 
        HttpEntity<String> entity = new HttpEntity<>(message, headers);
 
        String API_URL = "https://fcm.googleapis.com//v1/projects/ktadminlab/messages:send";
        ResponseEntity<String> response = restTemplate.exchange(API_URL, HttpMethod.POST, entity, String.class);
 
        System.out.println(response.getStatusCode());
 
        return response.getStatusCode() == HttpStatus.OK ? 1 : 0;
    }
 
    private String getAccessToken() throws IOException {
        String firebaseConfigPath = "firebase/ktadminlab-firebase-adminsdk-oivqa-d1db399e3d.json";
 
        GoogleCredentials googleCredentials = GoogleCredentials
                .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())
                .createScoped(List.of("https://www.googleapis.com/auth/cloud-platform",
                        "https://www.googleapis.com/auth/firebase.messaging",
                        "https://www.googleapis.com/auth/chat",
                        "https://www.googleapis.com/auth/chat.bot"));
 
        googleCredentials.refreshIfExpired();
        return googleCredentials.getAccessToken().getTokenValue();
    }
 
    private String makeMessage(FcmSendDto fcmSendDto) throws JsonProcessingException {
 
        ObjectMapper om = new ObjectMapper();
        FcmMessageDto fcmMessageDto = FcmMessageDto.builder()
                .message(FcmMessageDto.Message.builder()
                        .token(fcmSendDto.getToken())
                        .notification(FcmMessageDto.Notification.builder()
                                .title(fcmSendDto.getTitle())
                                .body(fcmSendDto.getBody())
                                .image(null)
                                .build()
                        ).build()).validateOnly(false).build();
 
        return om.writeValueAsString(fcmMessageDto);
    }
 
 
 
cs

 

 

1. initToken :

 

우리 메시지 발송 시스템을 구독할 유저분들을 Set 자료구조를 통해 등록하는 과정이다.

 

중복등록을통해 메시지 중복발송을 방지함이다.

 

즉, 이 메소드는 시스템을 사용할 유저를 구독하는 메소드다.

 

2. sendMail 

본격적으로  어떤 기술적 로직에 푸시알림을 발송하고싶으면, sendMail 메소드를 날린다.

 

Set 자료구조에 구독된 유저에게  sendMessageTo() 메소드를 호출하고,

 

보낼 내용  Title, body를 작성한다.

 

3. sendMessageTo

 

이 메소드는 fcm에 메시지를 발송하는 메소드다.

 

발송하기 전에  makeMessage()로 FCM에서 수신받는 발송형식을 맞춰 발송한다.

 

4.makeMessage

 

이 메소드는  sendMessageTo에서 메시지를 만드는 메소드다.

 

발송하고싶은 메시지내용을 FCM이 알아듣게끔 컨버팅하는작업이다.

 

 

 

 

 

마치며 ..

 

 

접수건마다  이 작은 푸시알림 만들기위해서 

 

백엔드, 프론트엔드에 엄청난 가내수공업이 들어간다..  매우 복잡하고 번거로운 작업이지만

 

사용하는 실무자분들께서는 인터넷하시다 즉각 알림을 받고 처리할수있으니, 

 

실무자도 물론, 접수하는 고객분들도 만족하는 결과를 얻어낼수 있다.

 

 

반응형