이번 프로젝트 채팅을 통한 FCM 웹 알림 서비스를 진행했습니다.
지식 전파를 위해 포스팅합니다.
FCM이란?
FCM은 Firebase Cloud Messaging으로 메세지를 안정적으로 클라이언트 인스턴스에게 전송할 수 있는 교차 플랫폼 메시징 솔루션
- FCM은 교차 플랫폼 메시징 서비스이기 때문에 메세징을 클라이언트 플랫폼(Web, IOS, Android) 환경별로 개발 할 필요가 없어지므로 플랫폼에 종속되지 않고 메세지를 전송할 수 있어 구현의 복잡성을 낮춰준다.
- 또한 만약 클라우드 메세징 서버 없이 서버 -> 클라이언트 메세지를 전송하는 구현부터 클라이언트가 계속해서 접속해야하기 때문에 네트워크 효율 문제가 발생한다.
FCM의 구성요소와 작동원리
FCM 작동에 필요한 두가지 주요 구성요소가 있다.
- Firebase용 Cloud Functions 또는 앱 서버와 같이 메세지를 작성, 타겟팅, 전송할 수 있는(혹은 신뢰할 수 있는) 환경
- 해당 플랫폼별 전송 서비스를 통해 메세지를 수신하는 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을 보내지않은 경우)
공통! (중요)
- 알람을 구현했는데 저렇게 뜨지 않는다면 설정을 해줘야합니다. 저는 이거 안하고 계속 왜 안되는건가 생각했습니다..
맥북
알람 설정 참고
'Spring' 카테고리의 다른 글
Spring Security Architecture (Spring MVC 기반) (0) | 2024.08.25 |
---|---|
@Bean과 @Component의 차이 (0) | 2024.08.25 |
[Spring Security] 다중 UserDetailsService 사용하기 (1) | 2024.08.04 |
[Security] Spring Security 이모저모 정리 (2) | 2024.07.28 |
스프링내맘대로정리하기 1-2편 (환경 구성, 연관 관계) (0) | 2024.07.28 |