반응형

패키지를 추가하거나

native코드를 수정하는 등의

큰 변화를 주지않고

 

js, img, css 를 수정하는 배포에대해

빌드를 하지 않고도 배포할수 있게 해주는

code-push기능 적용 일기이다.

 

참고 링크 1 Weiji 웨이지 님의 블로그

참고 링크 2 Gale Lee 님의 블로그

참고링크 3 appcenter 공식 문서

본 작업은 ,  react-native 0.66 버전에서 진행되었습니다.

 

1. 설치

npm i -g appcenter-cli

 

2. 로그인

appcenter login

위 명령어를 입력하면

? Enable Temporary(Y/n)? 라는 질문이 나타난다.

원투데이 개발할게 아니므로 당연히 n

 

그러고나면 terminal에

?Access code from brwoser이라는 질문과함께

연동된 페이지가 나타나며

구글/깃헙/페북 등 로그인 수단을 선택할것이라고 뜬다.

 

나는 github을 선택했고, username을 설정하고나니

code가 떴다.

코드 복붙

그러면 로그인 완료

 

3. 로그인 확인

appcenter profile list

위 명령어로 로그인된 계정의

Username, Display Name, Email 등을 확인할 수 있다.

 

4. 서비스 등록하기 (대소문자 주의)

appcenter apps create -d appname-aos -o Android -p React-Native
appcenter apps create -d appname-ios -o iOS -p React-Native

이름은 자유지만 android/ios 구분해서 짓기

 

5. 등록된 서비스 조회

appcenter apps list

 

6. 홈페이지 접속해보기

https://appcenter.ms/apps

 

7. react-native-code-push 설치하기

npm i --save react-native-code-push

 

8.  배포단계 설정하기

appcenter codepush deployment add -a username/appname-aos Staging
appcenter codepush deployment add -a username/appname-aos Production
appcenter codepush deployment add -a username/appname-ios Staging
appcenter codepush deployment add -a username/appname-ios Production

이렇게 배포단계에 대한 네이밍을 세팅해두면

appcenter codepush deployment list -a username/appname-aos

이런 방식으로 현재 진행중인 상태를 조회할수 있다고 한다.

 

9. 앱의 배포를 위해 key 조회하기

appcenter codepush deployment list -a username/appname-aos -k
appcenter codepush deployment list -a username/appname-ios -k

 

10. ios 설정

- 라이브러리를 설치했으니 pod도 설치해준다.

cd ios && pod install && cd ..

 

- xcode의 AppDelegate.m 수정

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
  #if DEBUG
    return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
  #else
    return [CodePush bundleURL];
  #endif <-- 새로 변경된 파트
  
//#if DEBUG
//  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
//#else
//  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
//#endif <-- 원래 파트
}

 

- app에 AppCenter-Config.plist 추가

NewFile -> Property List -> Save as (AppCenter-Config.plist)

 

- Project > info > Configuration > +

위 이미지처럼 Staging항목을 추가해줍니다.

 

- Info.plist에 Key 추가하기

key: CodePushDeploymentKey / value: ${CODEPUSH_KEY}

를 추가해줍니다.

 

Project > Build Settings > + Add user defined setting을 클릭

 

MULTI_DEPLOYMENT_CONFIG 추가

release $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)

staging $(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)

 

같은 방법으로

CODEPUSH_KEY 추가

appcenter codepush deployment list -a username/appname-ios -k

위 명령어로 조회된 키를 

release, staging에 알맞게 입력

 

 

11. android 설정

- android/setting.gradle

// add code push
include ':app', ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')

 

- key 추가하기

먼저, 아래 명령어로 키를 조회

appcenter codepush deployment list -a username/appname-aos -k

아래 내용 추가

// android/gradle.properties

CODEPUSH_DEPLOYMENT_KEY_DEBUG=
CODEPUSH_DEPLOYMENT_KEY_STAGING=????
CODEPUSH_DEPLOYMENT_KEY_PRODUCTION=????

 

- /android/app/build.gradle

// buildTypes
buildTypes {
  debug {
    ...
    resValue "string", "CodePushDeploymentKey", CODEPUSH_DEPLOYMENT_KEY_DEBUG
  }
  release {
    ...
    resValue "string", "CodePushDeploymentKey", CODEPUSH_DEPLOYMENT_KEY_PRODUCTION
  }
  releaseStaging {
    initWith release
    resValue "string", "CodePushDeploymentKey", CODEPUSH_DEPLOYMENT_KEY_STAGING
    matchingFallbacks = ['release']
  }
}




// enableHermes 관련 코드가 있다면
if (enableHermes) {
  ...
  releaseStagingImplementation files(hermesPath + "hermes-release.aar")
} else {
  ... 
}



// 맨 아래에 추가
apply from: "../../node_modules/react-native/react.gradle" <-- 에러나면 주석처리
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"

- MainApplication.java

import com.microsoft.codepush.react.CodePush; // <- add

new ReactNativeHost(this) {
  ...
  
  //add
  @Override
  protected String getJSBundleFile() {
    return CodePush.getJSBundleFile();
  }
}

 

+++ 안드로이드에서 코드푸시가 되지않아 에러 추적을해본 결과

[CodePush] Exception com.microsoft.codepush.react.CodePushUnknownException: Error in getting binary resources modified time

위와 같은 에러가 발견됨

// android/app/build.gradle

defaultConfig {
  ...
  resValue 'string', "CODE_PUSH_APK_BUILD_TIME", String.format("\"%d\"", System.currentTimeMillis())
}

이 방법으로 해결함

 

설정할게 많다...

 

그치만 우리가 얻는 이득에 비하면 이건 아무것도아니다.

(하, 그래도 이거는 솔직히 할거 너무많다)

 

설정은 끝났다.

업데이트를 수신할건지에 대한 코드만 추가해주면된다.

 

12. 코드 추가하기

(여기서부터는 자유롭게 하면 될것같다)

// 필자는 App.tsx 에서 작업하였다.

import CodePush from 'react-native-code-push';
import { AppState } from 'react-native';

useEffect(() => {
  const staging = AppState.addEventListener('change', (state) => {
    if (state === 'active') codePushSync();
  });
  
  return () => {
    staging.remove();
  }
}, [])

const codePushSync = () => {
  CodePush.sync({
    updateDialog: {
      title: '새로운 업데이트가 존재합니다.',
      optionalUpdateMessage: '지금 업데이트 하시겠습니까?',
      optionalIgnoreButtonLabel: '나중에',
      optionalInstallButtonLabel: '업데이트'
    },
    installMode: CodePush.InstallMode.IMMEDIATE
  })
}



export default CodePush({ checkFreQuency: CodePush.CheckFrequency.MANUAL })(App);

 

빌드파일을 지우고

re-빌드해서 배포테스트를 해보자.

반응형
반응형

나는 그동안 redux를 쓰며 의문을 가졌다.

사실 쓰기 매우 꺼려지기까지 했다.

 

global state 관리를 위해

redux를 쓰는것은 동의

 

다만, 그 많은 코드를 써가면서까지

비동기 요청을 하고

응답된 데이터를 state로 관리하고

해야되는걸까?

 

실제로 서비스를 개발하면서

gloal state로 관리해야될 데이터는

많지 않았던것같다.

 

때에 따라서는

global state로 관리되는 데이터들에대한

최신화 문제를 겪기도했다.

 

이번에 새로운 앱 MVP 제작을위해

자료조사를 하던도중

이 포스팅을 보고

"react-query" 를 도입해보았다.

 

react-query는 global state를

server-state와 client-state라는

두개의 개념으로 분리해서 바라본다.

 

나는 이에 크게 동의했다.

 

예를들어, 포스팅 목록을 불러왔을때는

글로벌로 관리해야한다고본다.

(사용자가 새로고침하기 전까지는)

 

대신, 포스팅 단일 데이터는

굳이 글로벌로 관리할 필요가 있을까?

아니라고 생각한다.

 

global state라는 개념을 도입하게된 배경은

[부모 -> 자식 -> 자식 ->  자식 -> 자식]

으로 props를 전달하는데 불편함이 있어서이며,

불필요한 fetch를 방지하기 위함이다.

 

포스팅 단일 데이터호출 및 렌더링에는

이런 문제가 있지도 않을텐대

그 많은 코드를 써가며 global state로

관리할 이유가 없다고 생각하는것이다.

 

이런 측면에서 접근했을때

경우에 따라 global state로 관리해야 하는 경우에만

Redux를 이용하고

일반적으로는 state 그 자체면 충분하다.

 

특히나, 비동기 요청을 위해

redux-sage, redux-thunk등을 쓸 이유?

는 없어진다고 생각한다.

 

아직까지는 대규모 프로젝트에서

관리의 용이성을 위해 redux + redux-saga를 많이 쓴다곤 하지만

react-query가 어느정도 궤도에 이르면

redux + react-query 구조로 가지는 않을까하고

조심스럽게 예상해본다.

 

이제 만들고있는 앱에

redux도 붙이러 가야겠다

 


 

redux-saga, redux-thunk가

너무나도 싫어서

기피하고 외면하던 게으른 개발자가

 

마음에 쏙드는 신문물을 발견해

작성한 포스팅입니다.

반응형
반응형
SDK location not found. Define location with sdk.dir in the local.properties

위와 같은 에러가 발생한다면

간단한 설정으로 이슈 해결이 가능하다.

 

android/local.properties 파일 추가

sdk.dir=/Users/user/Library/Android/sdk

작성

 

(혹은, 윈도우를 사용하거나 개별 경로를 설정하여 sdk의 경로가 다르다면 맞춰서 잡아주자)

반응형
반응형

https://reactnative.dev/docs/0.63/linking

위의 Docs를 기반으로 작성하였습니다.

 

알림톡, 링크 등을 이용해 앱을 열고자 할때

필요한 기능이 바로

Deep linking입니다.

 

마케팅이나 사용자 편의성 측면에서

굉장히 유용한 기능입니다.

설정도 간편하니 알아보도록 하겠습니다.

 

1. IOS 설정

// IOS 설정, Application.m


// top
#import <React/RCTLinkingManager.h>


// above @end
- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

 

그리고 xcod > targets > app > info > url types

+를 눌러

Identifier, URL Schemes를 고유명칭으로 잡아줍니다.

예를들어, hssteve

 

2. Android 설정

// AndroidMenifest.xml

<intent-filter>
  <action android:name="android.intent.action.MAIN" />
  <category android:name="android.intent.category.LAUNCHER" />
  <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
</intent-filter>
<intent-filter> <-- 이 블럭 추가됨
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="hssteve" />
</intent-filter>

 

3. App.jsx (App.tsx) 설정

Linking.addEventListener('url', schemeListener)

const schemeListener = ({ url }) => {
  console.log('from: ', url);
}

 

테스트를 해볼까요?

// android 테스트
npx uri-scheme open hssteve://some/thing --android

// ios 테스트
npx uri-scheme open hssteve://some/thing --ios

 

console이 잘 찍히나요?

페이지 이동을 위해 쓴다면

수신하는 url 맞게

redirect해주면 될것같습니다.

 

어떻게 쓸지는 자유!

유용한 기능 입니다.

반응형
반응형

react-native에서의 image 다루기는

웹에서 css를 통해 다루는것보다

조금 더 까다로운것같다.

 

이유는 auto라는 사기적인 옵션이

RN에는 존재하지않기 때문인것같다.

 

* 목표: 가로는 화면비율, 세로는 이미지 크기에 맞게 떨어지게 해보자

// Web이었다면?
width: 100%;
height: auto;
object-fit: contain;

 

웹에서는 이렇게나 간단한걸

우리는 약간은 길고 고생스럽게 해야한다.

import React, { useState } from 'react';
import { Image, Dimension } from 'react-native';

function ImageTestModule({ imagePath }) {
  const [height, setHeight] = useState(0);
  const { width } = Dimension.get('window');
  Image.getSize(imagePath, (w, h) => {
    setHeight(h * (width / w));
  });
  
  return (
    <Image
      style={{ width: '100%' }}
      source={{ uri: imagePath, height }}
      resizeMode='cover'
    />
  );
}

 

다소, 복잡하고 길어보이지만

완벽하게 원하던 바를 달성할수있다.

반응형
반응형

제작중인 앱에서 파일을

다운로드/업로드 해야한다.

 

주요 라이브러리중

rn-fetch-blob를 선택하였는데,

써보기도전에 cycle warning이 발생했다.

 

구글링 결과

node_modules > rn-fetch-blob > polyfill > Blob.js, Fetch.js, XmlHttpRequest.js

3개 파일을 아래와같이 일부 수정해줌으로써

경고를 지울수있었다.

 

// import RNFetchBlob from '../index.js'
import { NativeModules } from 'react-native';

...

const RNFetchBlob = NativeModules.RNFetchBlob;
const log = new Log('XMLHttpRequest')

 

다만, 이렇게했을때는

재설치, 업데이트 등을 했을떄 다시 해당 문제가

발생할수 있어보인다.

 

필요하다면 라이브러리를 포크해

손봐서 써야될지도

반응형
반응형

웹 혹은 앱을 개발하다보면

Select (Picker)는 거의 꼭 들어간다

 

RN에서는

https://openbase.com/categories/js/best-react-native-select-libraries

위에서 보면 알수있듯이

@react-native-picker/picker가

절대적으로 많이쓰인다.

 

그런데 현재 기준 최신버전인 2.1.0버전 설치과정에서

오류가 발생하고 해결했기에

정리해둔다.

 

npm 설치는 정상적으로 완료된다.

npm i react-native-picker-select

다음 작업도 문제 없어 보이는듯 하다

cd ios && pod install

 

그런데

react-native run-ios

막상 실행해보면 엄청난 빌드 오류들이 나타난다.

이거다 싶은 힌트도 없다

 

 

과거의 경험들로 비추어 봤을때

이런 경우에는 모듈간, 버전간 충돌인 확률이 높다.

 

 

아래와 같이 처리했다.

(제거) rm -rf node_modules
(제거) cd ios && rm -rf Pods && cd ..
(설치) npm i
(업데이트/설치) cd ios && pod update && cd ..
(실행) react-native run-ios

 

위와 같은 방법으로

나는 깔끔하게 문제를 해결했다.

반응형
반응형

최신 자바스스크립트 개발환경은

3강체제이다

 

React, Vue, Angular

 

그중에서도 최강자 React를

운좋게도 처음시작부터 계속해서 쓰고있다.

 

근데 이게 너무나도 빨리

유행이 변해가다보니

따라가기가 정말 쉽지않다

 

자칫 쉬어가다간 바로 고인물이다.

 

유행을 따라가는게 좋은것만은 아니라고들 하지만

React의 변화는 단순한 유행이 아니다.

 

예를들어, 망치가 처음 만들어졌을떄

지금의 모습이 아니었을것이다

 

힘을 더 잘주기위해 손잡이가 길어졌을테고

너무 길어지니 과유불급이라 판단되어

적정 수준의 길이를 찾았을테고

쇠뭉탱이의 무게나 크기도

오랜 시간을 거쳐 발전했을것이다

 

React도 마찬가지인것같다

 

그중에서도  React의 핵심이라고 할수있는

state (상태)

에 대한 관리는 많은 이슈를 가지고있다

춘추전국시대가 끝이 보이질않는다

 

Context API

Redux

MobX

Context API의 진화

Recoil

...

...

 

각각이 가진 고유한 특성과

장단점들이 있다

 

내가 가진 얄팍한 지식으로 그것들을 설명하기보다는

써본 결과..

 

너무나도 불편하고 지저분하고

이렇게까지 해야되나? 

그냥 예전 바닐라 스크립트 시절이 더 날지도...

라는 생각과

 

사과하나 자르려고

사과 공장을 차리는 느낌이 자꾸만 들었다

 

그런와중에 내가 원하는

바로 그 라이브러리를 찾게되었다.

 

내가 원하는건 별게아니다

1. 글로벌 상태 관리

2. 지저분한 코드 x

3. 간편한 유지보수

 

이 3가지면 충분하다.

뭐 미들웨어로 어쩌니저쩌니

커스텀 훅으로 두들겨패니 뭐니

 

그런거는 기본기만 되어있으면

원하는 사람이 붙여쓰면 되는것 아닌가?

 

이런 나의 니즈를 충족시켜준

라이브러리는

react-query (+ axios)

 

react-query는 서버로 부터 받아온 정보를

(통칭 Server state)

정말 간단하게 글로벌 state로써 관리하고

재호출할수 있으며

로딩/실패/성공에 대한 진행상황(status)을

체크할 수 있다.

 

진짜, 간략한 샘플코드를 확인해본다.

// index.js
import React from 'react';
import App from './App';
import { QueryClientProvider, QueryClient } from 'react-query';
...

function main() {
  const queryClient = new QueryClient();
  
  return (
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  )
}

...
// api.ts
import axios from 'axios';
import { useQuery } from 'react-query';

const getNameAPI = async () => {
  try {
    const { data } = axios.get('...');
    return data;
  } catch(error) {
    throw error;
  }
}

export const getName = () => {
  return useQuery('test/name', getNameAPI);
}
// App.tsx
import React, { useEffect } from 'react';
import { useQueryClient } from 'react-query';
import { getName } from './api';

function App() {
  const queryClient = useQueryClient();
  const { data, status, error, isLoading } = getName();
  console.log('fetching data 확인: ', data, status, error, isLoading);
  
  useEffect(() => {
    // 5초뒤에 이전에 호출했던 API에서 받아온 server state를 확인해본다.
    setTimeout(() => {
      const data = queryClient.getQueryData('test/name');
      console.log('data: ', data);
    }, 5000);
  }, []);
  
  return (
    <div>
      ...
    </div>
  )
}

 

어떤가?

아주 기본적인 방식으로 axios, fetch등을

이용해 데이터를 호출한뒤

useQuery에 key와 콜백함수만 등록해주면

 

queryClient를 이용해서

이전에 불러왔던 데이터를 확인할 수 있다.

 

redux를 쓸때의 코드양과 비교하면

이루 말할수없을정도로

행복한 코드라고 할수있다.

 

서버로 부터 불러온 데이터를 재가공할일이

많다던지 하는 경우에는

redux를 쓰라고 한다.

 

그건 그때가서 볼일이다.

웹/앱을 개발한지 얼마 안됐을때

그 프로젝트가 그 단계까지 갈지말지는

모르는법이다.

 

가볍게 시작해서

살을 더해가자

 

반응형
반응형

대부분의 성행하는 앱들은

앱 실행시 진입 화면이 있다.

 

그것이 광고성 이미지가 될수도있고

앱을 간략히 소개하는 이미지가 될수도 있다.

 

우리도 앱개발을 하다보면

반드시 넣을수밖에 없는 기능이라고 봐야한다.

 

준비물

1. react-native-splash-screen

2. @bam.tech/react-native-make

 

// splash-screen
npm install react-native-splash-screen
or
yarn add react-native-splash-screen


// helper
npm install --save-dev @bam.tech/react-native-make
or
yarn add npm --dev @bam.tech/react-native-make

 

@bam.tech/react-native-make는 설치시

설정할것은 따로없다.

 

react-native-splash-screen은 문서에 따르면

무언가 많이 설정해줘야하지만

그것을 따르지말고

아래의 가이드라인만 따라서 해보자

 

// android/app/src/main/java/com/[projectName]/MainActivity.java

...
import org.devio.rn.splashscreen.SplashScreen;
...

public class MainActivity extends ReactActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    SplashScreen.show(this, R.style.SplashScreenTheme); // 여기 추가
    super.onCreate(savedInstanceState);
  }

  /**
   * Returns the name of the main component registered from JavaScript. This is used to schedule
   * rendering of the component.
   */
  @Override
  protected String getMainComponentName() {
    return "ProjectName";
  }
}

 

...
#import "RNSplashScreen.h"
...

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...
  
  [RNSplashScreen show];
  return YES;
}

 

헬퍼로 이미지 변환해서 splash file 설정하는 스크립트 추가

// 사용방법: react-native set-splash --path [path-to-image] --resize [contain|cover|center] --background ["background-color"]
// NOTE: 이렇게 스크립트를 만들어두면 잊어버릴일도 없고, 이미지나 배경색상을 바꿀때 이 스크립트만 수정해주면 된다
// package.json

"scripts": {
  ...
  "splash": "react-native set-splash --path [image path] --background ['color'] --resize contain"
}

스크립트를 실행시켜보자

npm run splash

 

마지막으로, 실행된 스크립트가

앱 실행후 자동으로 닫히도록

코드를 추가

// project root - index.js or app.js

import SplashScreen from 'react-native-splash-screen';

...
useEffect(() => {
  SplashScreen.hide();
}, []);
...


or 

...
useEffect(() => {
  setTimeout(() => {
    SplashScreen.hide();
  }, 1500);
}, []);
...

 

반응형
반응형

[프로젝트 생성하기]

https://honeystorage.tistory.com/252?category=784116 

 

[개발환경 세팅하기]

https://honeystorage.tistory.com/253?category=784116 

 

[절대경로, 스타일 모듈 및 에디터 설정]

https://honeystorage.tistory.com/254

 

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

https://honeystorage.tistory.com/306

 

푸시알림 보내기와 관련된 키워드는 크게 4가지이다

1. react-native-push-notification && @react-native-community/push-notification-ios

2. react-native-firebase/app && react-native-firebase/messaging

3. 푸시알림 관련 firebase 설정

4. 푸시알림 관련 apple developer 설정

 

 

솔직히 앱을 개발할때 처음에 한번만 하면 되지만

할게 많아 마치 진입장벽처럼 느껴진다.

 

심지어 자주 안하다보니 매번 할때마다 새로운느낌...

 

 

(1) react-native-push-notification

그리고 @react-native-community/push-notification-ios

관련 세팅

 

늘 그렇듯 필요한 모듈을 설치하고

환경설정을 먼저 해본다.

 

yarn add react-native-push-notification
yarn add @react-native-community/push-notification-ios

https://www.npmjs.com/package/react-native-push-notification

환경설정은 위에 가이드라인을 충실히 따르자

https://www.npmjs.com/package/@react-native-community/push-notification-ios

ios를 위해 추가적으로 설정을 해주자

 

 

 

(2) firebase를 설정

낯선용어라도 겁먹지말고 잘 따라가면 

완성되어있을테니 가이드라인을 따라가보자

 

https://firebase.google.com/?hl=ko

위 링크로 접속해서 프로젝트를 생성해보자

그리고 위 버튼을 통해 ios와 android를 만든다.

다음, 안드로이드를 클릭

google-service.json을 다운로드해서

your-project/android/app/

경로에 위치시킨다.

 

ios에서도

GoogleService-Info.plist를

내려받아보자.

마찬가지로 적절한 위치에 배치할건데

xcode를 이용할것이다.

위 버튼에서 열리는 창에서

GoogleService-Info.plist를 add

 

아주 기본적인 설정은 여기까지이다.

 

지금부터는 돈이 들어간다.  ios때문이다.

안드로이드만 운영할 사람은 이 파트는 생략해도좋다.

 

(3) apple developer 설정

https://developer.apple.com/kr/

위 링크로 접속해서 계정(account)를 클릭

개발자 멤버십을 구매하자 (년 단위 갱신이 필요하다)

 

결제가 완료되면 아래와 같은

화면을 만날수 있다.

 

본격적인 시작에 앞서

키체인을 통해 CSR이라 불리는 파일을 하나 만들것이다.

mac에서 키체인 접근을 열어보자

키체인 접근 >>> 인증 기관에서 인증서 요청

위와 같이 옵션 체크해서 

저장하면 

이런 파일을 얻을수 있다.

해당 파일은 앞으로 작업에 계속 쓰일예정이다.

 

 

Apple developer 사이트를 열어보자

여기서 Apns key파일을 만들건데 여기서 만드는 키 파일은

최대 2개까지만 생성 가능하다.

관리에 주의하자

첫번째 항목을 선택해서

확실하게 알아볼수 있는 KeyName을 붙여준뒤

생성!

 

 

다음은

다시 apple developer 사이트로 돌아가

Certificates, IDs 탭으로 이동

 

바로 인증서를 만들어볼것이다

추가 버튼을 눌러보자

2가지 인증서를 만들어야한다.

한개는 개발용, 나머지 하나는 배포용이다.

 

우리는 10개정도의 인증서를 만들어낼 계획이니

인증서의 네이밍을 확실하게 하자.

 

여기서는

IOS Developer

Apple Distribution

두개의 파일을 추가로 얻었다

계속해서 진행해보자

 

다운받은 두개의 파일을

각각 더블클릭해서

키체인에 추가한다.

xcode에서 팀 인증을 할때 release, debug에 맞게 쓰면 된다.

 

 

AppID를 추가할 차례이다

다시 Apple developer 사이트로 돌아가보자

화면의 + 버튼을 눌러보자 >>> App Ids 상태로 Continue >>> App 상태로 Continue

 

** 앱 상세기능 목록에서 Push Notification을 추가할것이다.

중요한 포인트는 Push Notification 라인에

잘 보면 Configure 0개라고 써있을것이다

 

이것을 클릭해서 2개를 추가해줄건데

이때, 처음에 만들었던 CSR 파일을 이용하면 된다.

 

추가했다면 다음으로 넘어가

xcode를 참고해 Bundle ID를 입력,

Description은 어떤 앱을 위한 ID인지 분별할수 있게 작성

AppID 설정도 완료되었다

 

 

이제 60%쯤왔다.

정말 길고 힘들지만

피할수없다. 앱에 푸시를 안쓸거면

굳이 앱이 필요한가? 싶을정도로

푸시는 중요하기 때문이다.

 

 

테스트 Device를 등록하면

TestFlight이나 Firebase Distribution을 이용해

온디바이스 테스트를 진행해볼수도 있다.

 

근데 이 또한 지나칠수 없다.

Simulator에서는 푸시알림 테스트가

애플 정책상 불가하기 때문이다.

Apple Developer에서 테스트 디바이스를 추가할 수 있다.

맥에 아이폰을 연결하면

xcode에서 내 아이폰(혹은 다른 ios 디바이스)의

고유 아이디를 확인할수있다.

(xcode > Window > Devices and Simulators)

 

 

Apple Developer에서 해야할 마지막 작업이다.

바로 Privisioning Profiles...

어렵지 않다.

이전에 했던 작업들과 유사하다.

이미지의 선택된

두가지 항목에 대해서 작업을 진행할것이다.

위 작업은 계속해서 Continue하며 안내에 따라 진행하면된다.

이때 다운로드하는 파일의 이름을 dev, dist에 알맞게 지어주자

이렇게 앞에 수식어를 붙여줌으로써 구분해두면 좋다

이제 요 파일들을 가지고 xcode에서

signing 작업을 진행해줄것이다.

debug, release 탭에서 동일하게 위와같이 작업해주면된다

Auto는 꺼주고 다운로드 받은 Provisioning Profile로

인증을 진행해주는것이다.

 

마지막으로 

xcode에서 뺴먹지말고 해줄 셋팅이있다.

https://rnfirebase.io/messaging/usage/ios-setup

위 가이드라인에도 잘 나와있듯이

 

이렇게 Background Modes에 두가지 속성을 켜주고

PushNotification을 셋업해줘야한다.

 

 

자, 이제 지긋지긋한 셋업도

진짜 끝나간다.

 

아래 모듈을 설치하자

yarn add @react-native-firebase/app
yarn add @react-native-firebase/messaging

cd ios/ && pod install

(npm으로 하지말고 yarn으로 설치할것을 권장

안그러면 설치 오류를 만날수있음)

 

프로젝트에 firebase.json추가

// firebase.json

{
  "react-native": {
    "analytics_auto_collection_enabled": false,
    "messaging_auto_init_enabled": false,
    "messaging_android_headless_task_timeout": 30000,
    "messaging_android_notification_color": "@color/dark"
  }
}

 

드디어 코드를 만들어볼 차례이다.

 

utils/push.fcm.js

utils/push.noti.js

 

utils 폴더를 하나 만들고 그 아래 두개의 파일을 만들어보자

(babel.config.js와 tsconfig.json 세팅도 이전 게시물을 참고해서 설정해보기)

 

// push.noti.js

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

class LocalNotificationService {
  configure = onOpenNotification => {
    PushNotification.configure({
      onRegister: function (token) {
        console.log('[LocalNotificationService] onRegister : localtoken', token);
      },
      onNotification: function (notification) {
        console.log('[LocalNotificationService] onNotification ', notification);
        if (!notification?.data) {
          return;
        }
        notification.userInteraction = true;
        onOpenNotification(Platform.OS === 'ios' ? notification.data.item : notification.data);

        if (Platform.OS === 'ios') {
          notification.finish(PushNotificationIOS.FetchResult.NoData);
        }
      },
      permissions: {
        alert: true,
        badge: true,
        sound: true,
      },
      popInitialNotification: true,
      requestPermissions: true,
    });
  };

  unRegister = () => {
    PushNotification.unregister();
  };

  showNotification = (id, title, message, data = {}, options = {}) => {
    PushNotification.localNotification({
      // Android only Properties
      ...this.buildAndroidNotification(id, title, message, data, options),
      // IOS and Android properties
      ...this.buildIOSNotification(id, title, message, data, options),
      // IOS and Android properties
      title: title || '',
      message: message || '',
      playSound: options.playSound || false,
      soundName: options.soundName || 'default',
      userInteraction: false,
    });
  };

  buildAndroidNotification = (id, title, message, data = {}, options = {}) => {
    return {
      id: id,
      authCancel: true,
      largeIcon: options.largeIcon || 'ic_launcher',
      smallIcon: options.smallIcon || 'ic_notification',
      bigText: message || '',
      subText: title || '',
      vibrate: options.vibrate || true,
      vibration: options.vibration || 300,
      priority: options.priority || 'high',
      importance: options.importance || 'high',
      data: data,
    };
  };

  buildIOSNotification = (id, title, message, data = {}, options = {}) => {
    return {
      alertAction: options.alertAction || 'view',
      category: options.category || '',
      userInfo: {
        id: id,
        item: data,
      },
    };
  };

  cancelAllLocalNotifications = () => {
    if (Platform.OS === 'ios') {
      PushNotificationIOS.removeAllDeliveredNotifications();
    } else {
      PushNotification.cancelAllLocalNotifications();
    }
  };

  removeDeliveredNotificationByID = notification => {
    console.log('[LocalNotificationService] removeDeliveredNotificationByID:', notification);
    PushNotification.cancelLocalNotifications({id: `${notificationId}`});
  };
}

export const localNotificationService = new LocalNotificationService();

 

// push.fcm.js

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

class FCMService {
  register = (onRegister, onNotification, onOpenNotification) => {
    this.checkPermission(onRegister);
    this.createNotificationListeners(onRegister, onNotification, onOpenNotification);
  };

  registerAppWithFCM = async () => {
    if (Platform.OS === 'ios') {
      await messaging().setAutoInitEnabled(true);
    }
  };

  checkPermission = onRegister => {
    messaging()
    .hasPermission()
    .then(enabled => {
      if (enabled) {
        this.getToken(onRegister);
      } else {
        this.requestPermission(onRegister);
      }
    })
    .catch(error => {
      console.log('[FCMService] Permission rejected ', error);
    });
  };

  getToken = onRegister => {
    messaging()
    .getToken()
    .then(fcmToken => {
      if (fcmToken) {
        onRegister(fcmToken);
      } else {
        console.log('[FCMService] User does not have a device token');
      }
    })
    .catch(error => {
      console.log('[FCMService] getToken rejected', error);
    });
  };

  requestPermission = onRegister => {
    messaging()
    .requestPermission()
    .then(() => {
      this.getToken(onRegister);
    })
    .catch(error => {
      console.log('[FCMService] Request Permission rejected', error);
    });
  };

  deleteToken = () => {
    messaging()
    .deleteToken()
    .catch(error => {
      console.log('[FCMService] Delete token error', error);
    });
  };

  createNotificationListeners = (onRegister, onNotification, onOpenNotification) => {
    messaging().onNotificationOpenedApp(remoteMessage => {
      if (remoteMessage) {
        const notification = remoteMessage.notification;
        onOpenNotification(notification);
      }
            
      Alert.alert(remoteMessage.body);
    });

    messaging()
    .getInitialNotification()
    .then(remoteMessage => {
      if (remoteMessage) {
        const notification = remoteMessage.notification;
        onOpenNotification(notification);
      }
    })
    .catch(error => {
      console.log('quit state notification error : ', error);
    });

    this.messageListener = messaging().onMessage(async remoteMessage => {
      if (remoteMessage) {
        let notification = null;
        if (Platform.OS === 'ios') {
          notification = remoteMessage.data.notification;
        } else {
          notification = remoteMessage.notification;
        }
        onNotification(notification);
      }
    });

    messaging().onTokenRefresh(fcmToken => {
      onRegister(fcmToken);
    });
  };

  unRegister = () => {
    this.messageListener();
  };
}

export const fcmService = new FCMService();

 

// app.js - 단순 참고용이니 참고만하자

import {fcmService} from '@utils/push.fcm';
import {localNotificationService} from '@utils/push.noti';

const App = () => {
  const [token, setToken] = useState('');
  
  useEffect(() => {
    fcmService.registerAppWithFCM();
    fcmService.register(onRegister, onNotification, onOpenNotification);
    localNotificationService.configure(onOpenNotification);
  }, []);

  const onRegister = (tk: string) => {
    console.log('[App] onRegister : token :', tk);
    if (tk) setToken(tk);
  }

  const onNotification = (notify: any) => {
    console.log('[App] onNotification : notify :', notify);
    const options = {
      soundName: 'default',
      playSound: true,
    };

    localNotificationService.showNotification(
      0,
      notify.title,
      notify.body,
      notify,
      options,
    );
  }

  const onOpenNotification = (notify: any) => {
    console.log('[App] onOpenNotification : notify :', notify);
    Alert.alert('Open Notification : notify.body :' + notify.body);
  }
  
  const onCopy = () => {
    Clipboard.setString(token);
  }
  
  ...
  
  return (
    ...
      <TouchableOpacity onPress={onCopy}>
        <Section title="My token">{token || 'unknown token'}</Section>
      </TouchableOpacity>
    ...
  )
}
// index.js - 마찬가지다 참고만하자

import messaging from '@react-native-firebase/messaging';

messaging().setBackgroundMessageHandler(async remoteMessage => {
    console.log('Background remote message: ', remoteMessage);
});

function HeadlessCheck({isHeadless}) {
    if (isHeadless) {
        return null;
    }

    return <App />;
}

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

 

길고 긴 세팅이 모두 끝났다.

알림을 발송해보자

 

안드로이드와 아이폰에서 각각 앱을 빌드해두고

(npm run android, npm run ios)

firebase 페이지에 접속한다

인증키를 아직 등록하지 않았다면

화살표부분에 인증키를 입력하는 버튼이 있을것이다.

아까 처음에 만들었던 Apns키를 여기서 등록해주면 된다.

Team ID는

Apple Developer -> Account에서 확인할 수 있다.

 

 

자 발송을 해보자!

위 버튼을 이용해서 알림을 발송해 볼수있는데

이때 토큰을 입력하라고한다.

토큰의 디바이스별로 발급되는 고유 토큰인데

 

위에서 작성한 코드를 통해 우리는

UI로 확인하고 터치하여 코드를 복사할 수 있다.

화면에서 코드를 복사해다가

발송을 시도해보자

 

잘 되길 바란다.

미래의 나에게...

반응형