Firebase는 웹과 모바일 개발에 필요한 기능을 제공하는 BaaS(BackEnd as a Service) 이다. 백엔드 서버의 인프라들을 제공해주고 많은 기능들을 지원한다.
https://console.firebase.google.com/
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/6.5.0/firebase-app.js"></script>
<!-- TODO: Add SDKs for Firebase products that you want to use
https://firebase.google.com/docs/web/setup#config-web-app -->
<script>
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "AIzaSyBWbLdjemTbpsGckTlyDqaATpbI_0usdHw",
authDomain: "woowa-turkey.firebaseapp.com",
databaseURL: "https://woowa-turkey.firebaseio.com",
projectId: "woowa-turkey",
storageBucket: "",
messagingSenderId: "732784692174",
appId: "1:732784692174:web:555e23a165bac677"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
</script>
생성한 프로젝트 페이지 > 설정 > 서비스 계정항목
{
"type": "service_account",
"project_id": "woowa-turkey",
"private_key_id": "ef30ede0806a64f1acb2a35e3dd8ebd858cae458",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvQgYywH82jL1R\nTlNIWDW1txvZmdMa8vAKePPJioXAZGuAP4HY/+prbEGHKMPE+kml5fK9Mw8JNRb6\n7t9napjcTSSvPpN+1SBPTFomxVx7ko8ktjCIbMUjp7Dpeg7ePRUE/4m+RIIAJPSh\nhG2kvqMfIMM/9RoYGqfzo1BO503/U3Kzhjz9pWtyiWpXv0dAgVEVobKm8EzWSS/y\n3Frxm10pS2jMmuMHaBnReqPa0Db344sl1Q/4SPB6g6mKCsBvx5EoN+WoAmFkuiMu\noIptT6JKMECZQIIarO6ylwyjGC/k5U6cL/hNeqewpTNViRYQk006B5L0r3JevyLU\nhU7qNHBbAgMBAAECggEADqlE0HkXC06xxbv6Lpqmf5meXFFIfkAlgZLFvi3263Xb\nImLtr2QVTcBEKRTpb7CKebTnXhmH3/rPfdV+3e1mdUOE1p4zA5C7v7aA8i5fgrzr\neaQ+IiOLSGTOYXHDDadpez4PXVno0C9HQb3M2FPUXJXGj7Km6K5OL40RelBST5Id\nQ4bbcLO2g/NazRhujBiF/Cp2op/si99BqNJcZTZ1AkupUZ9u4tfQd+fZ2TuAUUxn\n5EvPKOK/kd6s0XizBgwdDcPUJ+GE4RyLyS6p2qeRIvQYQdEf4MY9qHC9Gpgc6kc4\nh5pm9hrnf+Xcmpb2bfs+bZLeVtIOReeFMTHq9P+W/QKBgQDzyX0EOdqRBKEntZ73\nIruueVWF66JDoyRO2nj3pwmlsnzDOg7mGc5DCa/Q2OrOeuPQc0A/nLChZcpGQNa2\nnVgZJQLKC0YFuzwiw0wsALaOFGkm3I0Ws0BU0DloMKETaGVm7LYiZhWABXDWmNHb\ngsGKQBD8J6/2CIoKTts8yrZV7wKBgQC4CapjQOzcFE639FH3I7jiStecxRWfHtdj\nmfaUpLgKxxzsiLq0eC3wGgdl2zGcSAOejhSeFSNRsLesupNXUpSt4Tg9dKQKqkob\nJBOsLaJ/XzU388gwdR+VHOzH5UySlKJT1kgmbWnLzrjJU47Ed6er0x+RMEw5E2vX\nDAtrtZyYVQKBgQCmkhZci9ceXSZrxnn+bHoGZZsc7Pqq8k59nmMm+7fowoLitcm2\nSQ2Y2oaJ0ZzmRitgo07T6BD7xlwmEmlizzGBxLsaKAfJJLybGAv0yOvzOlj6l5nD\nV+jtynRfD1MPurYGVFjCOjQYjB8kUbiaHsZ02v9/+vIMzprbHjRIXk2lewKBgHWc\nZ7FKWpRCvhO9JWyE/bBQF/5tzOWFdiyGxhg0NBFfMdLPhlGyw9i/KZ8kN04ij32S\nWwazh4C7KIZQLb0OX0nHtoM/uV2/eeBia9gKyeAQXhhFyNd+4hIPDBqrJQY/yoU9\nWZqFLak7kMfIiGJx/UtHftLwRKXtDWu+nMXWqB6JAoGARb+KXnhDXpLhqp+bVmMQ\n9WkmVdSsZNrHVhzqW4Hbgl/Hkn8gwrgCXXUU7ojKWmVR2AIlZE/50xQPBphqPlFQ\nCpiJ/tljCL0+egXaxVm7Z0Jdjxurz2Bd7B5J+W6ekwpM/aP8ykjEmL3kc6blBobq\nxsixpOSn5c5MzbU746t+j5Y=\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fplgj@woowa-turkey.iam.gserviceaccount.com",
"client_id": "109761708146793381222",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fplgj%40woowa-turkey.iam.gserviceaccount.com"
}
dependencies {
implementation 'com.google.firebase:firebase-admin:6.8.1'
}
Step3 에서 생성한 비밀키 파일을 가져올 수 있도록 이동한다. 원래 깃허브와 같이 공개된 장소에 이 파일을 같이 올리는게 안전하지 않다고 한다. 이번 프로젝트에서는 resource 디렉토리에 저장했다.
어플리케이션이 시작될 때 Firebase 프로젝트에 앱을 등록해줘야 한다. (두번 등록 되면 에러가 나므로 시작할 때 초기화 해준다.) @PostConstruct 어노테이션을 활용하여 Bean Object가 생성되고, DI 작업까지 마친 다음 실행시키도록 한다.
@Service
public class FCMInitializer {
private static final Logger logger = LoggerFactory.getLogger(FCMInitializer.class);
private static final String FIREBASE_CONFIG_PATH = "woowa-turkey-firebase-adminsdk.json";
@PostConstruct
public void initialize() {
try {
FirebaseOptions options = new FirebaseOptions.Builder()
.setCredentials(GoogleCredentials.fromStream(new ClassPathResource(FIREBASE_CONFIG_PATH).getInputStream())).build();
if (FirebaseApp.getApps().isEmpty()) {
FirebaseApp.initializeApp(options);
logger.info("Firebase application has been initialized");
}
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
.setCredentials(GoogleCredentials.fromStream(new ClassPathResource(FIREBASE_CONFIG_PATH).getInputStream()))
위 라인을 통해 다운받은 비밀키를 가져와서 증명한다.
메세지를 보낼 때 필요한 요소는 두가지이다.
(FCM 메세지 참조 : https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages)
아래와 같이 Message를 보내는 별도의 서비스를 만들었다. NotificationRequest에는 받을 상대의 토큰값, 푸시될 알림 메세지와 제목을 가지고 있고, 사용은 하지 않았지만 이미지도 지정해서 넣어줄 수 있다.
@Service
public class FCMService {
private static final Logger logger = LoggerFactory.getLogger(FCMService.class);
public void send(final NotificationRequest notificationRequest) throws InterruptedException, ExecutionException {
Message message = Message.builder()
.setToken(notificationRequest.getToken())
.setWebpushConfig(WebpushConfig.builder().putHeader("ttl", "300")
.setNotification(new WebpushNotification(notificationRequest.getTitle(),
notificationRequest.getMessage()))
.build())
.build();
String response = FirebaseMessaging.getInstance().sendAsync(message).get();
logger.info("Sent message: " + response);
}
}
Message Builder를 통해 만들어진 메세지를 정상적으로 보낸다면 response에는 projects/woowa-turkey/messages/0:1568180254610207%cc9b4facf9fd7ecd
과 같은 message의 값을 리턴받는다.
푸시 알림은 사용자와 서버가 서로 주고받는 과정으로 통신하는게 아니라 서버에서 일방적으로 사용자에게 보내줘야 한다. 따라서 사용자의 토큰을 서버가 관리하고 있어야 한다.
토큰 생성
NotificationApiController.class
@RestController
public class NotificationApiController {
private final NotificationService notificationService;
public NotificationApiController(NotificationService notificationService) {
this.notificationService = notificationService;
}
@PostMapping("/register")
public ResponseEntity register(@RequestBody String token, @LoginUser UserSession userSession) {
notificationService.register(userSession.getId(), token);
return ResponseEntity.ok().build();
}
}
사용자가 로그인 된후 Firebase에게 전달받은 token 값을 웹서버에게 등록한다.
NotificationService.class
@Service
public class NotificationService {
...
private final Map<Long, String> tokenMap = new HashMap<>();
...
public void register(final Long userId, final String token) {
tokenMap.put(userId, token);
}
}
사용자의 Id값을 Key, 토큰 값을 Value로 갖는 Map을 사용해서 토큰 값을 관리한다.
토큰 사용
로그인 된 유저에게만 푸시를 보내줘야한다.
private void createReceiveNotification(User sender, User receiver) {
if (receiver.isLogin()) {
NotificationRequest notificationRequest = NotificationRequest.builder()
.title("POST RECEIVED")
.token(notificationService.getToken(receiver.getId()))
.message(NotificationType.POST_RECEIVED.generateNotificationMessage(sender, receiver))
.build();
notificationService.sendNotification(notificationRequest);
}
}
private void createTaggedNotification(User sender, List<User> receivers) {
receivers.stream()
.filter(User::isLogin)
.forEach(receiver -> {
NotificationRequest notificationRequest = NotificationRequest.builder()
.title("POST TAGGED")
.token(notificationService.getToken(receiver.getId()))
.message(NotificationType.POST_TAGGED.generateNotificationMessage(sender, receiver))
.build();
notificationService.sendNotification(notificationRequest);
});
}
토큰 삭제
로그아웃 할 때 Map에서 삭제해준다.
@PostMapping("/logout")
public ResponseEntity logout(@LoginUser UserSession userSession, HttpSession httpSession) {
loginService.logout(userSession.getId());
notificationService.deleteToken(userSession.getId());
httpSession.removeAttribute(USER_SESSION_KEY);
return ResponseEntity.ok().build();
}
<script src="https://www.gstatic.com/firebasejs/5.9.2/firebase.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.9.2/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.9.2/firebase-messaging.js"></script>
const firebaseModule = (function () {
async function init() {
// Your web app's Firebase configuration
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/firebase-messaging-sw.js')
.then(registration => {
var firebaseConfig = {
apiKey: "AIzaSyBWbLdjemTbpsGckTlyDqaATpbI_0usdHw",
authDomain: "woowa-turkey.firebaseapp.com",
databaseURL: "https://woowa-turkey.firebaseio.com",
projectId: "woowa-turkey",
storageBucket: "",
messagingSenderId: "732784692174",
appId: "1:732784692174:web:555e23a165bac677"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
// Show Notificaiton Dialog
const messaging = firebase.messaging();
messaging.requestPermission()
.then(function() {
return messaging.getToken();
})
.then(async function(token) {
await fetch('/register', { method: 'post', body: token })
messaging.onMessage(payload => {
const title = payload.notification.title
const options = {
body : payload.notification.body
}
navigator.serviceWorker.ready.then(registration => {
registration.showNotification(title, options);
})
})
})
.catch(function(err) {
console.log("Error Occured");
})
})
})
}
}
return {
init: function () {
init()
}
}
})()
firebaseModule.init()
Step 2에서 받은 자바스크립트 스니펫을 사용하여 초기화 시켜준다. messaging.requestPermission()
을 사용하면 다음과 같은 권한 요청 화면이 나온다.
messaging.getToken()
을 통해 받은 토큰 값을 서버에 보내서 등록해준다.
importScripts('https://www.gstatic.com/firebasejs/5.9.2/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/5.9.2/firebase-messaging.js');
firebase.initializeApp({
messagingSenderId: "732784692174"
});
const messaging = firebase.messaging()
서비스 워커는 브라우저의 백그라운드에서 실행하는 스크립트로, 웹페이지와는 별개로 작동된다. 백그라운드 동기화나 푸시 알림 기능 등을 가능하게 한다.
https://developers.google.com/web/fundamentals/primers/service-workers/?hl=ko
notification.js 의 navigator.serviceWorker.register('/firebase-messaging-sw.js')
에서 서비스 워커를 등록해줄 수 있다.
Chrome - 개발자도구 - Application 탭 - Service Workers 에서 등록된 서비스워커를 볼 수 있다.
크롬에서는 서비스 워커를 등록했는데, Firebase에서 서비스워커를 등록하지 못하고 404 에러가 나옴.
서비스워커를 서버의 루트에 위치시켜야함. 현재 상황으로 예를 들면 firebase-messaging-sw.js가 static 디렉토리에 있어서 localhost:8080/firebase-messaging-sw.js 로 접근 가능하도록 만들었음.
Firebase에 대한 자료와 예제 코드를 찾아보는데 대부분이 안드로이드 환경이었다. 그리고 참고할 사이트가 많지 않다보니 사소한 에러들이 비일비재했다.
참고
https://golb.hplar.ch/2018/01/Sending-Web-push-messages-from-Spring-Boot-to-Browsers.html
/녕하세요. NotificationRequest 클래스는 어떻게 구현되어있는지 알 수 있을까요?