ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [RN 강의] Expo Go 그 이상의 기능들 (development builds)
    강의노트/React Native 2025. 9. 29. 15:29

     

    1. 로컬 Push 알림 보내기

     앱을 쓰는 가장 큰 이유 중 하나인 Push 알림 기능을 추가해본다. Push 알림을 통해서 앱을 열 수 있기 때문에, 사용자의 방문 횟수를 늘릴 수 있고, 다양한 마케팅 프로모션을 진행할 수 있다. 웹서비스로도 충분한 경우가 많지만, 고객에게 소식을 전달할 수 있는 방법이 Email, 문자 전송 밖에 없다. 이러한 Push 알림의 이점으로 인해 서비스에 모바일 앱을 적용할 정도로 Push 알림이 중요하다.

     하지만 이렇게 중요한 Push 알림 기능을 직접 구현하는 것은 매우 복잡하고 어려운 일이다. 그래서 대부분 라이브러리를 사용한다. Expo에서는 자체 서버가 있더서 더 간단하게 만들어 뒀다. 이전에는 구글의 경우 FCM(Firebase Cloud Messaging), 애플은 APNS(Apple Push Notification)의 서비스를 사용해서 각 플랫폼 마다 기능구현을 따로 해줘야 했다. Expo를 사용하면 설정 한 번으로 Expo Push Service를 통해서 요청을 구글과 애플 양쪽으로 보내주는 편리함을 누릴 수 있다.

     Push 알림은 백엔드 서버에서 보내는 것이다. 예를들어 어떤 사람이 내 게시글에 좋아요를 눌렀다면, 그 정보를 백엔드로 보내고, 백엔드에서 내 앱에 어떤 사람이 좋아요를 눌렀다는 것을 알려주는 것이다. 그래서 Push 알림을 하기 위해서는 백엔드를 한번 거쳐서 요청을 보내기 때문에 반드시 백엔드가 필요하다. 물론 백엔드 없이 Expo Push Service로 바로 HTTP 요청을 보낼 수 있지만, 사실상 실무에서는 거의 쓰이지 않는다.

     

    알림 기능 종류

    • Push (remote) 알림: 원격 서버에서 사용자 기기로 전송되는 알림
    • Local (in-app) 알림: 일정/캘린더 기능과 같이 앱 내에서 생성되고 표시되는 알림

    Expo Notifications 설치 및 설정

     아래와 같이 expo 알림을 설치하고, 공식문서를 참조하여 app/_layout.tsx에 설정 코드를 추가해준다.

     알림 설정을 하는 핸들러를 보면 소리, 알림 개수를 표시하는 뱃지, 배너나 리스트 표시를 해줄 지 선택할 수 있고, F12를 눌러서 더 자세한 속성들을 살펴볼 수 있다. 타입을 파고 들어가보면 NotificationBehavior 타입에 Priority 속성도 존재하는데 DEFAULT로 설정해두는 것이 좋다. 서비스 중인 앱이 더 중요하다고 해서 우선순위를 MAX로 두면 사용자 입장에서는 불편함을 느낄 수 밖에 없다.

    npx expo install expo-notifications
    import * as Notifications from 'expo-notifications';
    
    // First, set the handler that will cause the notification
    // to show the alert
    Notifications.setNotificationHandler({
      handleNotification: async () => ({
        shouldPlaySound: false,  // 소리
        shouldSetBadge: false,   // 알람 개수 뱃지
        shouldShowBanner: true,  // 배너
        shouldShowList: true,    // 리스트
      }),
    });
    
    // Second, call scheduleNotificationAsync()
    Notifications.scheduleNotificationAsync({
      content: {
        title: 'Look at that notification',
        body: "I'm so proud of myself!",
      },
      trigger: null,
    });

     

     알람을 실제로 보내는 비동기 함수에 trigger에는 알아둘 것이 있다. 안드로이드 전용 기능인 ChannelAwareTriggerInput 타입을 통해 channelID를 적어줄 수도 있다. 여기서 채널이란 알람 유형별로 아이디를 붙여두었다고 생각하면 된다. 예를들면, 좋아요/게시글 작성 알림/팔로잉/팔로잉 중인 사람의 활동 등의 다양한 알림 유형이 있는데, 이들이 모두 전용 채널이 된다. 알람 설정에 가면 각 채널별로 알림 수신 여부를 설정할 수 있다. 안드로이드에서는 이렇게 채널을 구분해서 메시지를 보내면 된다. iOS에서는 채널은 없지만 카테고리(setNotificationCategoryAsync)를 대신 사용해서 메시지를 담아 보낼 수 있다.

     참고로 세부 속성으로 Trigger를 null로 설정하면 바로 알림을 보낸다는 의미이다. 로컬 알림까지는 Expo Go를 통해서 보낼 수 있다.

     

    trigger 세부 설정

    • ChannelAwareTriggerInput: 특정 채널에 알림을 보내기
    • SchedulableNotificationTriggerInput
      • calendarTriggerInput: (iOS 전용) 특정 날짜에 반복해서 보내기 등 아래 옵션들을 다 합쳐두었음
      • TimeIntervalTriggerInput
      • DailyTriggerInput: 하루에 한번 보내기
      • WeeklyTriggerInput: 1주에 한번 보내기
      • MonthlyTriggerInput: 1달에 한번 보내기
      • YearlyTriggerInput: 1년에 한번 보내기
      • DateTriggerInput: 특정 날짜에 보내기

    추가로 알림을 받았을 때 성공이나 실패를 기록할 수 있게 로그를 찍는 옵션도 존재한다.

    Notifications.setNotificationHandler({
      handleNotification: async () => ({
        shouldPlaySound: false,
        shouldSetBadge: false,
        shouldShowBanner: true,
        shouldShowList: true,
      }),
      handleSuccess(notification) {
        console.log('handleSuccess', notification);
      },
      handleError(notification, error) {
        console.log('handleError', notification, error);
      },
    });

     

    /app/_layout.tsx에 앱이 시작되었을 때 알람을 보내도록 해본다. 이때 알람을 보내는 기능도 사용자의 권한 승인이 필요하여 아래와 requestPermissionsAsync()를 수행해줘야 한다. 권한이 없다면 사용자가 직접 설정창을 열어서 권한을 수동으로 설정하게 하도록 유도해준다.

    // app/_layout.tsx
      ...
    
      const onImageLoaded = async () => {
        try {
          // 데이터 준비
          await Promise.all([
            AsyncStorage.getItem('user').then((user) => {
              updateUser?.(user ? JSON.parse(user) : null);
            }),
            // TODO: validating accessToken
          ]);
          await SplashScreen.hideAsync(); // 수동으로 SplashScreen 숨기기
    
          const { status } = await Notifications.getPermissionsAsync();
          if (status !== 'granted') {
            return Linking.openSettings();
          }
          const notification = await Notifications.scheduleNotificationAsync({
            content: {
              title: 'Look at that notification',
              body: "I'm so proud of myself!",
            },
            trigger: null,
          });
          console.log('notification', notification);
        } catch (error) {
          console.error(error);
        } finally {
          setIsAppReady(true);
        }
      };

     

    참고로 expo-notifications에서 안드로이드 Push 알림 기능을 못쓴다는 Error 발생하더라도, Local Notifications 기능을 사용할 수 있으니 안심하자.

    안드로이드 Push 알림 에러

     

    2. Remote Push 알림

     Push 알림을 하기 위해서는 EAS로 클라우드에 빌드 한번 수행하며 EAS 설정을 해야 하고, Dev 빌드를 통해 실제 기기에 설치도 한번 해주어야 한다.

     앞서 했던 Remote 알림 코드 부분을 remote 알림으로 수정해본다. 우선 pushToken을 받아야 한다. token에는 데이터가 있고 이를 state에 저장을 해줘야 한다. PushToken은 알림을 특정 사용자의 특정 기기로 보내기 위한 고유 식별자(주소)이다. 서버는 이 토큰을 식별하여 알림을 발송한다. 이 정보는 서버의 데이터베이스에도 저장을 하고 있어야 한다. 그래서 전체 알람을 보낸다 하면 서버에 저장된 pushToken 전체에 알림을 다 보내주는 것이다.

     아래 코드에서 expoConfig는 app.json에 해당하고, easConfig가 eas.json에 매칭되어 값을 불러와 사용한다. 만약 app.json과 eas.json에 projectId가 없어도 괜찮다. EAS를 통해서 빌드를 하면 자동으로 projectId가 생성된다. 

    // /app/_layout.tsx
    
      ...
    
          const { status } = await Notifications.getPermissionsAsync();
          if (status !== 'granted') {
            return Linking.openSettings();
          }
          // 로컬 알림 예제
          // const notification = await Notifications.scheduleNotificationAsync({
          //   content: {
          //     title: 'Look at that notification',
          //     body: "I'm so proud of myself!",
          //   },
          //   trigger: null,
          // });
          // console.log('notification', notification);
          const token = await Notifications.getExpoPushTokenAsync({
            projectId:
              Constants.expoConfig?.extra?.eas?.projectId ??
              Constants.easConfig?.projectId,
          });
          console.log('token', token);
          // TODO: save token to server
          setExpoPushToken(token.data);
    
      ...
    // app.json
    {
      "expo": {
        ...
        "extra": {
          "router": {},
          "eas": {
            "projectId": "5164c622-03d9-432c-b242-b21248326681"
          }
        }
      }
    }

     

    공식문서를 참고해서 Remote Push 알림을 보내는 함수를 추가했다. 이전에 발급받아 상태에 저장한 token을 사용해서 백엔드 서버로 HTTP 요청을 보낸다. 처음 설명에서 실무에서 사용하지는 않지만, remote 알림을 백엔드 서비스 없이도 push 알림을 보낼 수 있는 방법이 있다고 했었다. 바로 Expo push 서비스에 바로 HTTP 요청을 보내는 것이다. sendPushNotification을 사용하면 remote Push 알림을 받을 수 있다. Remote push 알림이 동작하는지만 확인할 것이기 때문에 예제 그대로 두고 테스트를 수행한다.

     Push 알림을 보낼 때 Expo Go 시뮬레이션 환경에서는 (Push 알림을 지원하지 않는) 에러가 발생하기 때문에 useEffect에 실제 기기(Device.isDevice)에서만 동작하도록 조건을 추가한다.

    import * as Device from "expo-device";
    
    ...
    
    async function sendPushNotification(expoPushToken: string) {
      const message = {
        to: expoPushToken,
        sound: 'default',
        title: 'Original Title',
        body: 'And here is the body!',
        data: { someData: 'goes here' },
      };
    
      await fetch('https://exp.host/--/api/v2/push/send', {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Accept-encoding': 'gzip, deflate',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(message),
      });
    }
    
    // token 받은 코드
    
      useEffect(() => {
        if (expoPushToken && Device.isDevice) {
          sendPushNotification(expoPushToken);
        }
      }, [expoPushToken]);

     

    expo-device 설치

    Device 객체에는 실제 기기인지 여부나 제조사(애플, 샤오미 등) 정보, 모델 아이디, 모델명 등의 기기관련 정보를 가지고 있다. 그리고 indext.js에 있는 miragejs 코드에서도 동일하게 실제 기기가 아닐 때만 동작하도록 조건을 추가한다. 실제 기기 환경에서 miragejs 코드를 비활성해두지 않으면, feth 함수에서 요청보내는 주소가 실제 expo 서버로 전송되는 것이 아니라 miragejs로 전송된다. 이걸 빠뜨리고 십분이 넘게 걸리는 빌드를 했다가 발견할 수도 있으니 주의하도록 하자. 

    npx expo install expo-device
    // index.ts
    
    import * as Device from 'expo-device';
    
    ...
    
    if (__DEV__ && !Device.isDevice) {
      ...

     

     

    앞의 Push 알림을 보내는 과정을 정리하면 아래와 같다.

    1. 알림을 받았을 때 어떤 식으로 알림을 줄지 설정

    2. push 알림 권한을 받기

    3. 권한을 얻었다면 pushToken을 받아서 앱에 저장

      3-1. pushToken을 서버로 보내서 저장/관리

    4. (pushToken을 발급 받았고 실제 기기라면) push 알림을 현재 자신으로 앱으로 보내기

     

    3. Android에 필요한 Push 알림 설정

    안드로이드에서는 이후 과정을 추가해야 Push 알림을 받을 수 있다.

     

    Firebase 프로젝트 생성

    안드로이드에서 Push 알림을 보내기 위해서는 Firebase 계정과 관련 설정이 필요하다. 구글 클라우드 플랫폼 계정 또는 Firebase 계정을 만들고 접속한다. Firebase 콘솔에서 프로젝트를 만들기를 선택하고 threads 프로젝트를 생성해준다. Google 애널리틱스도 하나 구성해서 연결해준다. 애널리틱스는 강의에서 안 쓸 것이라 그렇게 중요하진 않지만, 만약 앱에 애널리틱스를 붙일 생각이 있다면 이렇게 프로젝트 생성 시에 연결해서 같이 쓰면 좋다. Firebase Analytics를 사용해서 앱에서 발생하는 유저 활동을 추적해 볼 수도 있다. 

    Firebase Console 신규 프로젝트와 구글 애널리틱스 생성

     

    FCM V1 service account 키 생성

    프로젝트가 생성되면 프로젝트 개요 옆에 있는 설정 버튼을 눌러 '서비스 계정' 탭을 연다. 이곳에서 JSON key 2개를 만들 것이다. 하나는 Firebase Admin SDK를 위한 service account 키를 하나 생성하고, 생성된 파일을 프로젝트 루트 경로에 넣어준다. 이 JSON key는 외부로 유출되면 안된다. 그렇기 때문에 .gitignore에 등록하여 Github에 업로드 되지 않도록 해준다.

    // .gitignore
    ...
    
    app-example
    google-services.json
    threads-*.json

     

    Google Services 키 생성

    미리 .gitignore에 추가해두었던 google-services.json 파일은 프로젝트 설정의 '일반' 탭에서 생성할 수 있다. 하단 '내 앱'에서 안드로이드 이미지의 버튼을 누르면, 앱 등록 절차부터 시작하게 된다. 앱 등록이 끝나면 google-services.json 파일을 다운로드 받을 수 있다. 이 파일도 Firebase Admin SDK 키와 동일하게 유출되면 안된다.

    google-services.json 파일 생성

    앞서 생성한 두 JSON 키 중에 더 중요한 것을 고른다면, 서비스 계정 탭에서 생성한 Firebase Admin SDK 키라고 볼 수 있다. 만약 해당 JSON 키가 유출되면 Firebase가 통으로 유출될 수 있다는 의미이다.

     

    이제 app.json에 생성한 googleServices.json을 등록한다.

    {
      "expo": {
        ...
        "android": {
          ...
          "package": "com.morgankim.threads",
          "googleServicesFile": "./google-services.json"
        },
        "web": {
          ...
      }
    }

     

    이제 공식문서의 Create a new Google Service Account Key 과정을 따라 Firebase 정보를 프로젝트에 반영해준다. 'Use an existing Google Service Account Key' 과정은 진행하지 않아도 된다.

    1. eas credential

    eas credentials

    eas credentials 실행

    이미지에서는 "Set up Google Service Account Key for Push Notifications (FCM V1)"을 선택했지만, "Select an existing Google Service Account Key for Push Notifications (FCM V1)"을 선택한다. 그러면 app.json에 등록한 대로 google-services.json를 찾아 정보를 찾아 등록된다.

    그리고 다시 원하는 작업을 선택하라고 나오는데,  "Go back" > "Exit"으로 eas credentials 설정을 종료한다.

     

    2. app.json에 googleServicesFile 등록 (했음)

    3. Expo.dev에 Project Credentials 등록하기

     Expo.dev 페이지에 들어가서 로그인을 하면 eas로 빌드했던 프로젝트가 나온다. 여기서 threads 앱에 해당하는 프로젝트의 credentials - android - Sevice Credentials에 Firebase Admin SDK를 위한 service account 키 파일을 등록해주면 된다. 나중에 앱을 PlayStore에 제출할 때 이 키가 똑같이 쓰인다. 이때 똑같은 파일로 올려주면 된다.

    Expo.dev에 service key 등록

     

    eas 빌드

    Expo Go 무료 유저의 경우 최대 1시간까지 대기열이 있을 수 있다. 특히 외국에서 많이 사용하는 시간대 (한국시간 밤 11시) 는 피하도록 하자. 그리고 월 30번 제한도 있다. 클라우드 빌드를 무료로 풀어줬다는 것은 좋지만, ChatGPT의 무료 사용자 버전이 느린 것처럼 Expo Go 무료 사용자 버전도 느리다. 회사의 경우 유료 결제를 진행해서 사용할 것이다.

     빌드 중에 프로그램을 설치하라고 할 수 있는데, eas 빌드는 Expo Go를 벗어나는 것이기 때문에 expo-dev-client를 추가적으로 설치해줘야 한다. 빌드를 할 때 터미널 두개를 띄워서 android와 ios 빌드를 함께 진행해주면 좋다. 그런데 무료 유저의 경우 Android와 iOS 빌드 시에 한번에 하나밖에 빌드할 수 있다. 그래서 무료 사용자는 Android가 끝나야 iOS를 빌드할 수 있다.

    # android 빌드
    eas build --platform android --profile development
    
    # ios 빌드
    eas build --platform ios --profile development
    
    # eas 없이 빌드하는 방법
    npx expo run:android

     

    처음 빌드 해서 Expo 클라우드에 올라가 있는 프로젝트가 나오면 재사용해주자. Android application id를 물어보면 Firebase에서 설정한 id를 넣어주면 된다. 이 id가 패키지 이름이 된다. iPhone에서도 Bundle Identifier로써 카카오 로그인이나 애플 로그인 같은 소셜미디어 로그인할 때도 사용되기 때문에 잘 기억해두자.

     android 빌드의 경우 새로운 Android KeyStore를 생성할지 물어보는데 Yes를 눌러주자.

     iOS 빌드의 경우 apple 로그인을 진행해주어야 한다. 로그인을 하면 6자리 코드를 사용한 기기인증을 진행하게 된다. 아이폰의 경우 개발자 계정이 필수이다. 매년 iOS 개발자 비용($99/연)을 내야 이용할 수 있는 계정이다. 빌드 중에 프로비저닝 프로파일 설치가 필요하다. 프로비저닝 프로파일이 없다면 발급을 받아야 한다. 제로초 강의 중에는 iPhone에 발급된 프로파일을 미리 설치해둔 상태였다. iPhone과 iOS 빌드 시에 같은 프로비저닝 프로파일을 사용하지 않으면 설치가 되지 않는다. 이 경우 무결성 검사에 실패했다는 문구가 뜨면서 설치가 안 된다. 그 다음으로 Push 알림 사용 여부에 "Yes"라고 답하면, Apple Push Notification service key를 생성할 것인지 물어볼 때 생성하면 된다. 기존 게 없다면 무조건 새로 발급 받는다.

     eas 빌드가 끝나면 app.json에 "extra" 속성으로 projectId가 생성된다. 이 projectId가 pushToken을 발급받을 때 사용된다. 터미널 창에 생성되는 QR 코드를 통해서 설치받을 수 있다. 안드로이드의 경우 QR 코드 밑에 있는 링크를 통해서도 다운로드를 받아 설치할 수 있다. 이는 애뮬레이터에서도 동일하게 동작하기 때문에 링크를 통해서 애뮬레이터에 앱을 설치할 수 있다.

     안드로이드 애뮬레이터에서 Push 알림을 보내도 동작하지 않는다. Push 알림은 실제 기기가 필요하다. 컴퓨터와 모바일이 같은 와이파이를 사용하고 있다면 npx expo start로 Expo Server를 실행하여 IP 또는 QR 코드 스캔으로 접속할 수 있다. 주의할 것은 내가 Development build를 사용하고 있는지, Expo Go를 사용하고 있는지 잘 구분해야 한다. (push 알림 테스트를 위해서는 development build로 선택하자. 그래야 Native 기능을 사용할 수 있다.)

    # 빌드를 수행하고, Expo Go와 안드로이드로 실행
    npx expo start --go --android

     

     QR코드를 기준으로 설명하면, Dev client를 설치할 때 한번 스캔하고 Metro 개발서버 연결할 때도 QR코드를 한번 스캔하여 총 두 번 스캔을 한다. 기억해야 할 것은 Expo Go나 Development build 모두 앱이 존재하며, 개발 서버가 하나 존재한다는 것이다.

     

    google-servies.json 파일이 eas build 인식 안되는 문제

    .gitignore에 google-services.json 파일이 eas에 인식되지 않는다. 터미널에서 제안하는 내용은 eas에서 인식할 수 있도록 .env와 GOOGLE_SERVICES_JSON 환경변수를 적용하라고 하는데 그렇게 좋은 방법 같지는 않다. 물론 에러도 변함이 없었다. 이 방법을 사용하기 위해서는 app.json 대신 app.config.js로 마이그레이션이 필요하다. 결국 나는 eas build를 수행할 때는 .gitignore에서 잠깐 무시 설정을 해제 해주었다.

    google-services 파일 에러

     

    iPhone에서 발생할 수 있는 에러

    No valid aps-environment entitlement string found error for iOS 에러가 뜨면, expo.dev에 접속해서 프로젝트 Credentials에 iOS의 Push Key가 잘 등록되어 있는지 꼭 확인을 해야 한다. 있는대도 해당 에러가 발생한다면 맥북에서 해결하는 영상을 참고하자. (Xcode에서 Push 알림 설정하는 방법)

     

    실제 백엔드 서버에서 필요한 작업

    보통 아래와 같이 Expo Server를 사용해서 Push 알림을 주지 않는다고 설명했다. 보통은 백엔드 서버를 어떻게 구성하는지 공식문서에 게시가 되어 있다. 실무에서는 백엔드에서 직접 Expo로 메시지를 보낸다. 아래 코드의 상황은 백엔드가 없어서 앱에서 직접 Expo에 메시지를 보낸 것이다.

    async function sendPushNotification(expoPushToken: string) {
      const message = {
        to: expoPushToken,
        sound: 'default',
        title: 'Original Title',
        body: 'And here is the body!',
        data: { someData: 'goes here' },
      };
    
      await fetch('https://exp.host/--/api/v2/push/send', {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Accept-encoding': 'gzip, deflate',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(message),
      });
    }

    Push 알림을 위한 백엔드 구성도

     

    Expo에서는 각종 언어로 expo server sdk를 만들어 두었으니 필요한 언어의 SDK를 사용하면 된다. Express 또는 Next.js라면 expo-server-sdk-node 문서를 참고해서 설치할 수 있다. 참고로 문서에 따르면 isExpoPushToken()을 통해서 프론트에서 보낸 Push 토큰을 검사할 수도 있다. 프론트에서 보내는 방식은 메시지를 담아서 POST 요청을 보냈다면, 백엔드에서는 메시지를 담아 messages.push()를 해주면 된다. 백엔드에서는 pushToken을 사용자별로 보낼 수 있도록 저장해두어야 한다. 데이터베이스에 유저와 pushToken을 연결해서 저장하는 것이다. pushToken은 일대다수의 구조를 가진다. 하나의 pushToken은 1 명의 사용자에 대해 여러명이 될 수 있다. 기계가 여러 개일 수 있고, 앱 버전이나 빌드 버전에 따라서 pushToken이 달라지는 경우가 생길 수 있기 때문이다. pushToken은 처음 1번 만이 아니라 매번 서버로 보낸다고 생각하자. 그래서 서버에서는 이미 등록되어 있는 토큰일 경우 무시하고, 등록이 되어 있지 않는 경우 등록하는 식의 로직을 채택해주면 된다. 그래서 최종적으로는 push 알림 보낼 것을 chunks로 모아서 한번에 보내준다.

    import { Expo } from 'expo-server-sdk';
    
    // Create a new Expo SDK client
    // optionally providing an access token if you have enabled push security
    let expo = new Expo({
      accessToken: process.env.EXPO_ACCESS_TOKEN,
      useFcmV1: true,
    });
    
    // Create the messages that you want to send to clients
    let messages = [];
    for (let pushToken of somePushTokens) {
      // Each push token looks like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]
    
      // Check that all your push tokens appear to be valid Expo push tokens
      if (!Expo.isExpoPushToken(pushToken)) {
        console.error(`Push token ${pushToken} is not a valid Expo push token`);
        continue;
      }
    
      // Construct a message (see https://docs.expo.io/push-notifications/sending-notifications/)
      messages.push({
        to: pushToken,
        sound: 'default',
        body: 'This is a test notification',
        data: { withSome: 'data' },
        richContent: {
          image: 'https://example.com/statics/some-image-here-if-you-want.jpg'
        },
      })
    }
    
    let chunks = expo.chunkPushNotifications(messages);
    let tickets = [];
    (async () => {
      // Send the chunks to the Expo push notification service. There are
      // different strategies you could use. A simple one is to send one chunk at a
      // time, which nicely spreads the load out over time:
      for (let chunk of chunks) {
        try {
          let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
          console.log(ticketChunk);
          tickets.push(...ticketChunk);
          // NOTE: If a ticket contains an error code in ticket.details.error, you
          // must handle it appropriately. The error codes are listed in the Expo
          // documentation:
          // https://docs.expo.io/push-notifications/sending-notifications/#individual-errors
        } catch (error) {
          console.error(error);
        }
      }
    })();

     

    Push notifications tool

    이 expo.dev notifications 웹페이지에서는 백엔드 없이 Push 알림을 보낼 수 있는 테스트 페이지이다.

    Expo.dev Push notifications tool

     

    4. 푸시 알림 클릭 시 딥링크 이동하기

    원문 코드는 Expo Notifications 공식 문서 설명에 있다. 원문 eventListener 코드를 앱 실행할 때 레이아웃에 붙여넣기만 해도 잘 동작한다. 붙여 넣은 이 훅은 useEffect를 통해서 컴포넌트가 마운트 됐을 때, 2가지 동작(getLastNotificationResponseAsync, addNotificationResponseReceivedListener)을 한다. 가장 중요한 것은 addNotificationResponseReceivedListener으로, 이 부분이 있어야 백그라운드/포그라운드 push 알림이 왔을 때 작업을 처리할 수 있는 구독과 같다. useEffect 클린업 함수에서 구독을 취소할 수 있는 remove()도 포함하고 있다. 앱이 사용자에 의해 실행 중에 완전히 종료되었을 때는 구독이 되지 않을 수 있다. 또는 예기치 못하는 상황에서 앱이 종료되었 때 마지막 알림의 유무를 통해서 구독작업을 이어서 처리해주는 것이 getLastNotificationResponseAsync의 역할이다.

    // _layout.tsx
    
    ...
    
    function useNotificationObserver() {
      useEffect(() => {
        let isMounted = true;
    
        ...
    
        Notifications.getLastNotificationResponseAsync().then((response) => {
          if (!isMounted || response?.notification) {
            return;
          }
          redirect(response.notification);
        });
    
        const subscription = Notifications.addNotificationResponseReceivedListener(
          (response) => {
            redirect(response.notification);
          }
        );
    
        return () => {
          isMounted = false;
          subscription.remove();
        };
      }, []);
    }
    
    export default function RootLayout() {
      useNotificationObserver();
      ...

     

    딥링킹을 하기 위해 라우팅 해주는 redirect 함수를 작성했다. app.json에 설정한 scheme으로 시작하다면, scheme을 슬래시(/)로 치환해서 라우팅 해주는 코드다. 문자열 치환이 싫다면 Linking을 사용하면 된다.

    // _layout.tsx
    
    ...
    
    function useNotificationObserver() {
      useEffect(() => {
        let isMounted = true;
    
        function redirect(notification: Notifications.Notification) {
          const url = notification.request.content.data?.url as string;
          if (url && url.startsWith('threadc://')) {
            Alert.alert('redirect to url', url);
            router.push(url.replace('threadc://', '/') as Href); // threads://@morgankim -> /@morgankim
            // Linking.openURL(url);
          }
        }
    
        ...
      }, []);
    }
    
      ...

     

    Push 테스트

    Expo.dev Push Notification에서 push 테스트를 하기 위해서는 preview 모드나 production 모드로 기기에 앱이 설치되어 있는 상태여야 한다. 혹시나 pushToken을 모른다면 아래처럼 Alert을 사용할 수 있다.

    // _layout.tsx
    
      useEffect(() => {
        if (expoPushToken && Device.isDevice) {
          // push notification test
          Alert.alert('sendPushNotification', expoPushToken);
          sendPushNotification(expoPushToken);
        }
      }, [expoPushToken]);

     

    웹 사이트에 push 테스트에 필요한 정보를 넣는다.

    • Recipent: [pushToken]
    • Message title: title
    • Message body: body
    • Data (JSON): { "url": "threadc://login" }

    위의 push 테스트 정보대로 보내면, push 메시지를 클릭 시에 로그인 페이지로 이동하게 된다.

     

    5. 각종 빌드 정리

    빌드 종류가 다양해서 헷갈리기 때문에 다시 한번 정리를 한다.

    • Expo Go
      • Metro (Dev Server) + Dev App (Native X)
      • 명령어: npx expo start --android (npm run android)
    • Development builds
      • Metro(Dev Server) + Dev App (Native O + prebuild)
      • android, ios 폴더는 볼 수 없지만 prebuild를 함께 수행하며 네이티브 기능을 사용할 수 있음
      • 명령어
        • npx expo start (Metro Server 실행)
        • eas build --profile development --platform android (App build, 횟수 제한 있음, 네이티브 폴더 생성 X)
        • npx expo run:android (App build, 네이티브 폴더 생성 O)
    • Prebuild
      • android, ios 폴더 생성
    • Preview/Production build
      • eas build --platform android
      • production ready (Metro Server X, 백엔드 서버 필요)
      • (MirageJS disable, if (__DEV__) 조건 필요)

    개발할 때 위에 정리한 순서대로 사용하면 된다. Expo Go로 개발하다가 Native 기능이 필요하다면 Development builds로 넘어간다. 그 다음으로는 안드로이드, iOS 폴더를 만들고 직접 네이티브를 건드릴 수 있는 빌드이다. 앱 실행과는 상없이 없으며, prebuild를 통해서 네이티브 단의 소스코드를 수정해볼 수 있다. Prebuild를 하는 순간부터는 development builds가 들어가야 한다. 그 다음인 Preview는 팀ㅁ원들 간에 실제 기기 설치를 위한 용도이고, Production은 진짜 고객들에게 서비스를 제공하는 용도이다. Preview/Production은 개발용이 아닌 완성품이기 때문에 Metro Server가 없으며 실제 백엔드 서버가 필요하다. 당연하지만 Server mocking인 miragejs도 비활성화 해야 한다.

     

    6. SNS 로그인

     OAuth 로그인 또는 SNS 로그인라고 불리는 기능을 개발해본다. Apple 정책에 따라서 소셜미디어(카카오, 구글, 페이스북 등) 로그인을 하나라도 넣으면 애플 로그인도 무조건 같이 넣어야 한다.

     

    카카오 로그인

     카카오 로그인은 한국분이 만든 React Native Kakao 라이브러리를 사용한다. 라이브러리 문서의 'Expo 설정'을 따라하면 된다. 안타까운 것은 Expo Go에서 안되는 것도 있다. 이런 경우 prebuild를 해서 Native 단을 건더는 단계로 넘어가야 한다.

     

    expo-build-properties 설치

    npx expo install expo-build-properties

     

    React Native Kakao를 설치하면 app.json의 expo 설정이 추가된다.

    "expo": {
      "plugins": [
          ...
          "expo-build-properties"
        ],
        "experiments": {
          "typedRoutes": true
        }
      }
    }

     

    여기에 공식문서를 참고하여 배포 시에 설정용 라이브러리를 배열 형태로 넣어준다.

      "expo": {
       "plugins": [
            ...
            [
              "expo-build-properties",
              {
                "android": {
                  "extraMavenRepos": [
                    "https://devrepo.kakao.com/nexus/content/groups/public/"
                  ]
                }
              }
            ]
          ],
        "experiments": {
          "typedRoutes": true
        }
      }
    }

     

    react-native-kakao/core 설치

    아래 명령어로 라이브러리를 설치해준다.

    npx expo install @react-native-kakao/core

     

    kakao developers에 애플리케이션 등록

    설치가 완료되면 kakao developers에서 애플리케이션을 추가한다. 애플리케이션의 정보를 넣어주고, '카카오 로그인' > 활성화 설정 > 상태: ON (활성화)를 진행해준다.

     그리고 '플랫폼' > Android > Android 플랫폼 등록을 완료해준다. iOS에도 플랫폼 등록을 해주는데, Android의 패키지명과 동일한 번들 ID를 넣어주는 것이 좋다. 번들 ID를 제외한 나머지 정보들은 앱 배포 이후에 생성되는 정보들이기 때문에 배포 이후에 추가해준다.

     카카오 로그인을 활성화 해주면 '동의항목'이 활성화 된다. 항목 중에선 닉네임, 프로필 사진에 필수 동의를 하고 앱 내에서 사용자로부터 정보들을 수집할 수 있도록 해준다. 나머지 항목들은 카카오로부터 자격 심사를 받아야 하는 항목들이다.

     

    kakao 로그인 프로젝트 설정

      앱 설정의 '앱 키'에 들어가면 네이티브 앱 키, REST API 키, JavaScript 키, Admin 키를 볼 수 있는데, 프로젝트에서는 네이티브 앱 키를 사용할 것이다. 주의할 점은 Admin 키는 절대 유출되면 안 된다. 네이티브 앱키와 함께 플랫폼별 설정을 app.json에 추가해준다.

      "expo": {
        "plugins": [
            ...
          ],
          [
            "@react-native-kakao/core",
            {
              "nativeAppKey": "sdflkjsdlfkjdsflkjdskldsklskl",
              "android": {
                "authCodeHandlerActivity": true
              },
              "ios": {
                "handleKakaoOpenUrl": true
              }
            }
          ]
        ],
        "experiments": {
          "typedRoutes": true
        }
      }
    }

     

    login.tsx에서 초기화를 한번 해줘야 한다. useEffect 사용해서 로그인 페이지 렌더링 이후 앱 키를 초기화 하는 것이다. 네이티브 키는 유출되도 되지만, 이런 문자열 키가 프론트 코드에 있다면 노출될 위험이 있다. 앱이라고 해서 못 열어보는 것이 아니라 소스코드 난독화는 되어 있더라도 열어볼 수 있는 위험이 있다. 그래서 나중에 이런 값들은 서버에서 받아오는 것이 안전하다. 서버에서 받아오는 타이밍은 스플래시 스크린이 돌아가는 도중에 받은 후에 초기화를 진행해주면 된다.

    npx expo install @react-native-kakao/user
    // login/tsx
    
    ...
    
    import { initializeKakaoSDK } from "@react-native-kakao/core";
    import { login as kakaoLogin } from "@react-native-kakao/user";
    
    export default function Login() {
      ...
      
      useEffect(() => {
        initializeKakaoSDK("sdflkjsdlfkjdsflkjdskldsklskl");
      }, []);
      
      const onKakaoLogin = async () => {
        const result = await kakaoLogin();
        console.log(result);
      };
      
      ...
    }

     

     

    ⚠️ 카카오 로그인을 할 때 꼭 development builds로 진행한다.

     

    keyHash 추출하기

    kakao developers의 플랫폼에 패키지 ID 뿐만아니라 keyHash 값도 등록해야 한다. 이 값을 추출하는 방법은core 패키지의 getKeyHashAndroid() 메서드 response를 console.log()로 찍어보는 것이다. 애뮬레이터에서 다시 로그인 페이지로 들어가면 터미널에 찍히는 keyHash 값을 Kakao developers의 플랫폼(Android)에 등록한다. 나중에 빌드를 다시하거나 해서 keyHash 값이 바뀐다면 다시 등록을 해줘야 한다.

    // login/tsx
    
    ...
    
    import { initializeKakaoSDK, getKeyHashAndroid } from "@react-native-kakao/core";
    import { login as kakaoLogin } from "@react-native-kakao/user";
    
    export default function Login() {
      ...
      
      useEffect(() => {
        initializeKakaoSDK("sdflkjsdlfkjdsflkjdskldsklskl");
      }, []);
      
      const onKakaoLogin = async () => {
        console.log(getKeyHashAndroid());
        try {
          const result = await kakaoLogin();
          console.log(result);
          // TODO: 이후 서버로부터 사용자 정보를 받아오기
          // TODO: AsyncStorage, SecureStore에 저장하기
        } catch (error) {
          console.log(error);
        }
      };
      
      ...
    }

     

     카카오 로그인이 성공하면 result 값이 console.log()로 터미널에 찍힐 것이다. 카카오 로그인 API는 쉽게 생각하면 비밀번호를 서버에서 검사해야 하는데, 카카오에서 대신 해준 것이라고 이해할 수 있다. 카카오 로그인이 성공하면 이후에 할 작업은 서버로부터 사용자 데이터를 받아오는 일이다. 카카오에서 주는 정보와 우리 팀의 백엔드 서버가 가지고 있는 정보는 다른 정보이다. 카카오로부터 사용자의 고유한 값(id, nickname, profileImageUrl 등)을 받아 오면 AsyncStorage에 저장하면 된다.

     카카오 로그인 자체는 비밀번호 검사를 대신해줄 뿐이라고 생각하면 된다. ⚠️ 주의할 점은, 카카오 login()의 결과값에 포함된 accessToken과 refreshToken을 우리 앱에서 사용할 토큰들이라고 생각하면 안 된다. 이 값들은 카카오 API에서 사용할 수 있는 토큰들이다. (카카오 공유하기, 채널 API 등등) 다른 소셜미디어도 마찬가지이다. 그래서 이와는 별개로 서버로부터 유저정보를 불러오면서 토큰을 발급하는 과정은 따로 개발해줘야 한다.

     

    애플 로그인

    애플 로그인은 iOS에서만 동작한다. 애플 로그인 또한 로그인에 성공하면 토큰을 주지만 이는 애플 API 요청을 위한 토큰이기 때문에 사용자 정보나 나의 앱 API를 위한 토큰은 팀내 백엔드 서버로부터 따로 받아와야 한다. 다른 소셜 로그인과 다르게 애플 로그인은 Expo Go에서도 동작한다. (prebuild 필요없음)

     

    애플 로그인에 필요한 라이브러리를 설치하고, app.json에 ios와 plugins 설정을 추가해준다.

    npx expo install expo-apple-authentication
    // app.json
    {
      "expo": {
        ...
        "ios": {
          "supportsTablet": true,
          "usesAppleSignIn": true
        }
        ...
        "plugins": [
          [
            ...
          ],
          "expo-apple-authentication"
        ],
        ...
      }
    }

     

    애플 로그인 이벤트 핸들러 함수를 작성해준다. 이 onAppleLogin 함수를 를 통해서 이름과 이메일을 받아올 것이다. 정보들은 credential에 담길 것이고 iOS 기기에서만 확인할 수 있다. Android에서는 조건부(Platform.OS === 'ios')로 애플 로그인을 빼주면 된다. 애플 로그인도 비밀번호가 맞는지를 통해 유효한 로그인인지를 대신 체크해주는 것이지 사용자 정보를 주는 것은 아니다. 그래서 사용자 정보는 서비스의 백엔드 서버에 따로 저장하고 관리를 해야 한다.

     로그인 후에 애플 로그인에 성공한 사용자와 우리 서비스의 사용자가 서로 맞는지 체크도 해야 한다. 이때는 고유한 값인 credential.user와 credential.email을 사용해서 서버에 저장된 사용자와 일치하는지 확인하면 된다.

    ...
    import * as AppleAuthentication from "expo-apple-authentication";
    
    export default function Login() {
      ...
      useEffect(() => {
        initializeKakaoSDK("csdfkljsdfkljljkskdlfjskldjflkjlkj");
      }, []);
      
      const onAppleLogin = async () => {
        try {
          const credential = await AppleAuthentication.signInAsync({
            requestedScopes: [
              AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
              AppleAuthentication.AppleAuthenticationScope,EMAIL,
            ],
          });
          console.log(credential);
        } catch (error) {
          console.log(error);
        }
      }
      ...
    }

     

    OAuth 로그인은 비밀번호 인증을 소셜미디어가 대신 해주는 것이고, 사용자 정보를 저장해주는 것이 아니다. 애플이나 카카오 로그인에 성공하면 credential.user와 credential.email을 서버로 보내서, 서버로부터 사용자 정보와 토큰을 따로 받아와서 AsyncStorage 또는 SecrueStore에 저장해줘야 한다.

     

    7. 보안 정보 관리: .env

     카카오 로그인에 사용한 Native 앱 키는 다행이 앱용으로 앱에서 노출될 것을 예상하고 있기 때문에 문자열 그대로 코드에 사요해도 괜찮다. 그렇다 하더라도 이런 보안 이슈가 있는 문자열 키를 그대로 넣는 경우는 거의 없다. 또 코드에 중복해서 넣어줄 경우 수정해야 하는 경우 일일이 다 찾아서 수정해줘야 하는 불편함이 발생한다.

     Expo 또한 .evn를 지원한다. 카카오 Native 앱 키의 경우 앱에서도 사용할 수 있다. 앱에서 .env에 설정한 환경 변수를 사용하려면 특별한 접두사(prefix) 'EXPO_PUBLIC_'이 필요하다. Next.js에서도 유사하게 NEXT_PUBLIC_ 을 붙여주는 것과 같다. 해당 환경변수는 코드 상에서는 process.env.EXPO_PUBLIC_ 형태로 사용할 수 있다. 주의할 점은 무조건 점(.)으로 접근해야지 대괄호([])는 빌드시 지원하지 않는다.

    // .env (등록)
    EXPO_PUBLIC_KAKAO_APP_KEY=ajsdlkfjalsdkfjslkdjfllsdlk
    
    // login.tsx (사용)
    useEffect(() => {
      initializeKakaoSDK(process.env.EXPO_PUBLIC_KAKAO_APP_KEY as string);
    }, []);

     

    간혹 Expo 코드를 보다보면 Expo constants를 사용하는 코드를 볼 수 있다. 이런 경우에는 EXPO_PUBLIC_ 접두사를 붙이지 않아도 된다. 대신 app.json을 app.config.js로 바꿔서 따로 등록해야 한다. 단순히 JSON 파일을 js 객체형태로 만들어서 export default를 해주고 process.env 를 app.config.js에 적용해주면 된다. app.config.js 파일을 사용하면 .env에 적용한 값을 앱 코드상에서 사용할 수 없다. 새로운 환경변수를 등록하기 위해서는 expo의 extra 속성으로 추가해줘야 앱에서 사용할 수 있다.  앱 코드 상에서는 EXPO_PUBLIC은 안 붙였지만 expo 설정의 extra를 통해서 값을 접근할 수 있게 된다.

    import Constants from "expo-constants";
    
    export default function Login() {
      ...
      useEffect(() => {
        initializeKakaoSDK(Constants.expoConfig?.extra?.kakaoAppKey as string);
      }, []);
      
      ...
    // app.config.js
    
    export default {
      expo: {
        ...,
        plugins: [
          {
            nativeAppKey: process.env.KAKAO_APP_KEY,
            android: {
              authCodeHandlerActivity: true,
            },
            ios: {
              handleKakaoOpenUrl: true,
            },
          },
          "expo-apple-authentication",
        ],
        extra: {
          kakaoAppkey: process.env.KAKAO_APP_KEY,
          router: {},
          eas: {
            projectId: "sdlfkjs-sdfs-sdfsd-sdfsdf-dldlkfjsdlkfj"
          },
        },
      },
    };

     

    .env 우선순위 (.env < .env.local)

    .env 파일과 .env.local 파일이 공존한다면, 우선순위는 .env.local이 더 높다. 그래서 .env 파일에는 공통인 환경 설정을 두도록 한다. 로컬의 경우에 개인 PC에서 .env.devleopment 파일을 만들어서 개발용 서버에서 쓰는 식으로 환경별로 설정값을 분산해서 사용할 수 있다.

     EAS Build 시에 Git에 커밋되지 않는 파일(.gitignore 등록된 파일)은 인식되지 않는다. 그 결과로 KAKAO_APP_KEY 처럼 .env에 등록된 변수들이 undefined가 될 수 있다. 그래서 EAS Build를 할 때는 항상 커밋이 된 파일들만 참조를 한다. 추후 EAS Update라는 앱 심사를 받지 않고 업데이트를 하는 기능을 쓸 때가 있다. 이때도 google-services.json이나 threads-clone-*.json 파일은 비밀키이기 때문에 Git에 커밋을 하면 안 된다. 그리고 커밋이 안된 파일들은 EAS 빌드에서 인식하지 못해 제외된다. 그때는 잠깐 빌드할 때만 잠깐 .gitignore 에서 빼어서 Git에 인식이 되게 하고 빌드가 끝나면 다시 되돌려 놓는다. 그런데 이렇게 할 경우 Git에 커밋을 해버린다든가 실수할 가능성이 높아진다.

     이런 경우에는 Expo.dev 페이지에서 프로젝트를 선택하고 .env를 업로드 하거나 직접 환경변수를 등록해주면 된다. 변수는 Plain Text, Sensitive, Secret으로 구분하여 공개되지 않도록 설정해줄 수 있다. 또한 해당 환경변수를 production, preview, development에서 사용할지 여부를 선택할 수 있다. 환경변수를 Secret으로 설정하면 다시 그 하위 보안 단계로는 수정할 수 없다.

     문제는 Expo.dev에 환경변수를 Secret으로 설정한다고 해도 앱 리버스 엔지니어링으로 해커들이 다 열어볼 수가 있다. 결국 등록한 값들은 다 노출이 될 수 있다. 그래서 환경 변수에는 절대로 민감한 값은 넣지 않는 게 좋다. 앱을 열어보면 비밀키들이 다 평문으로 나오는 경우가 많다.

     그렇다면 정말 비밀 키들은 어떻게 관리할 것인가? EXPO_PUBLIC을 빼거나, app.config.js에서 extra 속성에 추가를 하지 않는 것이다. 앱 코드로 값을 보내버리면 그 순간 노출이 되는 것이기 때문이다. 앱에서 사용되어야 하는데 절대 노출되면 안되는 상황에서는 모두 서버에서 관리하고 요청해서 받아와야 한다.

     다행이도 카카오 SDK 키와 같이 대부분 앱에서 사용될 것이라 예상하고 만들어진 네이티브 앱 키는 앱 코드 상에 들어가 있어도 문제가 되는 경우는 없다. kakao developers 내 애플리케이션에 보면 어드민 키, REST API 키 같은 것들이 정말 노출되면 안되는 키들이다. REST API 키는 서버에서만 사용해야 되며, 웹에서 노출돼도 되는 키는 JavaScript API 키가 따로 존재한다. 앱에서 노출되도 되는 키는 앱 Native 키로 이렇듯 보통 따로 구분되어 있기 때문에 키를 잘 선택해서 사용해야 한다. 참고로 카카오 앱 키는 노출이 된다 하더라도 현재 내 앱에서만 쓸 수 있기 때문에 노출이 되도 되는 것이다. 어떻게 내 앱인지는 Developers 콘솔에서 패키지 이름(번들 id)이나 keyHash를 등록했기 때문에 알 수 있다.

    8. App Icon 바꾸기

    앱 아이콘을 바꾸는 방법은 매우 간단하다. app.json 파일에서 icon 항목만 수정해주면 된다. 앱이 실행중이었다면 한번 꺼주고 재실행해주면 적용이 된다.

    // app.json
    {
      "icon": "./assets/images/icon.png"
    }

     

    터미널상에서 실행환경이 Expo Go라면 Hot Reload 하고, Development 환경이라면 다시 빌드를 따로 수행해줘야 한다.

    # 껐다가
    (Ctrl + c)
    
    # 앱 재실행
    npm run android      # or 'npx expo start --android'
    
    # 다른 터미널에서 빌드
    npx expo run:android

     

    그런데 모든 기기에서 적절한 사이즈의 아이콘이 보이게끔 해주면 좋을 것 같다. 안드로이드의 경우 android.adaptiveIcon (또는 Android Adaptive Icon Guidelines), iOS의 경우 OS별로 잘 보이도록 제공하는 Apple Human Interface Guidelines이 있다. 아래 iOS의 아이콘 설정처럼 별도로 다 이미지를 제공해주는 것이 좋다. app.json에 기본으로 android에 적용된 adaptiveIcon은 다크 모드에 최적화된 아이콘을 사용하고 있다.

    // app.json
    
    {
      "expo": {
        ...,
        "android": {
          "adaptiveIcon": {
            "foregroundImage": "./assets/images/adaptive-icon.png",
            "backgroundColor": "#ffffff"
          },
          "edgeToEdgeEnabled": true,
          "package": "com.morgankim.threadsclone"
        },
        "ios": {
          "icon": {
            "dark": "./assets/images/ios-dark.png",
            "light": "./assets/images/ios-light.png",
            "tinted": "./assets/images/ios-tinted.png"
          }
        },
        ...,
      }
    }

     

    공식문서를 보면 스플래시 스크린도 다크 모드가 지원된다. 아래 코드처럼 pulugins에 expo-splash-screen을 추가하고 dark 옵션으로 다크모드를 추가할 수 있다. 공식문서를 잘 읽어보면 간단하게 바꿀 수 있다.

    // app.json
    
    {
      "expo": {
        "plugins": [
          [
            "expo-splash-screen",
            {
              "backgroundColor": "#232323",
              "image": "./assets/splash-icon.png",
              "dark": {
                "image": "./assets/splash-icon-dark.png",
                "backgroundColor": "#000000"
              },
              "imageWidth": 200
            }
          ]
        ],
      }
    }

     

    .

    .

    .

    .

    .

    .

    관련 코드들

     

    GitHub - redcontroller/threads-clone: 제로초 React native 학습 레포

    제로초 React native 학습 레포. Contribute to redcontroller/threads-clone development by creating an account on GitHub.

    github.com

     

     

     

    출처

    React Native with Expo: 제로초에게 제대로 배우기

     

    React Native with Expo: 제로초에게 제대로 배우기| 제로초(조현영) - 인프런 강의

    현재 평점 4.9점 수강생 553명인 강의를 만나보세요. 웹 개발자가(특히 React 개발자라면) 정말 손쉽게 앱을 출시할 수 있는 시대가 되었습니다. Expo와 함께라면 더더욱 빠르게 Android와 iOS 앱 모두를

    www.inflearn.com

     

Designed by Tistory.