한달 전 앱을 론칭해 운영중에있다.

초기 서비스인 만큼 많은

굉장히 짧은 주기로 버전 업데이트를 진행하고있다.

 

문제는

1. 사용자들이 배포된 업데이트를 진행할것인가?

2. 코드 푸시는 만능일까?

3. 어떤게 최선의 방법이지?

 


 

문제 1. 사용자들이 배포된 업데이트를 진행할것인가?

사용자들이 와이파이를 환경에서

자동업데이트를 하는게 아니라면

직접 업데이트를 진행하는 경우는 매우 드물었다.

따라서, 사용자들 간에 다른 버전을 쓰는 경우가

많아지기 시작했다.

 

문제 2. 코드 푸시는 만능일까?

우리는 사용자가 업데이트를

잘 하지 않는다는것을 확인하고

코드 푸시를 통해 업데이트를 진행하기 시작했다.

 

아주 만족스럽게도

업데이트 내용을 대다수의 사용자에게

온전히 제공할 수 있었다.

 

문제는 다른곳에서 발생했다.

코드 푸시는 앱 버전 단위로 진행되게 되는데,

한 버전에서 많은(3회 이상) 푸시를 하고나니

업데이트를 내려 받는 속도가 굉장히 느려졌다.

 

또한, ms appcenter의 서버가

해외에 있다보니 업데이트 체크 및 진행 속도가

들쭉날쭉 해졌다.

 

결국 업데이트에 3-5분이상

소요되어버리는 지경에 이르렀다.

 


 

해결책. 버전관리 전략 수립 및 배포전략 수립

간혹 보면 상용 서비스에서

앱 업데이트를 강제하는 경우가있다.

거기에는 다 이유가 있었다.

 

더 질 좋은 서비스를 잘 만들어 놨는데

사용자가 쓰지를 않는다면...

혹은, 문제점을 개선해서 배포를 했는데

사용자가 구버전에서 계속 에러리포트를 한다면...

 

우리 개발자들은 현타가 올것이다.

 

따라서, 우리는 버전관리와 배포전략을 통해

위와 같은 문제들을 해결하기로 했다.

 

앱 버전은 a.b.c로 구성된다.

a: 주요 업데이트 (스토어에서 배포)

b: 서브기능 추가 (스토어에서 배포)

c: 핫픽스, 워딩 수정 (code push후 버전 올려서 스토어에서 배포)

 

가장 작은 단위의 수정인 c부터 보면

간혹 미흡한 예외처리나 오타를 위해

수정 배포해야되는 경우가 있다.

이때는 코드푸시를 통해 버전의 문제를 즉시 처리하고, 

앱 버전 c 를 하나 올려 빌드해 스토어에 배포한다.

이렇게 하면 기존 버전 사용자도 문제 수정이 진행되고

새 버전 다운로드 사용자도 문제 수정 버전을 설치하게된다.

 

 

앱의 큰 축의 변화는 아니지만

list만 되던 부분이 추가/수정도 가능하게 된다는지 등의 기능 업데이트가 발생하면

b를 하나 올려 빌드해 스토어에 배포한다.

 

업데이트 심사의 경우 앱스토어/플레이스토어 모두

1 영업일 (길게는 2영업일) 이내면 처리되기 때문에

앱 버전 관리를 배포를 통해 하더라도

크게 답답할 일이 없다.

 

다만, 위해서 언급한것처럼 사용자들은

앱 업데이트를 잘 진행하지 않는다.

그렇기에 우리는 강제 업데이트 (필수 업데이트)

기능을 추가해둘 필요가 있다.

 

a단위로 강제 업데이트를 할지

b단위로 강제 업데이트를 할지는

회사 내규 운영방책을 정해서 진행하면 될듯하다.

 

우리는 프리미엄 서비스 + 소수의 사용자를

타겟으로 하고있어, b단위로 강제업데이트를 진행한다.

 

강제 업데이트를 하는 방식

1) 서버에 현재 배포된 각 플랫폼의 최신 버전을 응답하는 api추가

2) 앱이 켜질때 앱 버전과 서버에서 응답한 버전을 비교

3) 앱 버전이 서버에서 응답 받은 최신버전 보다 구 버전이라면 강제 업데이트 진행

4) 강제 업데이트는 Alert + Redirect(store로 linking) 을 통해 진행했다.

5) 주의 사항은 지속적으로 서버 버전을 잘 관리해야된다는 것

+ 새 버전으로 서버 버전을 올릴때 새 버전이 스토어에 반영됐는지 꼭 확인할것
(앱 심사가 통과되더라도 새 버전이 스토어에 반영되기까지 어느정도 시간이 소요되기 때문)

728x90
반응형

기존의 파일 업로드 방식으로는

모바일 사파리 브라우저에서

한글 파일 깨짐 이슈가 발생햇다.

 

// 기존의 간단한 파일 업로드
const formConfig = (form: FormData) => ({ 
  headers: { 'Content-Type': 'multipart/form-data;' },
});

const pdfForm = new FormData();
pdfForm.append('file', state.teaser);
await axios.post('/common/pdf/upload', pdfForm, formConfig(teaserForm));

 

항상 한글깨짐 이슈는 UTF-8 인코딩과

연관되어 있던던 과거의 경험에 의해

issue 트랙킹을 한 결과

 

아래와 같이 약간의 옵션을 추가해줌으로써

한글 깨짐에 대처할 수 있었다.

 

// 기존의 간단한 파일 업로드
const formConfig = (form: FormData) => ({ 
  headers: { 'Content-Type': 'multipart/form-data; charset: UTF-8;' },
  transformRequset: [function() {
    return form;
  }]
});

const pdfForm = new FormData();
pdfForm.append('file', state.teaser);
await axios.post('/common/pdf/upload', pdfForm, formConfig(teaserForm));
728x90
반응형

react-native-audio

라이브러리를 적용중에

ios에서는 문제없지만

android에서 위와 같은 이슈가 발생하였다.

 

문제는 github에서도 논의 중

별다른 해결책 없이 끝난것같다

(github issue tracking: 링크)

 

에러 추적결과

위 문제는 필요한 권한이 부족하여 나타난 현상이었다.

 

위 문제 해결을 위해서는

react-native-permissions를 통해

라이브러리 사용전 필요한 권한 요청을

사용자로 부터 허락받아야한다.

 

const prepare = (path: string) => {
  const config = {
    SampleRate: 22050,
    AudioEncoding: 'aac',
    Channels: 1,
    AudioQuality: 'Low',
    AudioEncodingBitRate: 32000
  };
  
  return new Promise((resolve, reject) => {
    if (Platform.OS === 'android') {
      requestMultiple([
        PERMISSION.ANDROID.WRITE_EXTERNAL_STORAGE,
        PERMISSION.ANDROID.READ_EXTERNAL_STORAGE,
        PERMISSION.ANDROID.RECORD_AUDIO
      ]).then((isAuthorized) => {
        if (
          isAuthorized['android.permission.READ_EXTENRAL_STORAGE'] === 'denied'
          || isAuthorized['android.permission.WRITE_EXTENRAL_STORAGE'] === 'denied' 
          || isAuthorized['android.permission.RECORD_AUDIO'] === 'denied'
        ) {
          reject('no permission')
        } else {
          AudioRecorder.prepareRecordingPath(path, config);
          resolve('has permission')
        }
      })
    } else {
      AudioRecorder.prepareRecordingPath(path, config);
      resolve('has permission')
    }
  })
}

 

이런식으로

외부 저장소 읽기/쓰기, 오디오 레코딩 권한을 획득해야만

"Exception in native call from JS" 에러없이 진행할 수 있다.

728x90
반응형

위 에러를 겪었다.

 

https://github.com/software-mansion-labs/reanimated-2-playground/commit/71642dbe7bd96eb41df5b9f59d661ab15f6fc3f8

 

Android · software-mansion-labs/reanimated-2-playground@71642db

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

github.com

 

위 링크에 변경사항을 따라서

라이브러리 관련 작업을 해서

많은 사람들이 해결됐다고 하는데

 

 

나는  proguard설정에 따른 이슈였어서

아래의 코드를 추가해줌으로써

이슈를 해결할 수 있었다.

// android/app/proguard-rules.pro

-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
728x90
반응형

RN Firebase messaging 포스팅의 최종판이다.

 

[이전 관련글]

https://honeystorage.tistory.com/255

https://honeystorage.tistory.com/276

https://honeystorage.tistory.com/292

https://honeystorage.tistory.com/301

https://honeystorage.tistory.com/305

 

왜 이렇게나 한가지 기능에 대해

많은 포스팅을 하게 된것일까...

 

첫째,

아마도 너무나도 다양한

환경설정 속에서

내게 맞는 솔루션을 찾지 못했던것

 

둘째,

공식 가이드라인을

꼼꼼히 읽어보지않고

stackoverflow에 너무 크게 의존한것

 

셋째,

내 상황을 정확히 분석하지 못하고

해답만 찾으려고 시도했던것

 

넷째,

클라인지 서버인지

무엇이 잘못된건지모르고

해결하려고 삽질했던것

 

각설하고

앞으로 한국 RN 유저들이 더이상

나와 같은 시행착오를

겪지 않았으면 하는 마음으로

상세히 포스팅을 작성해봅니다.

 

[ 환경 설정 - client ]
* RN: 0.63
* @react-native-firebase/app: ^12.9.0
* @react-native-firebase/messaging: ^12.9.0
* @react-native-community/push-notification-ios: ^1.10.1
* @types/react-native-push-notification: *7.3.3


[환경 설정 - server]
* firebase-admin: ^10.0.0

 

자, 그럼 지금부터

길고  긴 push notification 구현을 위한

여정을 떠나보도록 하겠습니다.

 

1. 개발환경 설정

먼저, 구현 단계 이전의 환경설정 파트는

https://honeystorage.tistory.com/255

위 글에서 매우 상세히 다루었습니다.

 

위 글을 참고하여 코드 구현 이외의

환경설정을 완료하도록 하면 되겠습니다.

 

 

2. 코드 구현 - 서버

서버에서는 간략 하지만 강력한

푸시메시지 발송 방법을 소개하겠습니다.

(아래와 같은 설명은 그 어디에서도 명쾌하게 설명된것을 본적이 없습니다.)

 

서버에서 발송하는 메시지는

2가지로 나뉩니다.

 

Silent / None Silent

 

Silent, 말 그대로

쥐도새도모르게 푸시가 와서

Status Bar에 푸시가 떠있습니다.

 

이 경우엔 화면에 푸시 팝업이 뜨지 않으며,

진동이나 소리도 나지 않습니다.

 

조용히 알려주는 광고 알림등에서는

이를 이용하여 사용자가 기분 상하지 않는선에서

광고를 할수 있겠죠.

 

None Silent는 Silent와 반대입니다.

진동이나 소리가 나며 푸시가 도착합니다.

핸드폰 화면이 켜져있는 상태에서는

푸시 팝업이 나타나며 푸시가 왔음을 명확히 알려줍니다.

 

(저는 None Silent 기능이 필요했는데

대부분의 자료에서는 Silent관련 내용만 나와있더군요 ...)

 

어떻게 해야 알맞는 방법으로

메시지를 보낼 수 있는지

코드로 확인해보겠습니다.

// Silent Push Notification

import fbAdmin from 'firebase-admin';

type PushMessage = {
  token: string;
  title: string;
  body: string;
}

const sendPushNotification = ({ token, title, body }: PushMessage) => {
  const message = {
    token,
    notification: {
      title,
      body,
    },
    data: {
      title,
      body,
    },
    android: {
      priority: 'high',
      notification: {
        title,
        body,
        sound: 'default'
      }
    },
    apns: {
      headers: {
        'apns-priority': 5,
        'apns-push-type': 'background'
      },
      payload: {
        aps: {
          sound: 'default',
          'content-available': 1
        }
      }
    }
  };


  fbAdmin
    .messaging()
    .send(message)
    .catch(error => {
      console.log('push error: ', error);
      // delete token or do something
    })
}
// None Silent Push Notification

import fbAdmin from 'firebase-admin';

type PushMessage = {
  token: string;
  title: string;
  body: string;
}

const sendPushNotification = ({ token, title, body }: PushMessage) => {
  const message = {
    token,
    notification: {
      title,
      body,
    },
    android: {
      priority: 'high',
      notification: {
        sound: 'default'
      }
    },
    apns: {
      headers: {},
      payload: {
        aps: {
          sound: 'default',
          'content-available': 1
        }
      }
    }
  };


  fbAdmin
    .messaging()
    .send(message)
    .catch(error => {
      console.log('push error: ', error);
      // delete token or do something
    })
}

Silent Message가 설정해줄것들이

조금 더 많은것을 볼 수 있습니다.

 

Docs나 Stackoverflow에 많은 정보들이 있지만

이대로만 하면 위 두가지 사항중

원하는 목적을 달성할 수 있습니다.

 

3. 코드 구현 - 클라이언트

정확하게 알맞는 디바이스에

푸시를 발송해주기 위해서는

token을 사용자 디바이스별로

잘 관리해주어야합니다.

 

token일 불일치 할 경우,

아무리 올바르게 fcm 메시징 요청을 하더라도

디바이스에서 수신을 못합니다.

 

token을 관리하는 코드를

작성해보도록 하겠습니다.

import messaging from '@react-native-firebase/messaging'
import { Platform } from 'react-native';
import axios from 'axios';


const [_permission, setPermission] = useState(0);

useEffect(() => {
  _requestPermissionHandler();
}, []);

useEffect(() => {
  requestNotiPermission();
}, [_permission])

const _requestPermissionHandler = () => {
  const perm = await messaging().hasPermission();
  setPermission(perm);
}

const requestNotiPermission = async () => {
  // 사용자 푸시 권한 수락/거절상태 갱신
  await axios.put('url...', { uId: userId, permission: _permission });
  
  // 사용자 푸시 권한이 수락 상태면 토큰 얻어서 갱신
  if (_permission >= 1) {
    const token = await messaging().getToken();
    await axios.put('url...', { uId: userId, token });
    Platform.OS && messaging().setAutoInitEnabled(true);
  }
}

 

이런식으로

사용자의 푸시 권한 상태에 대한 확인 및 갱신과

푸시 토큰의 갱신을 위한 코드를 작성하면 되겠습니다.

 

얼마나 자주, 언제 토큰이나 권한을 갱신해줄지는

앱의 특성이나 상황에 따라 설정해주면 될것입니다.

 

 

4. background 알림 수신 처리 (optional)

마지막 파트는 선택사항입니다.

백그라운드 알림을 수신해

badge를 갱신한다던지 무언가 처리를 원할때

처리하시면 되겠습니다.

(* badge: 앱 아이콘 위에 나타나는 숫자)

 

바로, 말도 많고 탈도 많은

setBackgroundMessageHandler 기능을 활용한

background 알림 처리를 다뤄보겠습니다.

 

최근까지도 계속해서

되니 안되니 이슈가 많은것으로 보이는 기능입니다.

 

계속 연구를 해보니

아래와 같은 경우의 수를 체크해서

알람이 정상인지 확인할수가 있습니다.

  aos - 알림 aos - 백그라운드 ios - 알림 ios - 백그라운드
App - foreground        
App - background        
App - quit State        

 

앱의 각 상태별로, 그리고 OS별로

알림은 오는지, 백그라운드에서 수신은 되는지를 확인하는 것입니다.

 

모두 Ok라면 푸시는 완벽히 설정됐다고 볼 수 있습니다.

 

// index.js

import { Platform, Vibration, AppRegistry } from 'react-native';

messaging().setBackgroundMessageHandler(async remoteMessage => {
    onMessageReceived(remoteMessage);
});

function HeadlessCheck({ isHeadless }) {
  if (isHeadless) {
    <AppFake />;
  }
  
  return (
    <App />
  );
}

const AppFake = () => {
    return null;
};

const onMessageReceived = message => {
  console.log('background message: ', message);
  
  // 저는 ios에서 진동이 정확히 울리지 않는것으로 보여
  // background에서 알림이 수신될 경우 진동이 울리는 코드를 추가하였습니다.
  Platform.OS === 'ios' && Vibration.vibrate([400]);
}

AppRegistry.registerComponent(appName, () => HeadlessCheck);

 

 

 

[ 참고 ]

https://sweetdev.tistory.com/476

https://stackoverflow.com/questions/15834581/ios-push-notification-custom-format

https://firebase.google.com/docs/cloud-messaging/concept-options

https://firebase.google.com/docs/cloud-messaging/send-message#example-notification-message-with-platform-specific-delivery-options

https://mrgamza.tistory.com/837

https://hryang.tistory.com/34

728x90
반응형

[ 푸시알림 완벽구현 - 최종판 ]

https://honeystorage.tistory.com/306

 

 

위 문제는 많은 개발자들이 겪고있는 이슈이다.

많은 해결법이 나와있지만 명쾌하지 못하다.

만든 개발자조차 얻어걸린 해결법을 해결법이랍시고

제공하는듯한 느낌을 지울 수 없다.

 

왜 푸시 진동이 안울려? 글에서도

언급한 서버 푸시 코드 수정을 통한 방법도 있지만,

이 또한 만능은 아니었다.

 

보다 완벽한 푸시 처리를 위해

local-notification으로 알림으 띄우는 코드를

setBackgroundMessageHandler의 리스너로 등록한뒤

빌드해서 테스트를 해보았다.

 

왜냐면 시뮬레이터에서는

애초에 푸시알림이 지원되지않아

부정확한 테스트밖에 할수없다고 판단되었기 때문이다.

 

 

(공식 가이드의 지시에 따라 isHeadless의 경우 null 대신 임의의 컴포넌트를 리턴함)

// app.js 메시지 수신 설정
function HeadlessCheck({isHeadless}) {
  const queryClient = new QueryClient();

  if (isHeadless) {
    // App has been launched in the background by iOS, ignore
    return <SafeAreaView style={{flex: 1, backgroundColor: '#ffffff'}}></SafeAreaView>;
  }

  return (
    <QueryClientProvider client={queryClient}>
      <Provider store={store}>
        <App />
      </Provider>
    </QueryClientProvider>
  );
}

messaging().setBackgroundMessageHandler(onMessageReceived);
function onMessageReceived(message) {
  localNoti.fire({
    title: '알림 수신',
    message: '백그라운드 알림 수신 성공!!'
  })
}

AppRegistry.registerComponent(appName, () => HeadlessCheck);

# localNoti는 @react-native-community/push-noitification-ios 와

react-native-push-notification으로 만든 클래스 인스턴스이다.

 

 

테스트 결과

setBackgroundMessageHandler는 여전히 멍청이였다.

 

원인은 생각보다 더 멍청한 나에게 있었다.

프로젝트내에

setBackgroundMessageHandler 리스너가 2개가 있었고

다른 한곳에 등록된 setBackgroundMessageHandler가

동작중이었던 것이다.

 

따라서, 위 설정에 따라 셋업했다면

설정상에 문제는 없는거로!

 

 

[ 푸시알림 완벽구현 - 최종판 ]

https://honeystorage.tistory.com/306

 

 

[ 참고 ]

https://github.com/invertase/react-native-firebase/issues/5656

 

messaging.setBackgroundMessageHandler: How to invoke when app is in quit state on iOS · Issue #5656 · invertase/react-native-f

Documentation Feedback This is a solution for invoking messaging().setBackgroundMessageHandler() when the app is in quit state on iOS. In the past couple of days, I have gone through a ton of issue...

github.com

 

https://rnfirebase.io/faqs-and-tips#on-ios-when-the-app-is-in-quit-state-the-setbackgroundmessagehandler-is-never-invoked-even-when-i-receive-the-notification-how-can-i-fix-this

 

FAQs and Tips | React Native Firebase

Copyright © 2017-2020 Invertase Limited. Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License. Some partial documentation, under the

rnfirebase.io

 

https://notifee.app/react-native/docs/installation

 

Installation | Notifee

Quick start guide for installing and running on React Native. The Notifee core library is packaged an Android "AAR" file. It is distributed in a "local maven repository" for Android integration. You must add this repository to your android/build.gradle bui

notifee.app

 

 

 

728x90
반응형

rootview의 백그라운드 색상을 설정하는 법을 알아보기전에

 

왜 이것을 설정하게 되었는지

문제의 원인, 근원을 짚어본다.

 

react-native로 개발된 앱을 실행했을때

iOS 에서만 white flash 현상이 나타났다.

 

https://stackoverflow.com/questions/5618163/displaying-splash-screen-for-longer-than-default-seconds

 

Displaying splash screen for longer than default seconds

Is it possible to display the Default.png for a specified number of seconds? I have a client that wants the splash screen displayed for longer than its current time. They would like it displayed f...

stackoverflow.com

 

이런 내용도 있었지만

살펴보니 내게는 모두 적용되지 않는일들이었다.

 

기본적으로 black 컬러를 가져가는 우리 앱의 특성상

배경색만 검정으로 설정하면 되는데,

위 글에서는

splash-screen의 설정에 대해 다루는 내용이 대부분이 었기 때문.

 

 

그러던중

https://medium.com/modus-create-front-end-development/changing-the-react-native-rootview-background-color-for-ios-and-android-7da9acc4e502

 

Changing the React Native RootView Background Color (for iOS and Android)

While developing my React Native KeyGen Music Player app for iOS, I decided to create a custom 90’s retro theme. This required me to change…

medium.com

이런 글을 찾게되었다.

 

rootview의 색상을 지정하는것!

// AppDelegate.m -> didFinishLaunchingWithOptions

if (@available(iOS 13.0, *)) {
  rootView.backgroundColor = [UIColor blackColor];
} else {
  rootView.backgroundColor = [UIColor blackColor];
}

systemcolor, whitecolor 등으로

설정되어있던 값을 모두

blackColor로 변경해주었다.

728x90
반응형

1. LifeCycle

class 기반 react를 오랫동안 써왔다.

그동안은 LifeCycle이 굉장히 중요시 여겨지며

LifeCycle을 기반으로 적재 적소에 함수들을 호출해 개발했다.

 

Hooks로 기반을 변경한 뒤,

useEffect에 의존한 단조로운 LifeCycle위에서 개발하게 되었다.

 

그런데, 이 useEffect에서 호출되는 함수들은

어느 시점에 호출되는걸까?

 

useEffect : render -> useEffect -> (re-rendering)

 

DOM의 레이아웃 배치와 페인팅이 끝난 후,

useEffect의 사이트 이펙트에 해당하는 함수들이 호출된다.

 

여기서 아래와 같은 문제가 발생할 수 있다.

1) 사용자가 짧게나마 빈 페이지를 보게됨

2) 기본 값으로 구성된 페이지를 보게됨

 

대부분의 웹 페이지들이

페이지가 열릴때

데이터를 비동기적으로 불러와

state의 갱신을 동해 화면을 re-rendering해서

사용자에게 제공된다는 것을 생각하면

useEffect를 이런식으로 활용하는것에는

어느정도 문제가 있다고 볼수있겠다.

 

그러면 어떻게 하면 좋을까

하고 알아보니

useLayoutEffect라는 것이 있었다.

 

useLayoutEffect : useLayoutEffect -> render -> (useEffect) -> (re-rendering)

 

useEffect와 달리

DOM이 레이아웃 배치 및 페인팅을 하기 전에

즉, render 보다도 먼저 호출된다.

 

useLayoutEffect를 통해

완벽하게 위의 1), 2) 문제를 제어할수는 없어도

좀더 효과적으로 서비스를 제공할수는 있겠다.

 

(위 문제를 완벽하게 해결하려면 SSR방식의 next를 써야할듯하다.)

 

 

 

2. useState

useState는 단연코 가장 많이 쓰이는 hooks일것이다.

프로젝트를 진행하다보니

코드를 올바르게 작성한듯한데

state가 제때 갱신되지 않아서 의도와 다르게

결과값이 나오는 경우가 심심치않게 보였다.

 

이는 동일블록 내에서 setter를 사용할 때

Closer 구조로 인해 발생하는 문제로,

class기반의 react를 사용할때도 가지고 있던 문제다.

 

class기반에서도 

setState(prevState => ({ ...prevState, type: value }));

위와 같이 처리하곤 했었는데

hooks에서도 마찬가지로 위와 같은 방식으로

해당 문제를 해결할 수 있다.

 

예를 들어,

const [list, setList] = useState([]);


const updateList = (data) => {
  // setList(list.concat(data)); <-- X
  
  setList(prevList => {
    const newList = list.concat(data);
    return newList;
  });
}

이런식으로 처리하면 된다.

728x90
반응형

앱 개발이 완료되어가는 시점이다

 

눈이 안좋은 사람들이

시스템 설정상의 폰트 크기를 조정해서

앱의 텍스트나 디자인이 깨지는것을

방지하기위한 체크를 하는 도중

 

(안드로이드에서만)

앱을 백그라운드로 실행해둔 상태에서

환경 설정에서 폰트 크기를 변경한 뒤

다시 앱으로 돌아오니

앱이 튕겨버리는 현상이 나타났다.

 

추적결과

react-native-screens에서

문제가 있는것으로 발견되었다.

 

 rn-screens docs에도 나와 있듯이

// MainAcitivity.java

import android.os.Bundle;

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(null); <-- 여기
}

 

이 부분을 수정해줌으로써

튕겨버리는 이슈가 해결되었다.

 

그런데 여기서 파생된 이슈가 있었다.

우리는 기본적으로 폰트에

dp 단위  + font scale 무시를 적용하는데,

 

위의 null 처리로 인해

백그라운드의 앱이 열렸을때

무조건 그 당시의  font-scale을

1로 간주해버리는 것이 아닌가...

 

바꿔 말하자면,

사용자가 시스템 설정에서

폰트 크기를

1 -> 2로 변경한뒤 앱을 열면

앱이 2라는 것을 1이라는 기준으로 잡아버린다.

 

따라서, font scale을 무시하는

우리의 코드는 오작동을 했다.

(우리의 폰트 크기 = dp / font scale)

 

따라서, storage에 항상

이전 폰트 크기를 저장해뒀다가

폰트 크기가 변경된 경우

앱을 재실행 하는 코드를 추가하였다.

(재실행하면 font-scale 기준을 제대로 잡게됨)

 

const detectChangeFontScaleOnAndroid = async () => {
  const fontScale = PixelRatio.getFontScale();
  const _fontScale = await storage.getItem('fontScale');
  storage.setItem('fontScale', fontScale?.toString());
        
  if (_fontScale && _fontScale !== fontScale.toString()) {
    console.log('changed font scale');
    Platform.OS === 'android' && CodePush.restartApp();
  }
}

 

728x90
반응형

[ 푸시알림 완벽구현 - 최종판 ]

https://honeystorage.tistory.com/306

 

notification 기능도 구현하기 나름인것같다.

firebase의 remote notification만 있으면

만능일줄 알았건만

 

local notification을 쓸일이 생겨

지웠던 라이브러리를 다시 설치하고

셋팅하는 상황이 발생했다.

 

워낙 간단했지만

그래도 필요한 이들을 위해서 공유한다.

(혹은, 미래의 나를 위해...)

 

먼저, remote notification이

구현되어 있음에도 local notification이 필요했던 이유는

앱 안에서 채널톡 SDK를 통해

채널톡이 구현되어있는데...

 

운영자가 답장을 해줘도 사용자가 답장이 왔는지

알길이 없는 상황에 봉착했다.

(알림을 주던지, 카톡을 주던, 문자를 보내주던

무언가는 있어야지

사용자가 답변이 왔음을 알것이다.)

 

그래서 나는

setBackgroundMessagehandler에서

ChannelIO.isChannelPushNotification 메소드로

채널톡 메시지가 수신되었음이 감지됐을때

local에서 notification을 발생시켜

사용자에게 알려주는 전략을 취하기로했다.

 

// https://developers.channel.io/docs/mobile-channel-io#ischannelpushnotification

import messaging from '@react-native-firebase/messaging';
import {ChannelIO} from 'react-native-channel-plugin';

componentDidMount() {
    this.backgroundMessageHandler = messaging().setBackgroundMessageHandler(
      async (remoteMessage) => {
        ChannelIO.isChannelPushNotification(remoteMessage.data).then((result) => {
            if (result) {
              ChannelIO.receivePushNotification(remoteMessage.data).then((_) => { });
            } else {
              // TODO : Your FCM code
            }
          },
        );
      },
    );
}

componentWillUnmount() {
  this.backgroundMessageHandler();
}

 

채널톡에서도 그럴때 쓰라고 만들어 둔것인지

이렇게 예제를 제공하기도 한다.

 

result === true일때 

local notification이 작동되도록 코드를 설정했으며

아래와 같이

local notification을 세팅할수있다.

 

1. 설치 ( >= 0.60)

// react-native-push-notification 설치
npm install --save react-native-push-notification

// @react-native-community/push-notification-ios 설치
npm i @react-native-community/push-notification-ios --save

// pod file download
cd ios && pod install && cd..
// --- android ---

// android/app/src/main/AndroidManifest.xml
.....
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

    <application ....>
        <!-- local notification code -->
        <meta-data  android:name="com.dieam.reactnativepushnotification.notification_foreground" android:value="false"/>
        <meta-data  android:name="com.dieam.reactnativepushnotification.notification_color" android:resource="@color/white"/> <!-- or @android:color/{name} to use a standard color -->

        <!-- common notification code -->
        <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
        <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
        <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="android.intent.action.QUICKBOOT_POWERON" />
                <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
            </intent-filter>
        </receiver>

        <!-- common notification code -->
        <service
            android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
            android:exported="false" >
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
     .....
// ios

// AppDelegate.h
// --- local notification code ---
#import <UserNotifications/UNUserNotificationCenter.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate, UNUserNotificationCenterDelegate>
UNUserNotificationCenterDelegate <-- 이거 추가됨




// AppDelegate.m
// --- local notification code ---
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>


// --- local notification code ---
// Required for the register event.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
 [RNCPushNotificationIOS didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
// Required for the notification event. You must call the completion handler after handling the remote notification.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
  [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}
// Required for the registrationError event.
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
 [RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error];
}
// Required for localNotification event
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void (^)(void))completionHandler
{
  [RNCPushNotificationIOS didReceiveNotificationResponse:response];
}


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  ...
  // Define UNUserNotificationCenter
  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
  center.delegate = self;

  return YES;
}


//Called when a notification is delivered to a foreground app.
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
  completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}

 

 

2. 코드 작성

import PushNotificationIOS from '@react-native-community/push-notification-ios';
import PushNotification from 'react-native-push-notification';

type NotiOptoin = {
  title: string;
  message: string;
}

export class LocalNotification {
  private channel: any;

  constructor() {
    this.init();
  }

  private init() {
  // Must be outside of any component LifeCycle (such as `componentDidMount`).
    PushNotification.configure({
      // (optional) Called when Token is generated (iOS and Android)
      onRegister: function (token) {
        // console.log('TOKEN:', token);
      },

      // (required) Called when a remote is received or opened, or local notification is opened
      onNotification: function (notification) {
        // console.log('NOTIFICATION:', notification);

        // process the notification

        // (required) Called when a remote is received or opened, or local notification is opened
        notification.finish(PushNotificationIOS.FetchResult.NoData);
      },

      // (optional) Called when Registered Action is pressed and invokeApp is false, if true onNotification will be called (Android)
      onAction: function (notification) {
        // console.log('ACTION:', notification.action);
        // console.log('NOTIFICATION:', notification);

        // process the action
      },

      // (optional) Called when the user fails to register for remote notifications. Typically occurs when APNS is having issues, or the device is a simulator. (iOS)
      onRegistrationError: function (err) {
        console.error(err.message, err);
     },

      // IOS ONLY (optional): default: all - Permissions to register.
      permissions: {
        alert: true,
        badge: true,
        sound: true,
      },

      // Should the initial notification be popped automatically
      // default: true
      popInitialNotification: false,

      /**
      * (optional) default: true
      * - Specified if permissions (ios) and token (android and ios) will requested or not,
      * - if not, you must call PushNotificationsHandler.requestPermissions() later
      * - if you are not using remote notification or do not have Firebase installed, use this:
      *     requestPermissions: Platform.OS === 'ios'
      */
      requestPermissions: false,
    });

    PushNotification.createChannel({
      channelId: 'com.myApp',
      channelName: '앱이름',
    }, (created) => {
      console.log('noti channeld is created')
    })
  }

  fire = (option: NotiOptoin) => {
    PushNotification.localNotification({
      title: option.title,
      message: option.message,
      largeIcon: "ic_launcher",
      smallIcon: "ic_launcher",
      bigLargeIcon: "ic_launcher",

      /* Android Only Properties */
      channelId: "com.myApp", // (required) channelId, if the channel doesn't exist, notification will not trigger.
      vibrate: true,
      vibration: 300, // vibration length in milliseconds, ignored if vibrate=false, default: 1000
      priority: 'high',
      
      /* iOS and Android properties */
      id: 0, // (optional) Valid unique 32 bit integer specified as string. default: Autogenerated Unique ID
      playSound: true, // (optional) default: true
      soundName: "default", // (optional) Sound to play when the notification is shown. Value of 'default' plays the default sound. It can be set to a custom sound such as 'android.resource://com.xyz/raw/my_sound'. It will look for the 'my_sound' audio file in 'res/raw' directory and play it. default: 'default' (default sound is played)
    });
  }
}

export {PushNotification};

* 주의 : 채널을 생성해주지 않으면 알림이 뜨지 않습니다...

 

 

3. 코드 연동

const localNoti = new LocalNotification();
messaging().setBackgroundMessageHandler(async remoteMessage => {
  Platform.OS === 'ios' && Vibration.vibrate([400]);
  console.log('Message handled in the background!', remoteMessage);
  
  ChannelIO.isChannelPushNotification(remoteMessage.data).then(result => {
    if (result) {
      localNoti.fire({ title: '제목', message: '메시지' });
    } else {
      // TODO : Your FCM code
    }
  });
});

참고로,

Platform.OS === 'ios' && Vibration.vibrate([400]);

이 부분은 이 전편에서 다루었던 내용으로

ios에서 백그라운드 알림에 대한

진동이 울리지않아서

트릭용으로 추가한 코드입니다.

 

 

[ 푸시알림 완벽구현 - 최종판 ]

https://honeystorage.tistory.com/306

728x90
반응형