Expo 푸시 토큰 등록 루틴에서 배운 것

김민석·2025년 10월 10일
0

Tech Deep Dive

목록 보기
54/58

Intro

  • 로그인을 기준으로 푸시 알림 초기화를 제어하지 않으면 알림이 뒤늦게 도착하거나 아예 빠지는 일이 잦았어요.
  • 저는 Expo 기반 프로젝트에서 토큰 등록과 라우팅까지 한 번에 묶어 지연과 중복을 없애는 루틴을 만들었습니다.

핵심 아이디어 요약

  • 로그인 여부를 확인한 뒤에만 Expo 토큰을 발급하고, Supabase에 디바이스 정보를 멱등하게 업서트합니다.
  • iOS/Android별 권한 체계와 알림 채널을 분기해 초기화 타이밍을 안정화합니다.
  • 포그라운드/탭 이벤트를 감지해 뱃지 카운트를 즉시 동기화하고 알림 페이로드를 라우터로 넘깁니다.

준비와 선택

  • Expo SDK를 쓰고 있어서 expo-notifications, expo-constants, expo-device를 그대로 활용했습니다.
  • 백엔드와 토큰을 공유하기 위해 Supabase 서비스 모듈을 호출하는 구조를 유지했습니다.
  • 딥링크는 expo-router로 처리하고 있어 router.replace로 알림 탭 시 이동하도록 설계했습니다.

구현 여정

  1. 세션 선 확인: supabase.auth.getSession()을 먼저 호출해 로그인 상태가 아니면 루틴 전체를 중단하고 뱃지를 0으로 초기화했습니다.
  2. 알림 채널과 권한 셋업: Android에서는 채널을 직접 생성하고, iOS에서는 세분화된 권한 상태(AUTHORIZED, PROVISIONAL, EPHEMERAL)를 다시 점검했습니다.
  3. 프로젝트 ID와 토큰 발급: Constants.easConfig.projectId가 없을 때를 대비해 expoConfig.extra.eas.projectId까지 탐색했습니다.
  4. 디바이스 메타 업서트: Device.deviceName, Device.osVersion 등 네이티브 정보를 함께 등록해 서버가 푸시 실패를 추적하기 쉬워졌습니다.
  5. 알림 리스너: 수신/탭 이벤트 모두에서 updateBadgeCount를 호출해 뱃지를 맞추고, 커스텀 라우팅 함수로 화면 이동을 처리했습니다.
// src/shared/hooks/push.ts:22-274
export function usePushNotificationsSetup() {
  const lastRegisteredTokenRef = useRef<string | null>(null);
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(() => {
    const supabase = getSupabase();
    (async () => {
      const res = await supabase.auth.getSession();
      setIsLoggedIn(Boolean(res?.data?.session?.user?.id));
    })();
  }, []);

  useEffect(() => {
    if (Platform.OS !== 'ios' && Platform.OS !== 'android') return;
    if (!isLoggedIn) return;

    (async () => {
      Notifications.setNotificationHandler({
        handleNotification: async () => ({
          shouldPlaySound: false,
          shouldSetBadge: false,
          shouldShowBanner: true,
          shouldShowList: true,
        }),
      });

      if (Platform.OS === 'android') {
        await Notifications.setNotificationChannelAsync('default', {
          name: 'default',
          importance: Notifications.AndroidImportance.MAX,
        });
      }

      const { status } = await Notifications.requestPermissionsAsync({
        ios: { allowAlert: true, allowBadge: true, allowSound: true },
      });
      if (status !== 'granted') return;

      const projectId = Constants.easConfig?.projectId;
      if (!projectId) return;

      const tokenData = await Notifications.getExpoPushTokenAsync({ projectId });
      lastRegisteredTokenRef.current = tokenData.data;

      await registerUserPushToken({
        token: tokenData.data,
        provider: 'expo',
        projectId,
        deviceName: Device.deviceName ?? null,
        appVersion: Constants.nativeAppVersion ?? null,
        deviceInfo: { osName: Device.osName ?? undefined },
      });
    })();

    const receivedSub = Notifications.addNotificationReceivedListener(
      async () => updateBadgeCount(),
    );
    const responseSub = Notifications.addNotificationResponseReceivedListener(
      async response => {
        await updateBadgeCount();
        const payload = response.notification.request.content.data as NotificationPayload;
        const { route } = resolveNotificationRoute(payload);
        router.replace(route);
      },
    );

    return () => {
      receivedSub.remove();
      responseSub.remove();
    };
  }, [isLoggedIn]);
}

결과와 회고

  • 로그인 후 약 2초 안에 모든 디바이스에서 토큰이 등록되고, 서버 로그상 토큰 중복이 사라졌습니다.
  • 안내 배너를 숨기고 있었던 알림도 즉시 라우팅되면서 QA 팀이 테스트를 훨씬 빠르게 끝낼 수 있었습니다.
  • 다음엔 백엔드에서 토큰 중복을 더 정교하게 정리하고, 앱 내에서 실패 원인을 Toast로 바로 보여주려 합니다.
  • 여러분 팀은 Expo 푸시 등록 과정에서 어떤 문제를 자주 만났나요? 댓글로 경험을 알려주시면 감사하겠습니다.

Reference

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

0개의 댓글