티스토리 뷰

이번 포스팅에서는 FCM을 이용해서 특정 시간에 웹으로 알림을 띄워주는 API를 만들어보도록 하겠다. 

 

 

준비

여기를 들어가서 프로젝트를 생성한다. 마음에 드는 이름을 정하고 계속을 누른다. 

 

 

바로 계속 버튼을 누른다. 

 

 

계정은 기본 계정으로 한다. 새 계정을 만들어도 상관 없을 것 같다. 그리고 프로젝트 만들기를 클릭하자.

 

 

그럼 아래 화면이 뜨는데, 웹(네모 박스)를 클릭해주자. 

 

 

앱 닉네임도 아무렇게나 지어도 된다.

 

 

<script> 태그로 바꿔주고 복사한다. 그리고 콘솔로 이동 버튼을 눌러준다. 

 

 

콘솔로 이동하면 방금 만들었던 웹 앱이 추가되어있다. 여기서 설정 모양을 클릭한다. 

 

 

서비스 계정으로 이동해서 스니펫을 자바로 선택한 다음 새 비공개 키 생성 버튼을 눌러 json 파일을 다운받는다. 

 

 

다운 받은 json파일은 인텔리제이 resources 폴더 밑에 이동시킨다. json 파일은 민감한 정보가 담겨있으니 깃으로 푸시한다면 gitignore에서 해당 파일을 숨겨두어야 좋다. 

 

 

그리고 FCM을 실행시키는 코드를 짜보자. 

 

@Service
@Slf4j
public class FCMInitializer {

    private static final String FIREBASE_CONFIG_PATH = "mojaty-e4362-firebase-adminsdk-juq3h-9d6c888b5c.json";

    @PostConstruct
    public void initialize() {
        try {
            GoogleCredentials googleCredentials = GoogleCredentials
                    .fromStream(new ClassPathResource(FIREBASE_CONFIG_PATH).getInputStream());
            FirebaseOptions options = new FirebaseOptions.Builder()
                    .setCredentials(googleCredentials)
                    .build();
            FirebaseApp.initializeApp(options);
        } catch (IOException e) {
            log.info(">>>>>>>>FCM error");
            log.error(">>>>>>FCM error message : " + e.getMessage());
        }
    }
}

 

 

ClassPathResource()는 resources 폴더 아래에 있는 괄호 안 경로를 찾는다. 우리가 이동시킨 json파일을 찾아서 맞는 정보인지 확인한 후 FirebaseApp.initializeApp()을 통해서 실행한다. 서버 실행시에 딱 한번 실행되어야 하니 @PostConstruct 어노테이션을 붙여준다. 여기까지 했다면 모든 준비는 끝이 났다. 

 

 

구현

그럼 이제 구현을 해보자. 결론부터 말하자면 흐름은 아래와 같다. 

 

1. 프론트에서 firebase를 시작하게 되면 알림을 허용하겠습니까? 라는 작은 팝업창이 뜬다.

2. 사용자가 허용을 누르면 토큰이 생성된다. 해당 토큰을 서버로 보낸다. 

2. 서버는 토큰을 데이터베이스에 저장한다. 

3. 특정 시간에 메세지를 보낸다. 

4. 사용자가 로그아웃 할 시 토큰도 같이 삭제된다. 

 

먼저 토큰을 데이터베이스에 저장하는 로직부터 구현해보자. 토큰을 아래 url로 보낸다. 토큰 발급은 프론트에서 해주어야 하는데, 맨 마지막에 작성하겠다. 

 

@RestController
@RequiredArgsConstructor
@RequestMapping("/notification")
public class NotificationApiController {

    private final NotificationService notificationService;

    @PostMapping("/new")
    public void saveNotification(@RequestBody String token) {
        notificationService.saveNotification(token);
    }
}

 

 

saveNotification을 보면 Notification 엔티티 객체를 저장한다. 

 

@Service
@RequiredArgsConstructor
@Slf4j
public class NotificationService {

    private final NotificationRepository notificationRepository;
    private final UserRepository userRepository;

    @Transactional
    public void saveNotification(String token) {
        User user = userRepository.findByEmail(SecurityProvider.getLoginUserEmail())
                .orElseThrow(() -> new CustomException(ErrorCode.RETRY_LOGIN));

        Notification notification = Notification.builder()
                .token(token)
                .build();

        notification.confirmUser(user);
        notificationRepository.save(notification);
    }
}

 

Notification 엔티티를 확인해보자. User 엔티티와 1대1 단방향 매핑을 한다.

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Notification {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "notification_id")
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    private String token;

    @Builder
    public Notification(String token) {
        this.token = token;
    }

    public void confirmUser(User user) {
        this.user = user;
    }
}

 

이제 메세지를 보내는 로직만 짜면 된다. 이 포스트에서는 특정 시간에 메세지를 보내도록 구현할 것이다. 먼저 Application에 스케쥴링을 사용하기 위한 어노테이션을 추가한다.

 

@EnableScheduling

 

총 로직은 다음과 같다. 

 

@EnableJpaAuditing
@SpringBootApplication
@EnableScheduling
public class MojatyApplication {

	public static void main(String[] args) {
		SpringApplication.run(MojatyApplication.class, args);
	}

}

 

그럼 스케쥴링을 구현해보자. cron은 사용자가 정의함에 따라서 쉽게 시간을 설정할 수 있다. 해석하면 7시 30분에 해당 메서드가 돌아간다. 자세한 내용은 Baeldung을 참고하자. 

 

@Component
@RequiredArgsConstructor
@Slf4j
public class ScheduledService {

    private final NotificationService notificationService;

    @Scheduled(cron = "0 30 7 * * ?")
    public void scheduledSend() throws ExecutionException, InterruptedException {
        NotificationRequestDto notificationRequestDto = NotificationRequestDto.builder()
                .title("Mojaty")
                .token(notificationService.getNotificationToken())
                .message("공부할 시간입니다!")
                .build();
        notificationService.sendNotification(notificationRequestDto);
        log.info("보내졌습니다!");
    }
}

 

메세지를 담을 DTO를 생성한다. 푸시알람이기 때문에 사용자가 메세지 내용을 적지 않고 서버에서 적어주었다. NotificationRequestDto는 메세지 제목과 내용, 그리고 토큰을 가진다. 

 

@Getter
@NoArgsConstructor
public class NotificationRequestDto {

    private String title;
    private String message;
    private String token;

    @Builder
    public NotificationRequestDto(String title, String message, String token) {
        this.title = title;
        this.message = message;
        this.token = token;
    }
}

 

토큰을 가져오는 getNotificationToken을 봐보자. 사용자로 Notification을 찾아서 토큰을 반환한다. 

 

    public String getNotificationToken() {
        User user = userRepository.findByEmail(SecurityProvider.getLoginUserEmail())
                .orElseThrow(() -> new CustomException(ErrorCode.RETRY_LOGIN));

        Notification notification = notificationRepository.findByUser(user)
                .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

        return notification.getToken();
    }

 

NotificationRepository

 

public interface NotificationRepository extends JpaRepository<Notification, Long> {

    Optional<Notification> findByUser(User user);
}

 

sendNotification은 다음과 같다. 

 

    public void sendNotification(NotificationRequestDto req) throws ExecutionException, InterruptedException {
        Message message = Message.builder()
       			.setWebpushConfig(WebpushConfig.builder()
                        .setNotification(WebpushNotification.builder()
                                .setTitle(req.getTitle())
                                .setBody(req.getMessage())
                                .build())
                        .build())
                .setToken(token)
                .build();
                
                

        String response = FirebaseMessaging.getInstance().sendAsync(message).get();
        log.info(">>>>Send message : " + response);
    }

 

여기서 주의할 점은 Message import다. 

 

import com.google.firebase.messaging.Message;

 

사용자가 로그아웃 할 시 토큰도 함께 삭제해준다. 

 

    @DeleteMapping("/logout")
    public void logout(HttpServletRequest req, HttpServletResponse res) {
        LogoutResponseDto dto = authService.logout(req);
        notificationService.deleteNotification();
        res.addCookie(dto.getAccessToken());
        res.addCookie(dto.getRefreshToken());
    }

 

deleteNotification은 다음과 같다. 

 

    public void deleteNotification() {
        User user = userRepository.findByEmail(SecurityProvider.getLoginUserEmail())
                .orElseThrow(() -> new CustomException(ErrorCode.RETRY_LOGIN));

        Notification notification = notificationRepository.findByUser(user)
                        .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

        notificationRepository.delete(notification);
    }

 

 

프론트

서버단에서는 API를 완성했으니 프론트에서 사용자 기기 토큰을 보내주기만 하면 끝이 난다. 코드는 아래와 같다. 

 

import { initializeApp } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import APIController from "../hooks/useAPI";

const firebaseConfig = {
	sdk
};

const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);

//토큰값 얻기
getToken(messaging, {
  vapidKey: process.env.REACT_APP_VAPID_KEY,
})
  .then((currentToken) => {
    if (currentToken) {
      console.log(currentToken);

      if (
        localStorage.getItem("FCMtoken") === null ||
        localStorage.getItem("FCMtoken") !== currentToken
      ) {
        APIController.post("/notification/new", { token: currentToken })
          .then((res) => {
            console.log("FCM 토큰 보내기 성공!", res);
            localStorage.setItem("FCMtoken", currentToken);
          })
          .catch((err) => console.log("FCM 토큰 보내기 실패!", err));
      }
    } else {
      console.log(
        "No registration token available. Request permission to generate one."
      );
    }
  })
  .catch((err) => {
    console.log("에러 : ", err);
  });

//포그라운드 메시지 수신
onMessage(messaging, (payload) => {
  console.log("받은 알람 : ", payload);
});

 

sdk라고 적혀있는 쪽을 설명하겠다. 포스팅 처음부분에 만들었던 웹 앱에서 설정을 눌러 들어간다. 

 

 

밑으로 내려보면 SDK가 있는데 붙여넣기를 하면 된다.  

 

 

그럼 잘 돌아가는 것을 볼 수 있다! 

 

추가로, 메세지를 이것저것 커스텀하면서 알게된 점은, 아이콘이랑, 뱃지는 완벽하게 대치가 안된다. 아마 완벽하게 대치하려면 돈을 내야하는 것 같다. 그리고 포그라운드에서는 메세지를 콘솔로만 받을 수 있다. 왜냐하면 보낼 때 서버에서 푸시알람 방식으로 보내기 때문이다. 

 

  • 포그라운드 : 사용자가 우리가 만든 웹사이트에 접속중일 때
  • 백그라운드 : 사용자가 우리가 만든 웹사이트를 끄고 다른 것을 할 때 (ex: 유튜브)
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday