React Native에서 로컬 리텐션 알림을 스케줄링하며 확인한 포인트

김민석·2025년 10월 10일
0

Tech Deep Dive

목록 보기
58/58

Intro

  • 장기 미접속 사용자를 깨우는 로컬 알림을 만들었는데, 기기마다 중복 예약이나 권한 누락이 빈번했어요.
  • 저는 React Native에서 Expo Notifications를 활용해 스케줄링, 취소, 멱등성을 모두 잡는 유틸을 정리했습니다.

핵심 아이디어 요약

  • 알림마다 고유 식별자를 생성해 새 스케줄 전에 기존 알림을 취소합니다.
  • 플랫폼 권한을 다시 확인하고, 지원하지 않는 환경(web)은 빠르게 빠져나갑니다.
  • 리텐션/온보딩 등 시나리오별 예약과 해제를 하나의 서비스 모듈로 묶었습니다.

준비와 선택

  • 앱이 Expo 기반이라 expo-notifications를 썼고, 네이티브 브리지를 만들 필요가 없었습니다.
  • 사용자별 알림을 추적하기 위해 createNotificationIdentifier(userId, type) 패턴을 도입했습니다.
  • 반복 스케줄이 필요한 리텐션 알림은 하드코딩된 배열보다 타입 안전한 상수 배열을 사용했습니다.

구현 여정

  1. 권한 확인과 빠른 탈출: iOS/Android가 아닌 경우에는 바로 null을 반환해 크래시를 막았습니다.
  2. 기존 알림 취소: 동일 식별자의 알림이 이미 예약돼 있으면 cancelLocalNotification을 먼저 실행했습니다.
  3. 스케줄 공통화: scheduleLocalNotification 하나에서 title/body/trigger를 모두 처리하고, 개별 유스케이스는 헬퍼가 감싸도록 했습니다.
  4. 대량 스케줄: 리텐션 알림의 경우 RETENTION_REMINDER_SCHEDULE을 순회하면서 각각 await를 걸어 순차적으로 예약했습니다.
  5. 취소 메서드 통일: 온보딩, 리텐션 알림 모두 취소 함수에서 Promise.all로 묶어 멱등성을 보장했습니다.
// src/shared/libs/local-notifications.ts:20-248
export async function scheduleLocalNotification(
  data: LocalNotificationData,
): Promise<string | null> {
  if (Platform.OS !== 'ios' && Platform.OS !== 'android') return null;

  const { status } = await Notifications.getPermissionsAsync();
  if (status !== 'granted') return null;

  await cancelLocalNotification(data.identifier);

  return await Notifications.scheduleNotificationAsync({
    content: {
      title: data.title,
      body: data.body,
      data: { type: data.type, identifier: data.identifier },
      sound: true,
      badge: 1,
    },
    trigger: {
      type: Notifications.SchedulableTriggerInputTypes.DATE,
      date: data.scheduledTime,
    },
    identifier: data.identifier,
  });
}

export async function scheduleAllRetentionReminders(userId: string) {
  await cancelAllRetentionReminders(userId);
  for (const days of RETENTION_REMINDER_SCHEDULE) {
    await scheduleRetentionReminder(userId, days);
  }
}

export async function cancelAllUserNotifications(userId: string) {
  await Promise.all([
    cancelAllOnboardingNotifications(userId),
    cancelAllRetentionReminders(userId),
  ]);
}

결과와 회고

  • QA에서 테스트 케이스를 반복 실행해도 더 이상 알림이 중첩되지 않고, 예약 취소 후 즉시 재예약이 잘 동작합니다.
  • 사용자가 알림 권한을 끄면 함수가 조용히 null을 반환하기 때문에, UI에서는 토스트로 안내할 수 있게 분리할 예정입니다.
  • 다음 단계로는 사용자가 알림을 열었을 때 루트 화면으로 연결하는 라우팅 전략을 푸시 알림 코드와 공유하려고 합니다.
  • 여러분은 로컬 알림을 멱등하게 관리하기 위해 어떤 패턴을 쓰고 있나요? 다른 아이디어가 있다면 알려주세요.

Reference

profile
동업자와 함께 창업 3년차입니다. Nextjs 위주의 프로젝트를 주로 하며, React Native, Supabase, Nestjs를 주로 사용합니다. 인공지능 야간 대학원을 다니고 있습니다.

0개의 댓글