Spring

기몽수의 FCM 웹 알림 서비스

기몽수 2024. 8. 17. 13:01

이번 프로젝트 채팅을 통한 FCM 웹 알림 서비스를 진행했습니다.

 

지식 전파를 위해 포스팅합니다.

 

FCM이란?

FCM은 Firebase Cloud Messaging으로 메세지를 안정적으로 클라이언트 인스턴스에게 전송할 수 있는 교차 플랫폼 메시징 솔루션

  • FCM은 교차 플랫폼 메시징 서비스이기 때문에 메세징을 클라이언트 플랫폼(Web, IOS, Android) 환경별로 개발 할 필요가 없어지므로 플랫폼에 종속되지 않고 메세지를 전송할 수 있어 구현의 복잡성을 낮춰준다.
  • 또한 만약 클라우드 메세징 서버 없이 서버 -> 클라이언트 메세지를 전송하는 구현부터 클라이언트가 계속해서 접속해야하기 때문에 네트워크 효율 문제가 발생한다.

 

FCM의 구성요소와 작동원리

FCM 작동에 필요한 두가지 주요 구성요소가 있다.

  1. Firebase용 Cloud Functions 또는 앱 서버와 같이 메세지를 작성, 타겟팅, 전송할 수 있는(혹은 신뢰할 수 있는) 환경
  2. 해당 플랫폼별 전송 서비스를 통해 메세지를 수신하는 IOS, Android 또는 웹(JavaScript) 클라이언트 앱

동작원리

1. 클라이언트가 FCM 서버에서 디바이스 별 고유하게 발급되는 FCM 토큰을 발급 받는다.

2. 서버는 클라이언트로부터 해당 토큰을 전달받고 저장한다.( 저는 레디스를 사용했습니다. )

3. 특정 상황에 메세지를 토큰에 담아서 FCM Backend에 전송한다.

4. FCM Backend는 토큰을 발급 받은 클라이언트 앱에 메세지를 전송한다.

 

 

FCM의 두가지 Token

Fcm Token: 사용자의 기기를 구반하기 위한 토큰

  • 알람을 보낼 때 어떤 기기에 보내야할지 모르기 때문에 구분하기 위해 보내는 토큰이다.
  • 프론트에서 FCM(Firebase Cloud Message 이하 firebase) 서버에서 얻어야하는 값이다.
  • 얻은 FCM 토큰 값을 서버에 저장 요청을 하여 서버에서는 알림을 보낼 때 누구에게 보내야할지 식별할 수 있다.

 

Access Token: FCM 서버에 요청을 보내기 위해 필요한 값( 온전한 서버인지 확인하는 토큰 )

공식문서에 있는 FCM 서버에 요청을 보내기 위한 3가지 전략

  • Goole Application 기본 사용자 인증 정보(ADC)
  • 서비스 계정 Json 파일
  • 서비스 계정에서 생성된 수명이 짧은 OAuth2.0 액세스 토큰

 


 

리액트(클라이언트) 측에서 구현은 파이어베이스 서버에 접근하여 FCM Token에 접근 후 백엔드로 요청을 하면 되기에 넘어가겠습니다.

 

FCM을 Spring Boot 프로젝트에 적용하기

 

- Firebase-admin 의존성 추가

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

 

 

그 후 Firebase 콘솔에 접속해서 로그인 한 후 프로젝트 생성

프로젝트 설정 -> 서비스 계정 항목 새 비공개 키 생성

- 생성된 admin sdk는 Json 파일로 생성되며, 생성된 파일을 프로젝트의 resource 디렉토리로 이동

-> 이 때 생성된 json 파일을 .gitignore에 추가해서 올라갈 수 없도록 합니다.

 

 

FCM Config.java

@Slf4j
@Configuration
@RequiredArgsConstructor
public class FCMConfig {

    // Firebase 설정 JSON 파일의 경로
    @Value("${fcm.config.path}")
    private String FIREBASE_CONFIG_PATH;

    @Bean
    FirebaseMessaging firebaseMessaging() {
        // 클래스패스에서 Firebase 설정 파일을 리소스로 불러옴
        ClassPathResource resource = new ClassPathResource(FIREBASE_CONFIG_PATH);

        // JSON 파일에서 자격 증명을 불러와 Firebase 옵션을 설정
        try (InputStream serviceAccount = resource.getInputStream()) {
            FirebaseOptions options = FirebaseOptions.builder()
                    .setCredentials(GoogleCredentials.fromStream(serviceAccount))
                    .build();

            FirebaseApp firebaseApp;

          // FirebaseApp 인스턴스가 초기화되지 않은 경우 초기화, 이미 존재하는 경우 해당 인스턴스를 가져옴
            FirebaseApp firebaseApp = FirebaseApp.getApps().isEmpty() ?
                    FirebaseApp.initializeApp(options) :
                    FirebaseApp.getInstance();
                    
            log.debug("firebase 불러오기 성공");
            // 설정된 FirebaseApp 연결된 FirebaseMessaging 인스턴스를 반환
            return FirebaseMessaging.getInstance(firebaseApp);
        } catch (IOException e) {
            throw new CustomException(ErrorCode.SERVER_ERROR, "Firebase Key 불러오기 실패 ");
        }
    }


}

1. 해당 json 파일을 읽어와 Firebase 옵션을 설정합니다.

2. 이 때 배포 환경에서는 파일 경로가 다르기에 클래스패스를 이용하여 파일을 불러오도록합니다.

3. 해당 Firebase 인스턴스가 존재하는 경우에도 초기화하는 경우가 생겨 다르게 표기하였습니다.

 

 

 

FCMTokenDto.java

@Data
    public static class FcmToken{
        String token;
    }

UserController.java

@PostMapping("/fcm")
    @Operation(summary = "사용자 FCM 토큰 저장", description = "사용자의 FCM 토큰을 저장합니다.")
    public SuccessResponse<Void> saveFCMToken(
            @RequestBody FcmToken token,
            @CurrentUser UserAuthDto user
    ) {
        log.debug("{}", token);
        userService.saveFcmToken(user.getId(), token.getToken());
        userService.sendLoginAlarm(user.getId());
        return SuccessResponse.empty();
    }

    @DeleteMapping("/fcm")
    @Operation(summary = "사용자 FCM 토큰 제거", description = "사용자의 FCM 토큰을 제거합니다.")
    public SuccessResponse<Void> removeFCMToken(
            @CurrentUser UserAuthDto user
    ) {
        userService.removeToken(user.getId());
        return SuccessResponse.empty();
    }

1. 서버에서 유저 로그인시 FCM 토큰을 발급 받아 레디스에 저장하도록 구현했습니다. 로그아웃시에는 데이터베이스내의 토큰을 제거했습니다.

2. 만약 로그아웃시 토큰을 데이터베이스에 남겨둔다면, 로그아웃된 디바이스에 남아있는 토큰으로 인해 메세지를 전송할 수 있기 때문입니다.

 

 

FCMService.java

@Slf4j
@Service
@RequiredArgsConstructor
public class FCMService {
    private void sendNotification(String targetToken, String title, String body, String image) {
        WebpushConfig webpushConfig = WebpushConfig.builder()
                .putData("title", title)
                .putData("body", body)
                .putData("image", image)
                .build();

        Message message = Message.builder()
                .setToken(targetToken)
                .setWebpushConfig(webpushConfig)
                .build();
        try {
            String response = firebaseMessaging.sendAsync(message).get();
            log.debug("Send message: " + response);
        } catch (Exception e) {
            log.error("FCM 전송 오류 ", e);
        }
    }
}

- 단순히 메세지 전송을 보여드리기 위해서 간단하게만 작성하였습니다. 

- 해당 메세지 알람 전송시 응답을 기다리는 것을 막기 위해서 sendAsync를 통해 비동기로 알람을 전송하였습니다.

- 에러처리로 인해 다른 서비스가 막히지 않도록 별 다른 예외를 던지지않았습니다.

 

 

 

알람 결과

- 현재 이미지는 따로 설정해두지 않아서 보이는 부분입니다.

- 맥북은 크롬 아이콘이 뜨고 윈도우는 안뜨는데 저 크롬 아이콘을 안보이게 할 순 없습니다.

 


 

주의 할 점

백엔드로직으로만 확인 할 수 있는게 아니라서 간단한 리액트 코드 정도는 구현해야합니다.

제가 구현하면서 신경썼던 부분 적어보겠습니다.

클라이언트 해야하는 작업

1. FCM 서비스 워커 등록하기

2. FCM 초기화

3. 토큰 전송 전 미리 저장하기 ( 로그인 후 FCM 토큰을 서버로 전송해야하는데 이 때 느려지는 것 같더라구요. 저는 Zustand에 저장했었습니다.) 

4. 사용자가 알림을 허용하지 않으면 토큰을 보내지 않기

 

백엔드에서 해야하는 작업

1. 해당 알림 전송시 토큰이 없다면 그냥 알림 전송을 안해야합니다.

2. 채팅 알림 전송시 상태 확인 잘하기

- sender가 받은 채팅을 receiver이 알림을 받을 수 있는 상태인가? (ex. 이미 채팅방에 참여한 경우, FCM Token을 보내지않은 경우)

 

공통! (중요)

- 알람을 구현했는데 저렇게 뜨지 않는다면 설정을 해줘야합니다. 저는 이거 안하고 계속 왜 안되는건가 생각했습니다..

 

맥북

 

 

알람 설정 참고

https://www.daangn.com/wv/faqs/3304