오늘 배운 것

어제 개발하던 알림 기능을 마저 개발해보려고 한다. 어제 job과 관련된 함수는 작성해 두었으니 이제 settings.py 파일에서 CRONJOBS 변수로 해당 함수를 등록하면 된다. 

 

crontab schedule에 관한 문서를 참고해서 아침 알림은 매일 오전 8시, 오후 알림은 오후 2시, 저녁 알림은 오후 8시에 가도록 설정해 주었다. 

# settings.py

CRONJOBS = [
    ('0 8 * * *', 'todos.jobs.send_morning_alarm'),
    ('0 14 * * *', 'todos.jobs.send_afternoon_alarm'),
    ('0 20 * * *', 'todos.jobs.send_evening_alarm'),
]

 

이제 두 번째 알림 케이스를 작업하면 된다. 투두 CRUD 중 C, U, D API가 호출되는 경우, 해당 API를 호출한 유저가 해당 디바이스 말고 또 다른 디바이스를 갖고 있을 경우, 해당 디바이스에 다시 Read API를 호출하라는 백그라운드 알림을 보내주는 작업이다. 

 

이 로직은 Todo, SubTodo, Category 모델의 Create, Update, Delete API의 끝에 들어가는 작업이기에, 해당 뷰 함수에 반복적으로 로직을 붙이는 것은 좋지 못하다고 판단했다. 그래서 생각해 본 방법으로는 미들웨어와 시그널이 있었다. 

 

내가 이해한 시그널은 가령 DB 특정 모델의 변화를 주목하고 있다가 변화가 일어났을 때 FCM으로 해당 유저에게 노티를 주는 방식이었다. 그래서 공식문서를 조금 읽어보던 중 signal은 implicit function call로 여러 쪼개진 로직에서 같은 이벤트를 주목할 때는 효과적일 수 있지만 그만큼 디버깅을 어렵게 할 수 있다는 문구를 보았다. 

 

그런데 지금 생각해보면 middleware의 사용 목적은 요청이나 응답의 값 등을 적절히 변형하는 용도인데, 이는 FCM 알림처럼 관련 모델 값이 변경되었을 때 노티를 받는 목적과는 맞지 않는다는 생각이 들었다. 

 

그래서 디버깅을 어렵게 할 수 있다는 가능성이 있지만 일단은 signal을 사용해보기로 했다. 그게 사용 목적에 더 맞고, 시그널이 DB를 보고 있으면 데이터가 변경될 때마다 알림이 가니 목적에는 가장 부합하게 구현할 수 있겠다는 생각이 들었다. 그리고 내가 사용하려는 모델이 변경되면 그에 맞게 노티를 주는 시그널은 알고보니 장고에 기본으로 구현되어 있어서, built-in signal을 사용하면 되었다. 

 

그런데 여기서 또 다른 해답을 얻었다. 공식문서에서는 정 필요한 경우에만 signal을 사용하고 아니면 다른 방법을 찾아보라고 하고 있었다. 새삼 signal이 코드를 디버깅하기 어려울 수 있다는 단점이 생각났다. 여기서는 model signal을 사용하는 대신 model manager를 사용해서 비슷한 작업을 처리하는 방법을 추천해주고 있었다. 

 

그래서 다시 signal 대신 manager를 사용하기로 했다. Todo, SubTodo, Category 모델의 manager에서 save 또는 create, update, delete 메소드만 변형하면 되겠다고 생각했다. models.Model의 save 메소드를 override해서 save 메소드 호출 후에 fcm 알림 로직을 추가해주는 방법을 생각했다. 그런데 이렇게 되면 fcm 알림 코드가 또 save 메소드 안에 종속되지 않나... 라는 생각이 들었다. fcm 알림은 명백히 save 메소드가 호출된 다음에 실행되어야 한다. 그러므로 signal의 목적에 부합한다. 

 

여러 고민을 하게 되었는데, 디버깅의 단점이 있지만 일단은 signal을 사용하기로 했다. 다행히 built-in signal에 post-save라는 기본 signal이 제공되어 있었다. 어떻게 사용하는지 잘 와닿지 않아서 'how to use post_save signals in django'로 검색했더니 나온 덕에 해답을 얻을 수 있었다. 

 

그런데 이렇게 했더니 또 다른 문제가 있었다. model이나 signal에서는 request.user이나 request.auth의 값을 갖고 있지 않다. 그렇기 때문에 이 요청이 어떤 디바이스로부터 온 것인지 알 수 있는 방법이 없다. 다음은 임시로 작성한 코드인데, signal은 파라미터로 request를 받지 않고 있다. 

from django.db.models.signals import post_save
from django.dispatch import receiver
from todos.models import *

@receiver(post_save, sender=Todo)
def update_todo_alarm(sender, instance, **kwargs):
    devices = instance.user.device__set.all()

 

즉 내가 원하는 것은 request.auth (또는 request.user)에 접근할 수 있으면서, signal과 비슷한 방법을 통해 이를 수행하는 것이었다. 찾아본 글에 의하면 signal은 순수 model 기능을 이용하기 때문에 불가능하다고 한다. 즉 request.auth에 접근하려면 순수 model 단에서는 불가능하고, 이는 manager도 마찬가지겠다. 

 

그렇다면 위에서 고려했던 다른 옵션인 미들웨어를 살펴봐야 하겠다. 다만 고려할 점은 요청은 그대로 끝나고 요청과 관계 없이, 즉 비동기로 미들웨어를 실행하는 게 맞아 보였다. 알림이 보내질 때까지 응답을 늦출 필요는 없기 때문이다. 

 

그러므로 비동기 미들웨어를 구현해보자. 공식문서를 보니 장고에서는 비동기 미들웨어도 지원하긴 했다. 다만 장고의 기본값은 동기 미들웨어만 지원하는 것이므로, boolean 변수나 decorator 등을 사용해서 이 기본값을 바꿔주는 과정이 필요했다. 공식문서의 예제를 가져와 보니 coroutine function (iscoroutinefunction) 이라는 말이 있는데 이 부분도 자세히는 몰라서 찾아보았다. 

from asgiref.sync import iscoroutinefunction
from django.utils.decorators import sync_and_async_middleware


@sync_and_async_middleware
def simple_middleware(get_response):
    # One-time configuration and initialization goes here.
    if iscoroutinefunction(get_response):

        async def middleware(request):
            # Do something here!
            response = await get_response(request)
            return response

    else:

        def middleware(request):
            # Do something here!
            response = get_response(request)
            return response

    return middleware

 

찾아보니 coroutine function이란 멀티태스킹처럼 실행을 미룰 수 있는 함수이며, exception이나 iterator를 구현할 때 유용하게 사용된다고 한다. 

 

그런데 이 비동기 미들웨어는 현재의 내가 이해하기에는 조금 복잡하고, 무엇보다 API를 보낼 때 FCM 알림을 보내는 것이 그렇게 큰 부담은 아니었다. 빨리 구현하는 것이 중요하니 우선은 기존처럼 동기 방식의 미들웨어로 작업하고, 시간이 될 때 다시 개선하는 것이 맞다고 판단했다. 

 

 궁금한 점

1. explicit function call과 implicit function call의 차이는 무엇일까

2. signal과 middleware가 같은 목적에서 사용할 수 있는 다른 방법이라는 생각이 들었다. 둘의 개념적 차이는 조금 알겠는데, 그러면 어떨 때 signal을 사용하고 어떨 때 middleware를 사용해야 할까? 내가 이해한 바로는 요청이나 응답에 관련된 작업은 middleware에서 하고, 흩어져 있는 코드들에 대해서 알림을 줄 때는 signal을 사용하라고 이해했다. 

3. coroutine function은 exception이나 iterator를 구현할 때 어떻게 사용될 수 있을까?

4. 비동기 미들웨어는 어떤 식으로 동작할까? 미들웨어가 연쇄적으로 얽혀 있으면 비동기 미들웨어를 사용하게 되면 미들웨어의 순서가 보장되지 않을 것 같다. 장고에서는 어떻게 이 문제를 해결할까?

 

 오늘 배운 것

오늘은 알림 기능 개발을 마저 진행해보려고 한다. 지금까지는 서버에서 알림을 보내면 앱의 화면에 알림이 잘 뜨는지를 확인했고, 이제는 잘 뜨는 것을 확인했으니 목적에 맞게 알람을 보내도록 코드를 작성해주면 되겠다. 

 

목적에 맞게 알람을 보내려면 크게 두 가지의 요구사항을 충족해야 한다.

1. foreground 알림의 경우, 아침(오전 8시), 점심(오후 2시), 저녁(오후 8시) 시간을 기본값으로 하고, 각 시간마다 사용자에게 알림을 보낸다. 

2. background 알림의 경우, 사용자가 CRUD 중 CUD에 해당하는 API를 호출한다면 사용자(User)에 해당하는, 현재 알림을 보낸 디바이스가 아닌 다른 디바이스(Device)가 있는지 확인하고, 있다면 해당 디바이스에게 API를 다시 호출하라는 알림을 보낸다. 

 

우선 첫 번째 경우부터 작업해 보자. 

 

지정된 시간마다 알림을 보내야 하니, 처음에 생각난 방법은 cronjob을 이용해서 서버에서 특정 시간마다 배치 로직을 돌려주는 것이었다. 'django cronjob'으로 검색해 보니 django-crontab이라는 라이브러리가 나왔다. 찾아보니 지속적으로 실행해주었으면 하는 Job에 해당하는 함수를 정의해준 뒤, settings.py에 해당 함수를 cronjob 날짜 표시 규칙에 맞게 정의해주면 끝이어서 간단해 보였다. 

 

우선 todos 앱 내부에 jobs.py라는 cronjob에 쓰일 job 함수들을 정의하는 용도의 파일을 만들어 주었다. 그리고 다음과 같이 send_day_alarm 이라는 공통 로직을 만들어 주었다. 

MORNING_ALARM_TITLE = "오늘의 할 일을 확인해보세요"
AFTERNOON_ALARM_TITLE = "지금 할 일을 확인해보세요"
EVENING_ALARM_TITLE = "오늘의 남은 할 일을 확인해보세요"


def send_morninig_alarm():
    send_day_alarm(MORNING_ALARM_TITLE)


def send_afternoon_alarm():
    send_day_alarm(AFTERNOON_ALARM_TITLE)


def send_evening_alarm():
    send_day_alarm(EVENING_ALARM_TITLE)


def send_day_alarm(alarm_title):
    users_prefetch = Prefetch('user__todo_set', queryset=Todo.objects.filter(is_completed=False))
    devices = FCMDevice.objects.all().select_related('user').prefetch_related(users_prefetch)
    try:
        for device in devices:
            todos_queryset = device.user.todo_set.filter(is_completed=False).values_list("content", flat=True)
            todos_list = "\n".join(todos_queryset)
            device.send_message(
                messaging.Message(
                    notification=messaging.Notification(
                        title=alarm_title,
                        body=todos_list,
                    ),
                )
            )
    except Exception:
        pass

 

FCMDevice는 fcm_django에서 send_message() 메소드를 통해 알람을 편하게 보낼 수 있게 하는 전용 모델이다. 이 FCMDevice와 User는 N:1 관계이고, User와 할 일을 나타내는 Todo 모델은 1:N 관계이다. 그리고 여기서 원하는 것은 FCMDevice를 조회하면서 관련 User와 Todo 데이터 모두를 한 번의 쿼리로 조회하는 것이었다. 

 

이를 위해서 django의 Prefetch를 사용했다. django의 select_related와 prefetch_related 안에 Prefetch() 객체를 넣어서 두 번의 join문을 통해 한 번의 쿼리로 User, Todo 데이터를 가진 FCMDevice 쿼리셋을 만들었다. 

 

궁금한 점

1. INSTALLED_APPS에 'django_crontab'을 넣으면 django에서 이를 어떻게 인식하는 것일까?

2. 'python manage.py crontab add' 명령어를 입력하면 settings.py에 입력한 함수 커맨드가 crontab에서 관리해야 할 job으로 등록되는 것으로 이해했다. 이 과정이 어떻게 일어나는지 궁금하다. 

3. Prefetch를 통해서 데이터를 불러오는 것과 그냥 objects.all()를 통해서 데이터를 불러오는 것은 차이가 있을까? 

4. select_related나 prefetch_related 안에 Prefetch() 객체를 넣으면 django에서 이를 어떻게 인식해서 join문으로 DB에 쿼리를 날리는지 궁금하다. 

 

 오늘 배운 것

오늘은 두 가지의 과제가 있다. 하나는 main workflow를 정상화해서 develop 브랜치의 내용이 main 브랜치로도 반영되도록 모종의 에러를 해결해야 한다. 또 다른 하나는 현재 develop 브랜치에서 settings 파일의 debug 변수를 True로 두고 있음에도 swagger 페이지가 제대로 표시되지 않는 오류(오류인지 정상 작동하는 것인지는 모르겠지만 우리 입장에서는 나타나야 하기 때문에 오류로 간주했다)가 있다. 

 

우선 첫 번째 업무부터 해 보자. main workflow가 과연 모종의 이유로 롤백되고 있는지를 먼저 확인하자. AWS ECS의 클러스터의 서비스 로그를 확인해봤는데 'rolled back'이라는 문구가 눈에 띄었다. 모종의 이유로 ECS가 롤백되고 있었다. 

 

CloudFormation에서 더 구체적인 로그를 보니, 이전에 개발서버에 있었던 거의 똑같은 이슈가 프로덕션 서버에 발생하고 있었다. 디렉토리명이 맞지 않아서 생긴 오류였다.

 

그래서 'onestep_be.settings.prod' 라는 모듈을 main 브랜치에서 사용하고 있는지를 확인해 보았다. 태스크 정의 JSON 파일에서 변수를 동적으로 넣어 주는 코드에서 해당 값을 넣어주고 있었다..!! 이 부분은 develop, main 브랜치 모두에서 수정되지 않은 부분이기 때문에 두 브랜치 모두 바꿔주어야 했다. 

 

해당 코드의 settings를 setting으로 바꿔 주고 다시 커밋을 올렸다. 다시 롤백이 발생하는지를 지켜봐야겠다. 우선 워크플로우는 무사히 잘 실행되었다. 

 

다행히 develop에서 새로 개발된 피쳐들이 프로덕션 서버에도 잘 적용되는 것을 확인할 수 있었다.


다음은 swagger view가 제대로 표시되지 않는 오류를 다뤄 보려고 한다. 찾아보니 이 부분은 크게 두 가지의 원인이 있을 수 있겠다. 첫 번째는 settings 파일의 DEBUG 변수 값에 따라서 swagger view를 표시하지 않도록 설정이 될 수 있다고 한다. 이 부분은 예상했던 부분이었다. 

 

그러나 DEBUG의 값이 False인 프로덕션과는 달리, DEBUG의 값이 True인 개발 서버에서도 swagger view가 표시되지 않으므로 이 문제만은 아닌 것 같았다. 

 

다른 가능성으로는, 현재 문제는 'python manage.py runserver' 명령어를 사용하다가 gunicorn 기반의 명령어를 사용하면서 swagger 페이지가 보이지 않게 된 것이므로 이 부분과 관련이 있을 수 있겠다. 찾아보니 python manage.py runserver로 서버를 띄울 때랑 gunicorn으로 서버를 띄울 때랑 정적 파일을 참조하는 방식이 다르다고 한다. 

 

그렇다면 gunicorn에서 어떻게 로컬 서버에 있는(사실 swagger 관련 파일이라 나는 이 파일들의 존재도 모르긴 했다. 어쨌든!) 정적 파일들을 서빙하게 할까? 집단지성의 힘을 빌리니 간단한 코드 몇 줄을 추가하면 된다고 한다. 

# onestep_be.urls
from django.contrib.staticfiles.urls import staticfiles_urlpatterns

urlpatterns += staticfiles_urlpatterns()

 

 궁금한 점

1. gunicorn은 원래는 어떻게 정적 파일을 서빙했을까

2. staticfiles_urlpatterns는 어떤 역할을 하길래 gunicorn이 python manage.py runserver와 똑같은 정적 파일을 서빙하도록 할 수 있는 걸까

 

 오늘 배운 것

오늘은 별도로 개발을 하지는 않았다. 현실적인 이유로 어쩔 수 없다고 생각하긴 하지만, 명색이 주니어 주니어 개발자인데 이래도 되나 싶어 조금 반성되는 하루였다. 일단 죄책감을 잠시 미뤄두고 내일까지 마감인 공고에 자기소개서를 썼다. 개발 직무가 사실 이력서를 중심으로 보지 자기소개를 써야 하는 기업은 많지 않아서, 간만에 레포트 과제를 하던 본전공 시절로 돌아간 것 같았다.

 

다행히 무사히 자소서를 제출했다. 이제는 다른 pdf 버전 자소서를 한번 더 셀프 첨삭하고, 멘토님들께 피드백을 받은 다음, 이틀 뒤 개발 컨퍼런스의 리쿠르팅 존에 가져가는 것이 목표이다. 

 

그럼에도 불구하고, 며칠간 안 하면 또 감이 떨어지고 개발과 낯을 가리게 되는 걸 알고는 있다. 그래서 내일은 조금이라도 꼭 개발을 해야 하겠다. 

 

그리고 여러 기업에 원서를 넣는 중이긴 한데 공채가 아니라 수시 채용인 곳들은 메일을 준다는 전제 하에 얼마 후에 메일을 주는지도 잘 모르겠다... 일단 체감상 2주 이상 연락이 없으면 알아서 서류 탈락이겠거니 하긴 하는데, 이게 맞으려나 모르겠다. 

취준 기록용으로 노션을 쓰고 있는데 정리가 잘 되어서 너무 잘 쓰고 있다... 노션 짱

 

암튼 내 멘탈도, 팀원들이랑 프로젝트도, 틈틈이 취준하면서 원서 넣는 것도 모두모두 파이팅이다. 

 

 오늘 배운 것

이제는 기존의 알림 코드를 조금 수정해서 어제 찾은 라이브러리를 사용하는 코드를 추가해 주면 된다. 다음과 같은 코드를 추가해 주었더니, 다음과 같은 경고 메시지가 떴다. 

import PushNotification from 'react-native-push-notification';

PushNotification.localNotification({
  title: remoteMessage.notification.title,
  message: remoteMessage.notification.body,
});

 

공식문서의 readME를 읽어보니 channelId 값이 없으면, 즉 channelId 값을 안 주거나 해당 channelId에 해당하는 채널이 없으면 notification이 trigger되지 않는다고 한다. 어쩐지 이렇게 채널 Id같은 추가 정보도 안 받고 알림이 보내지나 싶었다. (다시 생각해보면 FCM 라이브러리에서는 해당 문제가 없었던 이유가, 이미 FCM 토큰으로 특정 디바이스를 타겟팅해서 보내기 때문에 별도의 channel ID가 필요없던 것이라고 추측해본다.) 그리고 여러 가지 이유로 레포가 활발히 업데이트 되지 않는다며 Notifee와 같은 다른 라이브러리를 사용하기를 추천하기도 했다... 

 

사실 새 라이브러리를 배우는 것은 문제가 되지 않았으나, 현재 여러 팀원이 프론트 개발에 참여하고 있었기 때문에 안드로이드와 같은 RN 영역 외의 설정이 복잡하지 않는 라이브러리였으면 좋겠다고 생각했다. 그래서 Notifeereact-native-notifications, 그리고 현재 라이브러리인 react-native-push-notifications의 공식문서를 비교해 보면서 가장 안드로이드 설정이 간단한 라이브러리를 선택하기로 했다. 

 

자세히 보지는 않았지만, Notifee의 설치 및 셋업 가이드를 보니 매우 간단하고, 중간에 /android 폴더를 건드리는 일도 없었다. 그래서 일단은 Notifee 라이브러리를 선택했다. 설치 가이드를 따라서 진행했다. 뒷부분에는 Expo 설치 가이드도 있었는데, 그 부분은 시도해보니 expo에서 지원하는 valid plugin이 아니라는 에러가 나서 이후로 쭉 진행하지는 못했다. 

 

그런데 'npm run android:dev'(팀원이 scripts 값으로 별도로 설정한 명령어다)로 서버 prebuild를 시도하니 에러가 났다. 이 부분은 expo dependency(app.config.js에 있는 plugins 변수의 값)에서 @notifee/react-native 부분을 제거해서 오류를 임시로 해결하였다. 

 

그리고 'rnfirebase' 공식문서를 보는데 팀원이 빼먹은 부분을 알려주었다. 바로 유저의 permission을 받아야 알림을 표시할 수 있었는데, 그렇게 권한을 요청하는 코드를 빼먹었었다. 

import {PermissionsAndroid} from 'react-native';
  
PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS);

Allow

 

그런데 권한도 받았는데 알림이 여전히 오지 않았다. 그런데 rnfirebase 공식문서에서는 notification 속성이 메시지에 있고, 앱이 background 상태에 있다면 알림은 표시되는 것이 기본값이라고 말하고 있었다. 그래서 혹시 서버에서 알림을 보내는 코드가 잘못되었을 가능성은 없을까? 라는 의문이 들었다. 다시 fcm_django 공식문서를 봤다. 

 

이상한 점이 하나 있었는데, 나는 기존의 messaging.Message 인스턴스를 별도의 message 변수에 할당해서 사용하고 있었다. 그런데 공식문서에서는 전부 messaging.Message 인스턴스를 FCMDevice의 send_message 안에 직접 선언하고 있었다. 그러니까 이런 식이었다. 나는 아래와 같이 코드를 작성하고 있었다. 

from firebase_admin import credentials, messaging
from fcm_django.models import FCMDevice

def send_push_notification(token, title, body):
    message = messaging.Message(
        notification=messaging.Notification(
            title=title,
            body=body,
        ),
    )
    device = FCMDevice.objects.filter(registration_id=token).first()
    try:
        device.send_message(message)

 

그런데 위의 코드를 아래와 같이 바꿔주었더니 잘 동작하는 게 아닌가. message 변수로 할당된 값을 그대로 device.send_message() 안의 인자 값으로 넣어주었을 뿐인데 알람이 잘 들어왔다. 

def send_push_notification(token, title, body):
    device = FCMDevice.objects.filter(registration_id=token).first()
    try:
        device.send_message(
            messaging.Message(
                notification=messaging.Notification(
                    title=title,
                    body=body,
                ),
            )
        )

좋은데 쫌 어이가 없다

 

 궁금한 점

1. 위와 같은 알람 이슈는 왜 일어난 것일까

 

 오늘 배운 것

오늘도 마저 어제 하던 FCM 알람 기능을 적용하려고 한다. 어제 코드로 보았을 때는 백엔드 쪽에서는 알림을 잘 보내고 있는데 프론트엔드 쪽에서 그 알림을 화면에 띄워주는 부분이 되지 않는 것 같아서, 다시 공식문서를 보면서 내가 놓친 부분이 있는지를 보았다.

 

다시 보니 프론트에서 특히 앱이 foreground 상태일 때는 Notification이 아예 표시되지 않도록 되어 있었다. 그렇다면 알림은 잘 수신되는데 단순히 화면에서 나타나지 않는 게 아닐까 싶어서 onMessage() 메시지 핸들러 안에 로그를 찍어보았다. 그랬더니 로그가 찍히고 있었다. 

 

즉 메시지는 잘 도착했는데 Notification 타입의 메시지는 앱이 Foreground 상태일 때 표시되지 않는 것이 기본값이므로 표시되고 있지 않았던 거였다. 하지만 나는 앱이 Foreground 상태가 아닐 때 메시지가 표시되었으면 좋겠고 이를 테스트하고 싶었다. 그래서 앱을 잠시 백그라운드 상태로 만들고 다시 알림을 보내보았다. 그랬더니 메시지가 백그라운드에서 수신되었다는 알림 자체는 왔다. 다만 화면에 표시되지 않았다. 이는 Quit 상태일 때도 마찬가지였다. 

 

아마도 메시지 핸들러가 호출은 잘 되는데 별도로 알림을 띄우라는 코드가 없으니 안 띄우고 메시지 핸들러만 호출되는 것이라고 생각했다. 알림을 띄우는 것과 관련된 다른 라이브러리가 있길래 추가로 이 라이브러리를 사용하면 좋을 것 같았다. 

npm install --save react-native-push-notification

 

 오늘 배운 것

오늘은 우선 구글 플레이스토어 콘솔에서 배포가 잘 되고 있는지 확인했다. 다행히 아직까지는 오류가 난 것이 없어서, 당장 세 명이 뛰어들 만한 급한 이슈는 없다고 판단했다. 

 

어제 잘 돌아가는 것을 확인한 서버를 일단 켜 두자. 프론트엔드와 백엔드 서버 모두 켜 두었다. 

 

일단 디자인은 잠시 미뤄둔 상황이다. 그리고 구글 플레이스토어의 앱 배포도 아직까지는 문제가 없다. 물론 다운로드 수가 20을 넘어야 하지만, 그건 차차 사용자 수를 모으면 되는 상황이므로 현재의 문제는 아니다. 그렇다면 이 상황에서 뭘 우선으로 해야 할까? 일단 앱이 배포된 다음에는, 사용자를 모으는 것이 우선이다. 사용자를 모으려면 우선은 앱이 정상 작동을 해야 하고, 고도화 기능(알람과 하위투두 고도화)이 필요하며, 틈틈이 홍보도 해야한다. 

 

논의 끝에 팀원 한 명은 앱의 정상 작동을 위해 버그를 고치는 일, 다른 한 명은 하위 투두 고도화를 위해 DB 및 모델을 설계하는 일, 그리고 나는 알람을 맡았다. 계속 미뤄지게 된 알람 이슈를 작업하게 된 것이다. 

 

사실 이미 작성된 로직이 있었어서, 해당 로직이 잘 작동하는지 확인을 하면 되었다. 문제는 결과값 자체는 잘 보내진다고 나오는데 에뮬레이터 기기에 알림이 오지 않는다는 점이었다. 

 

원인을 추측해 보자면 해당 토큰값이 에뮬레이터의 FCM 토큰값이 아니거나, 코드에 있는 Messaging 또는 Notification 객체가 제대로 동작하지 않았을 수 있겠다. 

 

우선 첫 번째 추측의 경우, 프론트에서는 다음과 같은 코드로 기기의 FCM 토큰을 가져오고 있었다. 이는 RN 공식문서에도 나와있는 방법이므로 이게 틀릴 가능성이 꽤 낮았다. (사실 만약 틀렸다고 하면, 깃헙 이슈를 보면서 어떻게 해야 하는지를 또 일일이 찾아봐야 해서 그게 아니길 바란다.)

import messaging from '@react-native-firebase/messaging';
const token = await messaging().getToken();

 

... 라고 생각했었는데, 알고보니 프론트에서 다른 브랜치로 작업을 하고 있었다. 즉 백엔드에서 맞는 기기로 알림을 보내도 프론트엔드에서 그 메시지를 받아서 표시해주는 로직이 동작해야 화면에 알림이 표시되는데, 그 로직을 작성해놓은 브랜치가 아니었던 것이다. git checkout과 git rebase(앱이 동작하지 않는 문제가 dev 브랜치에서 고쳐진 상태였기 때문에 rebase도 같이 했다)을 하고 다시 시도해 보았다. 여전히 되지 않았다. 

 

백엔드에서는 로직 중간에 오류가 없는 것으로 보아 프론트엔드의 알림을 받는 부분에 문제가 있을 것이라고 추측했다. 짚이는 부분 하나는 프론트에 현재 알림이 오면 그걸 그대로 화면에 나타내주는 코드가 없다는 거였다. 또한 헷갈렸던 부분은, 메시지를 보내거나 받을 때 디바이스의 FCM 토큰만 알면 별도로 채널 등의 정보를 따로 지정하지 않아도 무사히 해당 FCM 토큰을 가진 디바이스에서 메시지를 받을 수 있는지 의문이 들었다. 

 

다시 RN firebase 공식문서fcm django 공식문서를 보자. 공식문서를 보면서 여러 문제점을 찾았다. 첫 번째는 fcm-django에서 메시지를 보내려면 fcm-django에서 정의한 FCMDevice라는 모델을 사용해야 한다. 정확히는 해당 모델의 send_message 라는 메소드를 통해서 메시지가 보내진다. 그런데 그 부분이 코드에서 빠져 있었다. 아마도 디바이스 토큰 값은 정확했을 텐데 메시지가 제대로 전송되지 않은 것 같았다. 

 

그러면 이전에 임의로 정의해두었던 Device 모델 대신에 FCMDevice 모델을 사용해야 한다. 이를 위해서 해당 코드가 있는 구글로그인 로직의 일부를 바꿔주었다.

 

기존의 Device 모델에서 get_or_create 메소드를 통해 device_token(앞서 프론트에서 보내는 device token 값)의 값을 가진 Device 인스턴스가 있으면 이를 불러오고 없으면 만들어 주었다. 이번에도 마찬가지로 FCMDevice에서 get_or_create 메소드를 사용하려고 했는데, 그러려면 device_token 값을 어떤 필드에 저장해야 할지를 알아야 했다. 

 

fcm_django.models에 관련 모델들이 모두 정의되어 있었다. 우리가 사용하려는 FCMDevice는 AbstractFCMDevice를, AbstractFCMDevice 모델은 Device 모델을 상속받고 있었다. 그래서 이 FCM 토큰 값을 어디다 넣으라는 건가 싶었는데, 공식문서에 친절히 registration_id 필드에 넣으라고 나와있었다.

 

기존의 Device 모델에서 FCMDevice 모델로, 그리고 일부 필드값만 바꿔주었다. 

FCMDevice.objects.get_or_create(
    user=user, registration_id=device_token
)

 

그리고 'python manage.py showmigrations' 명령어로 확인해 보니 fcm_django 라이브러리와 관련된 모델 변경 사항이 아직 마이그레이션에 반영되지 않은 상태였다. 이것도 반영해 주었다. 

 

이렇게 기본 로직을 변경하고, 다시 프론트 에뮬레이터를 켜서 구글로그인을 한 번 실행해 주었다. 그래야 FCMDevice 모델에 해당 에뮬레이터의 토큰 값이 들어가기 때문이다. 그리고 나서 기존의 메시지를 보내는 로직을 다시 바꿔주었다. 

def send_push_notification(token, title, body):
    message = messaging.Message(
        notification=messaging.Notification(
            title=title,
            body=body,
        ),
    )
    device = FCMDevice.objects.filter(registration_id=token).first()
    try:
        device.send_message(message)
    except Exception as e:
        # sentry capture exception
        return PUSH_NOTIFICATION_ERROR
    return PUSH_NOTIFICATION_SUCCESS

 

이렇게 바꿔 주었고 메시지를 보내는 로직을 실행시키니 success라고 떴다. 그런데 여전히 에뮬레이터에서는 알림이 표시되지 않아서, 이번에는 프론트 쪽에 문제가 있을 가능성을 두고 프론트 코드를 고쳐봐야 할 것 같다. 

 

 궁금한 점

1. kafka-python 등의 kafka를 python에서 사용하기 위한 라이브러리가 있는 걸로 알고 있다. 문득 우리 서비스에서 kafka-python과 같은 라이브러리가 필요할 일이 있을지 궁금해졌다. 

2. FCMDevice의 send_message 로직은 어떻게 되어 있을까?

 

 오늘의 러닝 인증

 

 오늘 배운 것

오늘 오후 2시까지는 팀원들과 논의해서 새 앱 디자인을 정하고, 이후 개인 시간에는 FCM 알림을 붙여보려고 한다. 2시부터 4시까지는 소마 메이커스 특강이 있었고, 원래는 2시에 AI가 추천해줄 하위 투두를 어떻게 보여줄지에 대한 디자인은 정하지 못했었다. 그런데 너무 고맙게도 팀원들이 특강을 듣고 있는 동안 해당 디자인에 대해서 정하고 논의를 해 줘서, 이제 새 앱 디자인은 거의 정해졌다고 봐도 무방하겠다. 

 

남은 것은 FCM 알림이다. 오늘 '인프런이 성장하면서 변화해온 아키텍처'가 강연 주제였는데, 인프런이 조직 및 비즈니스 상황 변화에 따라 어떤 결정을 했는지도 알 수 있었지만 중요한 인사이트는 '회사의 비즈니스나 조직 상황에 맞는 적정 기술을 추구하는 것'이라는 생각이 들었다. 그리고 회사에서도 현재 자신이 어떤 상황인지(어떤 리소스가 얼마나 있는지)를 알고 그에 맞는 결정을 내리는, 즉 적정 기술을 추구하는 개발자를 조직에서도 당연히 선호하겠다는 생각을 했다. 발상의 전환을 준 강연이었다. 

 

아무튼! 그래서 원래는 FCM 알림을 백그라운드 푸시알림으로 구현하는 것에 대해서 원래는 이게 트래픽이 많아지면 비효율적인 방법이 아닐까, 아니면 원래 알림과는 맞지 않는 취지 아닐까(실제로 그렇기는 하다)... 등등 여러 생각이 들었었다. 그런데 그러면 그건 그때 가서 고민하면 되는 게 아닐까? 라는 방향으로 생각이 바뀌었다. 일단 구현해보자. 

 

우선 구현을 하려면 프론트엔드의 앱이 제대로 동작해야 했다. develop 브랜치로 체크아웃한 뒤 한번 확인해봤다. 어떠한 이유인지는 모르겠지만, 프론트에서 로컬 서버를 호출하고 있는 것으로 보였다. 

 

이 문제도 해결되어야 하긴 한데, FCM 알람과 직접적인 연관은 없는 문제이므로 우선 백엔드도 로컬 서버를 띄워두기로 했다. 이번에는 또다른 에러가 났다. 전혀 상관 없는 에러인데, 문제는 이걸 해결하지 않으면 알림 테스트를 못 한다는 것이었다...! 그래서 해결해야 하는 상황이다. 아니면 다른 버그 이슈를 하나 파고 해당 이슈에 dependency를 걸어두는 것이 낫겠다. 즉 원래는 FCM 알림 이슈 해결 후 커스텀을 하려는 계획이었는데, 버그 해결 후 FCM 알림 이슈 후 커스텀을 하게 되었다. 

 

알고보니 이 문제는 다른 브랜치에서 해결 중이었는데 머지가 되지 않은 상황이었다. 해당 브랜치를 머지한 다음 .gitignore 파일에 올리지 않은 파일을 따로 Firebase에서 받아서 올려두니 잘 동작하였다. 

 

궁금한 점

 

+ Recent posts