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
반응형
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
반응형
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
반응형
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
반응형
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
반응형
728x90

ios에서 App이 종료된 상태에서

push notification이 왔을대

메시지를 클릭해 앱을 여는 과정에서

앱이 splash screen 상태에서 멈춰버렸다가

결국 뻗어버리는 이슈를 경험했다.

 

삽질에 삽질을 더한결과

notification 문제는 아니고

react-native-splash-screen의 이슈였다.

 

https://github.com/crazycodeboy/react-native-splash-screen/issues/397

 

위 링크의 가이드가

99% 같은 내용이라

내게도 해결책이 될것이라고 판단했다.

 

(그외 참고 자료들)

https://github.com/crazycodeboy/react-native-splash-screen/issues/488

https://stackoverflow.com/questions/66598301/react-native-firebase-push-notification-crash-report-problem

https://ios-development.tistory.com/384

 

 

그러나 또

적용 과정에서 문제가 발생했으니...

바로 앱을 실행하면 바로 뻗어버리는 현상이었다.

 

왜 그럴까?

이슈를 찾다가 자꾸 시간이 지연되어

 

그냥 라이브러리없이

splashscreen을 구현하는 방향으로

돛을 돌렸다.

 

 

728x90
반응형
728x90

xcode에 디바이스 연결 후

테스트를 하려다보니

빌드는 성공적으로 완료되었는데

 

failed to get the task for process

라는 에러가 등장했다.

 

[ 솔루션 ]

Build Settings -> code signing identity -> debug -> "ios Develpoer"

 

"ios Developer" 부분이 핵심이다

저 부분이 "ios Distribution"으로 되어있을때 에러가 발생했었다.

 

728x90
반응형
728x90

*** nginx는 이미 설치되어있다는 가정

 

 

php 설치

// php install
yum -y install http://rpms.remirepo.net/enterprise/remi-release-7.rpm 
yum -y install epel-release yum-utils
yum-config-manager --enable remi-php74
yum install php php-mysql php-fpm

// set address
nano /etc/php-fpm.d/www.conf

listen=localhost:9000 -> listen=/var/run/php-fpm/php-fpm.sock

// start
systemctl start php-fpm
systemctl enable php-fpm.service

// nginx setting
server {
    server_name my_domain;
    client_max_body_size 5G;

    #charset koi8-r;
    access_log  /var/log/nginx/my_service.access.log;

    root /usr/share/nginx/html/my_service;

    location / {
        index index.html index.htm index.php;
    }

    location = /50x.html {
        root /usr/share/nginx/html;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html/my_service$fastcgi_script_name;
        include fastcgi_params;
    }
}

systemctl restart nginx

ERR 403 Page

// 403 Page error
2021/12/20 23:38:06 [error] 2157#2157: *29 "/usr/share/nginx/html/my_service/index.php" is forbidden (13: Permission denied), client: 218.235.68.160, server: my_domain, request: "GET / HTTP/1.1", host: "my_domain"

// dir 및 하위 파일 권한 체크
ls -lZd /service_root/my_service

// dir 및 하위 파일 권한 변경
chcon -R -t httpd_sys_content_t /service_root/my_service

 

ERR mysql :: Connect Error: Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)

// mysql install
yum -y install http://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
yum -y install mysql-community-server 
systemctl start mysqld
systemctl enable mysqld

// find your  temporary password (ex: XcheTa.X-5o1)
/var/log/mysqld.log

// change db password
ALTER USER 'root'@'localhost' IDENTIFIED BY 'XcheTa.X-5o1';
FLUSH PRIVILEGES;
use mysql;
UPDATE user set authentication_string=password('newpassword') where user='root';
-> policy error?
  SET GLOBAL validate_password_policy=LOW;
  UPDATE user set authentication_string=password('newpassword') where user='root';
FLUSH PRIVILEGES;

// mysql exit
quit;

 

필요시 mysql 계정 생성
db생성 하여 쓰면됩니다.

 

ERR -> /install/ajax.install.check.php

// 버전 이슈일 확률이 높음 -> php 7.4 재설치 진행

 

ERR unix:/var/run/php-fpm.sock failed (13: permission)

// /etc/php-fpm.d/www.conf <-- 약간씩 다를 수 있음
user = nginx
group = nginx

listen.owner = nginx
listen.group = nginx
listen.mode = 0660

// 실행
systemctl restart nginx
systemctl restart php-fpm

 

ERR 로그인 에러 / data 접근 권한 에러

// SElinux 에러
// SElinux는 특정 서비스에 대한 권한을 필요한 만큼 허용하고 이외에는 모두 차단하는 정책을 가지고있어, 서비스의 취약성을 이용한 공격이 발생해도 관계된 프로세스나 파일 시스템에 쉽게 접근하지 못하도록 사전에 차단하는 역할을 함.
// 따라서, 완정 중지를 하게되면 보안상 문제가 발생할 수 있음
// 아래와 같이 임시 비활성화 처리가 가능하다.
// Permissive -> 0, Enforcing -> 1
setenforce 0

 

ERR php mb_string

// 관련 내용: https://zetawiki.com/wiki/CentOS_php-mbstring_%EC%84%A4%EC%B9%98


// 확인
php -r "mb_substr();"
-> PHP Fatal error:  Call to undefined function mb_substr() in Command line code on line 1

rpm -qa php-mbstring
yum info php-mbstring | grep Repo
-> Repo        : base

// 설치
yum install php-mbstring -y

// 설치자료 확인
rpm -qa php-mbstring
-> php-mbstring-5.3.3-3.el6_2.8.x86_64


// 재실행
systemctl restart nginx
systemctl restart php-fpm
728x90
반응형
728x90

"늘 그랬듯이"

라는 생각은 개발자에게 치명적이다.

 

그 현상을 더 이상

해결해야될 문제로 보지않고

안주하게 만들어버린다.

 

나에게 server.js 스크립트는

그런 대상이다.

 

수 많은 MVP 모델을 만들면서

server.js를 어떻게 구조화할지는

크게 고민하지않앗다.

 

express가 너무나도 쉽게 서버를 만들어주기에

필요할때 route만 더 붙여준다던지

cors를 설정해준다던지 하면 끝이었다.

 

// server.js
const express = require('express');
const api_v1 = require('../api/v1');

const app = express();
const http = require('http').createServer(app);

app.use('/api/v1/', api_v1);
http.listen(process.env.PORT, () => {
  console.log('server is listening on', process.env.PORT);
});

 

간단하게는

이정도면 충분히 훌륭한 앱서버가 된다.

 

문제는

서버에서 담당하는 역할이 많아지면서

여러가지가 붙다보면

코드가 지저분해지고

더이상 보기 싫은 스크립트가 되어버린다.

 

// server.js
const express = require('express');
const { Server } = require('socket.io');
const api_v1 = require('../api/v1');
const config = require('./config/server');

const app = express();
const http = require('http').createServer(app);
const io = new Server(http, {});

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser());

app.use(function (req, res, next) {
  const allowedOrigins = config.allowedOrigin;
  const origin: any = req.headers.origin;
  if (allowedOrigins.indexOf(origin) > -1) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  res.header('Access-Control-Allow-Headers', 'Authorization, X-Requested-With, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS,PATCH');
  res.header('Access-Control-Allow-Credentials', 'true');
  req.method === 'OPTIONS' ? res.sendStatus(200) : next();
});

app.use('/api/v1/', api_v1);

io.sockets.on('connection', (socket) => {
  console.log('socket is connected..!');
});

http.listen(process.env.PORT, () => {
  console.log('server is listening on', process.env.PORT);
});

 

이정도만 되어도 보기싫어지기 시작한다.

우리는 class로 스크립트를 새로 짜보도록 하자.

 

// server.js
const express = require('express');
const { Server, createServer } = require('http');
const { Server as ioServer } = require('socket.io');
const api_v1 = require('../api/v1');
const config = require('./config/server');

const app = express();
const http = require('http').createServer(app);
const io = new Server(http, {});

class Server {
  private app: express.Aplication;
  private server: Server;
  private io: ioServer;
  
  constructor() {
    this.createApp();
    this.createServer();
    this.sockets();
    this.configure();
    this.routes();
    this.listen();
  }
  
  private createServer = (): void => {
    this.server = createServer(this.app);
  }
  
  private createApp = (): void => {
    this.app = express();
  }
  
  private sockets = (): void => {
    this.io = new ioServer(this.server);
  }
  
  private routes = (): void => {
    app.use('/api/v1/', api_v1);
  }
  
  private configure = (): void => {
    app.use(express.urlencoded({ extended: true }));
    app.use(express.json());
    app.use(cookieParser());

    app.use(function (req, res, next) {
      const allowedOrigins = config.allowedOrigin;
      const origin: any = req.headers.origin;
      if (allowedOrigins.indexOf(origin) > -1) {
        res.setHeader('Access-Control-Allow-Origin', origin);
      }
      res.header('Access-Control-Allow-Headers', 'Authorization, X-Requested-With, Content-Type, Accept');
      res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS,PATCH');
      res.header('Access-Control-Allow-Credentials', 'true');
      req.method === 'OPTIONS' ? res.sendStatus(200) : next();
    });
  }
  
  private listen = (): void => {
    this.server.listen(process.env.PORT, () => {
      console.log('server is listening on', process.env.PORT);  
    });
    
    io.sockets.on('connection', (socket) => {
      console.log('socket is connected..!');
    });
  }
}

const App = new Server();

 

이렇게 정리해놓고 보니

목적에 맞는 코드끼리 분리되어

유지보수가 확실히 수월해진듯한 느낌이든다.

 

코드를 짤때는 내 코드를

함께 공유할 개발자도 배려하는 자세를 갖도록 해야겠다.

728x90
반응형
728x90

객체지향 공부에 이어

자료구조에 대해 간단히 짚어보았다.

 

공부했던 객체지향 개념을 적용해서

직접 스택을 구현해본 결과

 

interface Stack {
  readonly size: number;
  pop(): string;
  push(value: string): void;
}
  class StackTest1 implements Stack {
    private obj: any = {};
    private _size: number = 0;

    get size(): number {
      return this._size;
    }

    push = (value: string) => {
      this.obj[this.size.toString()] = value;
      this._size++;
    };

    pop = () => {
      this._size--;
      const value = this.obj[this._size.toString()];
      delete this.obj[this._size.toString()];
      return value;
    };
  }

  const stack = new StackTest1();
  stack.push("jk");
  stack.push("zl");
  stack.push("steve");
  while (stack.size !== 0) {
    console.log(stack.pop());
  }
type StackNode = {
    value: string;
    next?: StackNode;
  };

  class StackTest2 implements Stack {
    private _size: number = 0;
    private head?: StackNode;

    get size(): number {
      return this._size;
    }

    push = (value: string) => {
      const node = { value, next: this.head };
      this.head = node;
      this._size++;
    };

    pop = (): string => {
      if (this.head == null) {
        throw new Error("no more data");
      }

      const node = this.head;
      const value = this.head.value;
      this.head = node.next;
      this._size--;

      return value;
    };
  }

  const stack2 = new StackTest2();
  stack2.push("value1");
  stack2.push("value2");
  stack2.push("value3");
  while (stack2.size !== 0) {
    console.log(stack2.pop());
  }

위 두가지 방식으로 해보게됐다.

첫번쨰 방식은

올바른 방식이라기 보다는

이렇게도 할 수 있다 정도로만 봐야될것같다.

 

2번은 단일 연결 list라는 자료구조의 개념을 도입하여

스택을 구현한 예제이다.

728x90
반응형