본문 바로가기
프로젝트/SUFY

[도착 알리미 SUFY] 7. FCM을 활용하여 푸시 알림 전송하기

by kim-dev 2024. 2. 19.
반응형

 

이제... 드디어 알림을 보낼 때이다.
아니 그런데 카카오 API 문서에 있는 메시지 전송은 SUFY 채널에서 사용자에게 메시지를 보낼 수 있는 게 아니라, 사용자가 자신에게 보내거나 사용자의 친구에게 메시지를 보내는 용도로 사용되는 것이라는 이슈가 있었다...

 

그래서... 진짜 오만 문서들을 다 찾아봤다. 푸시 알림 서비스도 찾아봤는데 이건 앱 전용인 것 같아서 못 쓰고... 알림톡 서비스는 사업자 계정이 아니면 사용하지 못하고... 진짜 이 때 멘붕 그 자체...........

 

결국 찾은 건 구글 FCM을 활용하여 푸시 알림을 보내는 것이었다!
Firebase Cloud Messaging이라고 하는 건데... 진짜 역대급으로 힘들었다...
FCM 역시 설명이 다 제각각이어서 찾는 데 힘들었지만... 지금부터 다 같이 보면서 천천히 구현을 해 보자...


우선, FCM을 활용해야 하니, FCM 웹사이트에 들어가서 계정을 하나 만들어 주었다.

 

사실... FCM은 버전이 바뀔수록 변화가 엄청 큰 것 같다.
이 글을 쓰고 있는 현재 최신 버전이 10.8.0인데, 구글링해서 나오는 과거의 게시물들을 참고해도 도저히 호환이 안 돼서 나도 그냥 8.0.0 버전으로 바꿔서 사용하려고 한다... 스택오버플로우에서도 이렇게 사용들 하시는 듯 ㅋㅋ

 

여튼, 뭐 FCM 웹사이트 계정을 만들고 웹 앱을 하나 생성하였다면 이제 FCM을 사용할 준비는 끝이다.
FCM을 사용하기 위해서는 여러 단계를 거쳐야 하는데, 사실 다 작성하기는 귀찮고... 구글링 하면 많이 나오니 자세한 것이 궁금하다면 구글링 하시길 바랍니다.

 

저는 그냥 간단히 제가 작성한 코드들만 의식의 흐름대로 게시하겠읍니다.

 

우선, 클라이언트 측에서는 로그인하는 유저의 디바이스 토큰을 FCM에서 받아와야 한다.
디바이스 토큰이란? 로그인하는 유저 개개인들이 사용하는 디바이스를 식별하는 코드를 말한다. 어떤 기기로 푸시 알림을 보내줄 건지 알아야 그 기기로 푸시 알림을 보낼 수 있으니까!

 

resources/static 경로에 firebase-messaging-sw.js라는 이름의 js를 만들어준 후, FCM 웹페이지에서 만든 자신의 웹 앱의 firebaseConfig를 복사해서 붙여넣는다.

(이게 안 돼 있으면 클라이언트 측에서 firebase SDK를 사용하려면 오류가 발생한다... 나도 진짜 이거 무시하고 jsp에 한 번에 다 우겨 넣으려다가 하루동안 머리 싸매고 고민함 ㅋㅋ)

< firebase-messaging-sw.js >

const firebaseConfig = {
    apiKey: "*****************",
    authDomain: "*****************",
    projectId: "*****************",
    storageBucket: "*****************",
    messagingSenderId: "*****************",
    appId: "*****************",
    measurementId: "*****************"
};

 

그럼 이제 클라이언트 측에서 firebase SDK를 이용하여 디바이스 토큰을 발급받을 수 있다.
나는 로그인 시 디바이스 토큰을 발급받은 후, 이 토큰을 DB에 저장시키도록 작성하였다.

디바이스 토큰이 영원한 게 아니라 로그인 시 마다 계속 업데이트 해주는 형식으로 해줬음!

< login.jsp >

<script src="https://www.gstatic.com/firebasejs/8.0.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.0.0/firebase-messaging.js"></script>
<script>
    const firebaseConfig = {
        apiKey: "*****************",
        authDomain: "*****************",
        projectId: "*****************",
        storageBucket: "*****************",
        messagingSenderId: "*****************",
        appId: "*****************",
        measurementId: "*****************"
    };

    const app = firebase.initializeApp(firebaseConfig);
    const messaging = firebase.messaging(app);

    messaging.requestPermission()
        .then(function () {
            return messaging.getToken();
        })
        .then(async function (token) {
            localStorage.setItem('Fcmtoken', token);
        });

    console.log(localStorage.getItem('Fcmtoken'));
</script>

아 참고로 requestPermission() 함수를 동작시키면, 웹에서 푸시 알림을 보내는 것을 허용할 것이냐고 묻는다.
여기서 당연히 허용을 해 줘야만 푸시 알림을 수신받을 수 있음!

 

여튼 이렇게 작성해 주면, 토큰을 발급받을 수 있고 console에서 확인할 수 있다.

내 컴퓨터의 디바이스 토큰

 

여튼 이 토큰을 로그인 시 DB에 유저 정보와 함께 저장해주면 된다!
토큰을 저장할 클래스는 아래와 같다.

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
public class Fcmtoken {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    
    @ManyToOne
    @JoinColumn(name="userId")
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Account user;
    
    private String token;
}

 

이제 클라이언트에서 로그인을 시도하면, 자신이 가지고 있는 토큰을 아래 url로 넘겨준 후 DB에 저장시키면 된다.

나는 카카오 로그인 성공 시 tokenSave.jsp로 이동시켜 Jwt토큰들을 저장시켜 줬었는데, 여기서 Fcm토큰도 함께 저장시키는 로직을 넣어 줬다!

그래서... 앞서 login.jsp에 설정해 둔 FCM 토큰 발급 과정을 tokenSave.jsp로 모두 옮겨주었고, 로직을 작성했다.

< tokenSave.jsp >

<script src="https://www.gstatic.com/firebasejs/8.0.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.0.0/firebase-messaging.js"></script>
<body>
    <div id="Contents">

        <div id="SUFY_label">
            잠시만 기다려 주세요...
            <script>
                localStorage.setItem('Authorization', '${accessToken}')
                localStorage.setItem('Refresh-Token', '${refreshToken}')
                localStorage.setItem('accessExpire', '${accessExpire}')
                localStorage.setItem('refreshExpire', '${refreshExpire}')


                const firebaseConfig = {
                    apiKey: "*****************",
                    authDomain: "*****************",
                    projectId: "*****************",
                    storageBucket: "*****************",
                    messagingSenderId: "*****************",
                    appId: "*****************",
                    measurementId: "*****************"
                };

                const app = firebase.initializeApp(firebaseConfig);
                const messaging = firebase.messaging(app);

                messaging.requestPermission()
                    .then(function () {
                        return messaging.getToken();
                    })
                    .then(async function (token) {
                        let data = {
                            accountId: ${accountId},
                            fcmToken: token
                        };

                        $.ajax({
                            type: "PUT",
                            url: "/saveFcmtoken",
                            data: JSON.stringify(data),
                            contentType: "application/json; charset=utf-8"
                        }).fail(function (error) {
                            alert("FCM 토큰 저장에 실패하였습니다.");
                            console.log(error);
                        });
                    });

                setTimeout(function() {
                    window.location.href = "/index";
                }, 1500);
            </script>
        </div>

    </div>


    <%@ include file="../../layout/footer.jsp"%>
</body>
< FcmController.java >

@PutMapping("/saveFcmtoken")
public int saveFcmtoken(@RequestBody FcmTokenDTO fcmTokenDTO) {
    fcmService.saveFcmtoken(fcmTokenDTO);
    return 1;
}
< FcmService.java >

@Transactional
public void saveFcmtoken(FcmTokenDTO fcmTokenDto) {
    int accountId = fcmTokenDto.getAccountId();
    String fcmToken = fcmTokenDto.getFcmToken();

    Account user = accountRepository.findById(accountId).get();

    Fcmtoken fcmtoken = fcmtokenRepository.findByUser(user);
    if (fcmtoken == null) {
        fcmtoken = new Fcmtoken();
        fcmtoken.setUser(user);
        fcmtoken.setToken(fcmToken);
        fcmtokenRepository.save(fcmtoken);
    } else {
        fcmtoken.setToken(fcmToken);
    }
}

 

이렇게 하면, 로그인 완료 후 tokenSave.jsp로 리다이렉팅 되면서 FCM토큰이 전부 발급되고 저장된다.

 

이제 토큰을 발급하고 DB에 저장하는 단계까지 완료했다.
즉, 이제 푸시를 전송할 수 있는 준비 단계는 모두 끝난 것이다!

이제 서버에서 푸시를 보내는 로직을 작성해 보자.

 

우선, firebase 라이브러리를 사용해야 하므로 build.gradle에 아래 코드를 추가한다.

// FCM
implementation 'com.google.firebase:firebase-admin:7.1.1'

 

그리고 서버에서 이 firebase SDK에 접근하기 위해서는, FCM이 제공하는 비공개 키가 있어야 한다.
이 비공개 키는 FCM 웹사이트에서 '서비스 계정' 탭에서 다운받을 수 있으니 다운받으십쇼..

 

그리고 이 비공개 키를 resources 하위 폴더에 이동시키면 된다.
나는 firebase-messaging-sw.js와 같은 위치인 /resources/static에 넣어 주었다.

 

그리고 이제, 프로그램이 가동될 때마다 Firebase를 초기화해줄 수 있도록 Initializer 함수를 만들어 줘야 한다.
나는 그냥 FcmService.java에 추가해 줬다.

< FcmService.java >

@PostConstruct
public void fcmInitialize() throws IOException {
    ClassPathResource key = new ClassPathResource("비공개 키 json 파일의 경로");

    try (InputStream serviceAccount = key.getInputStream()) {
        FirebaseOptions options = new FirebaseOptions.Builder()
                .setCredentials(GoogleCredentials.fromStream(serviceAccount))
                .setDatabaseUrl("*********************")
                .build();

        if (FirebaseApp.getApps().isEmpty()) {
            FirebaseApp.initializeApp(options);
        }
    }
}

이렇게 해 주면 FcmService가 스캔될 때 해당 메서드가 실행돼서, firebase가 초기화 된다.

 

이제 Firebase 라이브러리를 동작시킬 수 있는데, FirebaseMessaging에서 제공하는 send()를 이용하면 푸시 알림을 보낼 수 있다.

< FcmService.java >

@Transactional
public void sendPush(SendPushDTO sendPushDto) throws FirebaseMessagingException {
    String username = sendPushDto.getUsername();
    Account user = accountRepository.findByUsername(username);
    if (user == null) { // 비정상적인 유저
        return;
    }

    String token = fcmtokenRepository.findByUser(user).getToken();
    String title = sendPushDto.getTitle();
    String content = sendPushDto.getContent();

    // 메시지 객체 생성
    Notification notification = Notification.builder()
            .setTitle(title)
            .setBody(content)
            .build();
    Message message = Message.builder()
            .setNotification(notification)
            .setToken(token)
            .build();

    FirebaseMessaging.getInstance().send(message);
}

 

 

이제 푸시 알림을 보내는 메서드는 다 만들었다!
푸시 알림을 보내고 싶을 때마다 이 sendPush() 함수를 동작시키면 된다.

그래서 아래와 같은 테스트 jsp를 만들어 보았다.

input 박스에 원하는 메시지를 적고, 푸시 전송 테스트를 누르면 /sendPush로 POST 요청이 들어간다.
해당 컨트롤러는 아래와 같다.

@PostMapping("/sendPush")
public int sendPush(@RequestBody SendPushDTO sendPushDto) throws FirebaseMessagingException {
    fcmService.sendPush(sendPushDto);
    return 1;
}

이렇게 POST 요청이 들어오면 sendPush()를 호출하는 것!


그러면 아래와 같이 성공적으로 테스트 완료된다.

 

이제 푸시 알림 로직 자체는 구현 완료했다.

다음 번에는 저번 포스트에서 구현했던 @Schedule 어노테이션을 활용해서, 목적지 전역을 출발하면 푸시 알림을 보내도록 해보겠다!


 

 

PWA 환경에서 푸시 알림 구현하기 (Spring Boot, FCM, Redis)

 

headf1rst.github.io

 

 

FCM 알아보기

FCM 알아보기

musma.github.io

 

 

[FCM] #3. Firebase 메시지 전송

[FCM] #1. 소개 및 메시지[FCM] #2. Firebase 프로젝트 생성 및 Android 앱 FCM 설정[FCM] #3. Firebase 메시지 전송[FCM] #4. Android 메시지 처리 일반적인 FCM(Firebase Cloud Message) 발송은 앱 서버에서 FCM 서버로 메시지

team-platform.tistory.com