알람 앱은 그냥 만들어도 귀찮지만 리액트 네이티브로 만들면 더 귀찮다 ⏰ Part2 — 안드로이드
안드로이드와 iOS에서 알람 앱을 만드는 방법과 그 위에 리액트 네이티브를 끼얹는 개발기
Content
Part 2 — 안드로이드 ← Here 🤚
RN과 네이티브는 각각 무슨 역할을 할까?
본격적으로 안드로이드에선 어떻게 구현이 되어있나 살펴보기 전에 RN과 네이티브들에서 해야 할 일들은 무엇이고 어떤 방식으로 통신하게 만들어놨는지 보자.
- [JS → Native] 알람 예약
- [JS → Native] 알람 취소
- [JS → Native] 반복 알람 취소
- [Native → JS] tap 한 Local Notification의 딥링크 정보를 전달
사실 별 기능들도 없다. 이렇게 보면 알람 기능은 별로 구현이 귀찮은 기능도 아니다. 사전 지식이 좀 필요할 뿐이다.
기본적으로 구현해야 되는 것은 JS->Native 방향으로 알람을 예약하는 함수이다. 이외에도 알람을 예약을 취소하거나 첫 알람이 보인 후에 유저가 Foreground에 진입해서 반복되는 알람만 취소하거나 하는 귀여운 함수들만 만들어주면 된다.
이후 [Native → JS] 방향으로는 굳이 RCTEventEmitter
같은 걸 써서 이벤트를 전송하지는 않았다.
Local Notification을 탭할 때 발생하는 이벤트를 각 OS의 API로 적절히 받아와서 RN의 Linking
모듈이나 마지막으로 탭한 알림이 딥링크 정보를 갖고 있다면 그걸 네이티브 모듈에 저장하고 RN단에서 적절한 시점에 pop 해주는 방식을 사용했다.
각각 세부사항은 후에 살펴보자.
[JS → Native] 네이티브 모듈 메소드들
Native 컴포넌트의 함수들이 JS -> Native 방향으로만 가는 단방향성을 갖는다.
- reserveAlarm(hour, minute, weekBit)
이 함수는 말 그대로 알람을 예약하는 함수이다. 왜 세 번째 인자의 이름이 week
나 weekdays
도 아닌 weekBit
인가? 그렇다. 리스트로 넘기는 코드를 짜기 귀찮아서 Int 형으로 넘긴 다음 비트 연산으로 파싱을 했다.
코틀린의 비트 연산은 왜 이런 식으로 구현을 해놨을까?
아예 더하기도 1 + 2
말고 1 plus b
라고 해놔야 된 거 아닌가?
이게 된다고?
사실 infix function으로 잠시 구현했다. 실제 코틀린엔 저런 건 없다는 걸 알아두자.
infix fun Int.plus(b: Int) = this + b
- removeAllPendingAlarm(), removeAllTodayPendingRepeatedAlarm()
예약 알람과 반복 알람을 예약 해제하는 함수들이다. iOS와 Android의 구현이 상이하기에 Today 같은 infix는 신경 쓸 필요 없다. iOS에선 오늘의 반복만 해제해 주기 때문에 저런 단어를 붙였다.
- getPendingTappedNotificationUrl(Promise)
현재 유저가 탭한 알람이 처리해야 될 딥링크 url을 갖고 있다면 반환해 주는 함수이다. poll 동작이라고 할 수 있다.
안드로이드 구현
JS 측에서 구현은 아무런 어려움이 없다. 그냥 Native의 함수들을 호출해 주기만 하면 된다. 안드로이드부터 살펴보자.
알람 기능을 구현한 순서가 iOS -> JS(TimePicker나 기본 동작 등)-> 안드로이드 순서인데, 안드로이드의 구현체가 가장 간단하다.
하.. 이것도 할 말이 많은데 처음에 커스텀 BroadcastReceiver
를 정의해서 AlarmManager
에 저걸 호출해 주는 PendingIntent
를 담아 알림을 예약하는 과정을 구현하며 엄청난 안드로이드 고전 API와 씨름했으나 코드를 다 짜고 나서 정확히 알람이 가지 않는다는 것을 깨달았다.
이 부분들에 대해서 구글링을 하면 6년 전 8년 전 10년 전 답변이 나오며 A_n = 6 + 2(n — 1)
의 등차수열을 형성하기 때문에 조심하는 게 좋다. 10년 전 개발자의 사고방식과 당신의 지향점은 어딘가 달라야 할 수도 있다.
따라서 200줄 정도 짠 코틀린 코드를 모두 다 삭제해버리고 WorkManager
로 눈길을 돌렸다.
WorkManager
는 Android Architecture Component의 늦둥이인데 AsyncTask
, Thread
, Coroutine
, Reactive X
, Service
, BroadcastReceiver
등 안드로이드의 concurrency와 background task와 관련된 그 모든 헷갈릴 수 있는 모듈들을 모두 집어치워버리게 만들 수 있는 강력한 친구이다.
운 좋게도 나는 WorkManager
에 대하여 2년 전에 이런 날이 올 줄 알고 글을 써놨었고, 왜인지는 모르겠지만 최신 Android Studio 버전에서 background task inspector가 도입되어 WorkManager
debugging이 매우 쉬워졌다는 것도 어디선가 본 기억이 났기 때문에 AlarmManager
와 BroadcastReceiver
따위로 짠 더러운 코드 200줄 정도는 기쁜 마음으로 지울 수 있었다.
혹시나 이 부분을 잘 모르는 독자가 있다면 다음과 같은 글을 참고하자. 2년 전 글이라 좀 구릴 수 있어서 다른 글을 봐도 무방하다. 독스를 읽는 걸 추천한다.
WorkManager
로 코드를 다 짜고 나서 있었던 해프닝을 잠시 소개하자면 WorkManager
를 Gradle dependency에 추가한 뒤 기쁜 마음으로 기능을 테스트해 보려 실행 버튼을 눌렀을 때 가장 먼저 날 반기는 건 최신 버전의 WorkManager는 compileSdkVersion
이 31 이상에서만 동작한다는 것이었다.
템플릿으로 그대로 프로젝트를 만들어도 아직 compileSdkVersion
이 30 인 RN 따위에서 에러가 나지 않을 리는 만무했다. 여하튼 이걸 업데이트해야 되는 줄 알고 31로 바꾼 다음에 빌드를 실행하니 SDK 31부터는 intent filter를 가진 모든 Activity, BroadcastReceiver, Service가 exported 옵션을 Manifest에 명시해야 한다는 에러가 떴다.
명시해 주마 하고 다 옵션을 달아줬는데도 저 에러가 죽어도 안 없어졌다. 몇 시간 을 merged Manifest output을 눈이 빠지도록 찾아봐도 문제가 없는데 자꾸 저 에러가 났다. Gradle build log를 보면서 어떤 라이브러리에서 문제가 있는지 살피느라 눈이 더 빠질것같았다. 일단 내 잘못은 아닌거같음;;
결론적으로 WorkManager
의 2.7.1 버전이 아니고 2.6.x 버전을 쓰면 compileSdkVersion
이 31 이상이 아니어도 된다는 걸 깨달았다. 아니 그럼 왜 독스 페이지에 위 사진처럼 달려 있는 것인가 2.7.1 이상부터는 31이 필요하다고 써놓던가.
여하튼 WorkManager로 바꾼 알림 기능을 코드를 돌리자마자 한 번에 원하는 대로 동작이 되었다. 역시 성능 확실하다.
코드를 좀 살펴보자.
알람 예약/취소
PeriodicWorkRequest
를 이용해 특정 시간대에 알람이 계속 오게 만든다. WorkManager
는 아직 exact 한 시간에 Worker
가 실행되는 것이 구현이 안 되어있는데, setInitialDelay
를 이용해 현재 시간과 target 시간의 차를 적절히 계산해서 넘겨주면 정말 1초의 오차도 없이 실행된다.
다음과 같이 계산하자.
이제 WorkManager
의 Worker
에서는(이를 AlarmWorker
라고 하자) inputData
로 전달된 weekBit
를 파싱 해서 현재 요일과 비교해서 오늘 요일에 알람이 울려야 한다면 울리게 해준다.
이때, 반복 알람을 울리게 하기 위해 AlarmRepeatWorker
라는 녀석을 하나 더 만들어서 5초마다 또 울리게 해준다. 이는 PeriodicWorkRequest
로 구현되는 것이 아닌 OneTimeWorkRequest
로 구현되며, 5초마다 쏜다.
물론 for 문 따위를 돌리는 건 아니고 doWork
함수 안에서 자기 자신(AlarmRepeatManager
)을 WorkManager
에 enqueue
를 또 하는 것이다.
for 문을 돌리면 반복 알람 취소에 대해서 구현이 곤란해질 수도 있다.
PeriodicWorkRequest
는 5초마다 알람을 쏘는 기능 따위를 위해 만들어진 것이 아니기 때문에 하루간의 간격은 잘 작동할지 몰라도 5초는 동작을 안 한다.
WorkManager
에 모든 Task는 Unique name 과 함께 넣어주면 취소할 때도 쉽다.
모든 알림을 취소한다? 바로 AlarmWorker
의 Unique name을 취소
알림이 울려서 이제 유저가 앱을 켰을 때 반복만 안되게 반복 알림만 취소한다? 바로 AlarmRepeatWorker
의 Unique name을 취소
AlarmRepeatWorker
의 trigger는 AlarmWorker
이기 때문에 AlarmRepeatWorker
는 부담 없이 취소해버리고 매일 알람이 오는 AlarmWorker
녀석만 정확한 시간에 오게만 해주면 되는 구조를 갖게 되기 때문에 개발을 하다가 기분이 좋다.
App Background Task Inspection
앞서 언급했듯이 Android Studio의 App Inspection 기능을 이용하면 WorkMananger의 Worker와 Service 등의 실행 상황을 쉽게 모니터링할 수 있다.
알람 탭 처리
요건 이제 그냥 AAC 따위도 없이 고전 안드로이드 API의 영역이다.
일단 AlarmRepeatWorker
에서 Local Notification을 때릴 때, Notification.Builder
에서 content intent를 Activity
를 실행하는 PendingIntent
로 넣어주고 이 PendingIntent
가 갖고 있는 Intent
는 이미 앱이 켜진 상태에서도 다시 Activity
를 실행하지 않게
android:launchMode="singleInstance"
와
Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
를 조합해 주면 된다.
그리고 Intent
의 extra
에 tapUrl
따위 같은 이름의 키를 넣어준다.
그리고 나서 우리의 MainActivity든 뭐든 Activity에서는 다음과 같이 Intent를 처리해 준다.
여기서 이제 pendingTappedNotificationUrl
이란 녀석은 Notification
네이티브 모듈의 static string이며 앞서 말한 getPendingTappedNotificationUrl
요 함수가 반환해 주는 녀석이 된다.
What’s the next?
이제 2단계인 안드로이드에서의 알람/반복 알람 기능까지 살펴보았고 마지막으로 iOS의 알람 구현을 다음 글에서 살펴보도록 하자.
- Github
- Website
- Medium Blog, Dev Blog, Naver Blog
- Contact: mym0404@gmail.com