프로젝트 시작하기

1. 환경 셋팅

1.1 자바 17

MacOS에서는 zsh, bash shell에 각각 JAVA_HOME 적용 및 확인

$ java -version
java version "17.0.9" 2023-10-17 LTS

1.2 Ruby 3.0.0 MacOS

$ ruby -versions
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [arm64-darwin22]

$ rbenv versions
  system
* 3.0.0 (set by /Users/ios/.rbenv/version)

1.3 yarn

MacOS 이슈 : npm으로 react native 설치 후 init시 버전 이슈로 최신 버전으로 생성이 안됨.

$ sudo npm i -g yarn

1.4 React Native CLI

리액트 네이티브 설치

$ yarn global add react-native-cli


2. 프로젝트 셋팅

2.1 프로젝트 생성

$ sudo npx react-native init JJAMONG_APP --version 0.73.0
Do you want to install CocoaPods now? - n

2.2 폴더 권한 변경 MacOS

프로젝트를 sudo로 생성했기에 모든 폴더 권한 허용으로 변경
JJAMONG_APP 폴더 > 정보 가져오기 > 자물쇠해제 > everyone, staff 읽기 및 쓰기로 권한 변경 > … 선택 > 하위 항목에 적용

2.3 코코아포드 설치 IOS

$ cd ios
$ pod install

pod install이 오류 발생 할 경우 아래의 명령어로 해결 됨. 최초 이후 발생 안함.
(https://velog.io/@cullen/React-Native-초기-설정-오류Pod-install)

sudo xcode-select --switch /Applications/Xcode.app

2.4 No bundle URL present. ~ .jsbundle ~ IOS

Xcode로 불러온 다음 실행 시 No bundle URL present. ~ .jsbundle ~ 에러가 발생할 경우 아래 명령어 입력

$ yarn react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios'

이후 생성되는 main.jsbundle파일이 xcode에 자동 추가 되지 않아 프로젝트 상단에 드롭다운
(https://velog.io/@haerim95/React-Native-Error-Log-No-bundle-URL-present-이슈)

2.5 ~ (run ‘react-native start’) or that your bundle ‘index.android.bundle’ ~ AOS

/JJAMONG_APP/android/app/src/main 경로에 assets 폴더 추가
추가 이후 아래 명령어 입력
(https://borntodevelop.tistory.com/entry/React-Native-Error-Unable-to-load-script-Make-sure-youre-either-running-a-Metro-server-run-react-native-start-or-that-your-bundle-indexandroidbundle-is-packaged-correctly-for-release)

yarn react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle

2.6 패키지명 변경, 폴더변경

AOS

com.jjamong_app > com.jjamong.app 로 변경
com/jjamong_app 폴더 아래의 파일들 com/jjamong/app 폴더 아래로 추가

IOS

Xcode JJAMONG_APP ROOT 선택 > Siging & Capabilites > All > Team > DA Information. Co. Ltd 선택
Xcode JJAMONG_APP ROOT 선택 > Siging & Capabilites > All > Bundle Identifier > com.jjamong.app 입력

2.7 앱 이름 변경

AOS
/JJAMONG_APP/android/app/src/main/res/values/string.xml

<resources>
    <string name="app_name">자라다</string>
</resources>
IOS
/JJAMONG_APP/ios/JJAMONG_APP/info.plist

Information Property List > Bundle display name > 자라다 입력

2.8 앱 아이콘 변경

앱 아이콘 이미지는 앱 제작 및 리사이징 사이트 등을 통해 aos, ios에 맞게 이미지를 준비합니다.

AOS

/JJAMONG_APP/android/app/src/main/res 아래 폴더(hdpi, mdpi, xhdpi, xxhdpi, xxxhdpi)에 아이콘 이미지 변경

IOS

Xcode에서 JJAMONG_APP > JJAMONG_APP > Images 선택
AppIcon안에 이미지 사이즈에 맞게 드롭다운

2.9 스플래시 빈 화면으로 변경 IOS


Xcode에서 JJAMONG_APP > JJAMONG_APP > LaunchScreen 선택 화면에 출력되는 텍스트들 제거 후 저장

2.10 푸시를 위한 config 파일 추가

AOS

google-services.json 파일 /JJAMONG_APP/android/app/ 경로에 추가

IOS

GoogleService-Info.plist 파일 /JJAMONG_APP/ios/JJAMONG_APP/ 경로에 추가 후
Xcode JJAMONG_APP > JJAMONG_APP 경로에 드롭다운

2.10 Xcode 푸시 설정 IOS

Xcode JJAMONG_APP ROOT 선택 > Siging & Capabilites > + Capability 선택 > Push Notifications 선택

Xcode JJAMONG_APP ROOT 선택 > Siging & Capabilites > + Capability 선택 > Background Modes 선택
Remote notifications 체크


3. 플러그인 셋팅

3.1 웹뷰

3.1.1 플러그인 설치

$ sudo yarn add react-native-webview@13.6.3

ios일 경우 pod install

3.1.2 코드 추가 AOS

AOS에서 webview의 alert이 실행되면 alert창 title에 기본적으로 web url이 출력됩니다.
아래 코드를 추가하면 webview의 alert 기본 타이틀을 빈값으로 설정합니다.

RNCWebChromeClient (/JJAMONG_APP/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview)
// /JJAMONG_APP/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview

@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
  new AlertDialog.Builder(this.mWebView.getThemedReactContext())
      .setTitle("")
      .setMessage(message)
      .setPositiveButton("확인", new AlertDialog.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
          result.confirm();
        }
      }).setCancelable(false).create().show();
  return true;
}

@Override
public boolean onJsConfirm(WebView view, String url, String message, final android.webkit.JsResult result) {
  new AlertDialog.Builder(this.mWebView.getThemedReactContext())
      .setTitle("")
      .setMessage(message)
      .setPositiveButton("확인", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int which) {
          result.confirm();
        }
      })
      .setNegativeButton("취소", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int which) {
          result.cancel();
        }
      })
      .create()
      .show();

  return true;
}

3.2 푸쉬

3.2.1 플러그인 설치

$ sudo yarn add @react-native-firebase/app@18.7.3
$ sudo yarn add @react-native-firebase/messaging@18.7.3

3.2.2 코드 추가 IOS

podfile

podfile(/JJAMONG_APP/ios/podfile)

아래 내용 코드 config = use_native_modules! 부분 아래 추가

pod 'Firebase', :modular_headers => true
pod 'FirebaseCoreInternal', :modular_headers => true
pod 'FirebaseCore', :modular_headers => true
pod 'FirebaseMessaging', :modular_headers => true
pod 'GoogleUtilities', :modular_headers => true

코드 추가 후 pod install

AppDelegate.m

AppDelegate.m (/JJAMONG_APP/ios/JJAMONG_APP/AppDelegate.m)

아래 코드 최상단에 추가

#import <Firebase.h>

didFinishLaunchingWithOptions 안에 추가

if ([FIRApp defaultApp] == nil) {
  [FIRApp configure];
}

(https://velog.io/@ordidxzero/rn-random-chat-connect-firebase)

3.2.3 코드 추가 AOS

build.gradle

build.gradle (/JJAMONG_APP/android/build.gradle)

dependencies {} 영역에 아래 코드 추가

classpath("com.google.gms:google-services:4.3.14")

build.gradle

build.gradle (/JJAMONG_APP/android/app/build.gradle)

apply plugin: “com.android.application” 아래 코드 추가

apply plugin: "com.google.gms.google-services"

dependencies {} 영역에 아래 코드 추가

// Import the Firebase BoM
implementation platform('com.google.firebase:firebase-bom:31.1.0')
implementation 'com.google.firebase:firebase-messaging'

MainActivity.kt

MainActivity.kt (/JJAMONG_APP/android/app/src/main/java/com/dainformation/zarada)

아래 코드 추가

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  askNotificationPermission()
}

// Declare the launcher at the top of your Activity/Fragment:
private val requestPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
  if (isGranted) {
  // FCM SDK (and your app) can post notifications.
  } else {
  // TODO: Inform user that that your app will not show notifications.
  }
}

private fun askNotificationPermission() {
  // This is only necessary for API level >= 33 (TIRAMISU)
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ==
        PackageManager.PERMISSION_GRANTED
    ) {
      // FCM SDK (and your app) can post notifications.
    } else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
      // TODO: display an educational UI explaining to the user the features that will be enabled
      //       by them granting the POST_NOTIFICATION permission. This UI should provide the user
      //       "OK" and "No thanks" buttons. If the user selects "OK," directly request the permission.
      //       If the user selects "No thanks," allow the user to continue without notifications.
    } else {
      // Directly ask for the permission
      requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
    }
  }
}

3.3 모달

3.3.1 플러그인 설치

$ sudo yarn add react-native-modal@13.0.1

4. App.tsx 코드 추가

import React, { useEffect, useState, useRef } from "react";
import { Alert, View, BackHandler, SafeAreaView, StyleSheet } from "react-native";
import { WebView } from 'react-native-webview'
import messaging from '@react-native-firebase/messaging';
import Modal from "react-native-modal";

// 백그라운드 푸시
messaging().setBackgroundMessageHandler(async remoteMessage => {
    console.log('[Background Remote Message]', remoteMessage)
})

// IOS푸시 허용
async function requestUserPermission() {
    const authStatus = await messaging().requestPermission();
    const enabled =
    authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
    authStatus === messaging.AuthorizationStatus.PROVISIONAL;

    if (enabled) {
        console.log('Authorization status:', authStatus);
    }
}
requestUserPermission()

const App = () => {
    const webview = useRef();
    //const source = {uri: 'http://192.168.0.1:8080'}; // 로컬
    const source = {uri: 'https://dayyoung.github.io'}; // 스테이징
    const [backState, setBackState] = useState({url: '', canGoBack: false})

    const [modalVisible, setModalVisible] = useState(false)
    const [modalSource, setModalSource] = useState({uri: ''})
    
    // FCM토큰 가져오기
    const getFcmToken = async () => {
        try {
            const fcmToken = await messaging().getToken();
            console.log('[FCM Token] ', fcmToken);
        } catch (error) {
            console.log('[FCM Token] error', error);
        }
    }

    useEffect(() => {
        getFcmToken()

        // 포그라운드 푸시
        messaging().onMessage(async remoteMessage => {
            console.log('[Remote Message] ', JSON.stringify(remoteMessage));
            let message = JSON.stringify(remoteMessage)
            Alert.alert("", remoteMessage.notification?.body)
        })
    }, [])

    useEffect(() => {
        // 종료 창
        function close() {
            Alert.alert("", "확인을 누르면 종료합니다.", [
                {text: "취소", onPress: () => {}, style: "cancel"},
                {text: "확인", onPress: () => BackHandler.exitApp()},
            ])
        }

        // 뒤로가기
        const backAction = (): boolean => {
            if (backState.canGoBack) {
                if (backState.url == source.uri + "/resources/index.html#/login") {
                    close();
                } else {
                    // 뒤로 갈 수 있는 상태라면 이전 웹페이지로 이동한다
                    webview.current.goBack()
                }
            } else {
                close();
            }
            return true;
        }
        
        BackHandler.addEventListener('hardwareBackPress', backAction)
        return (): void => {
            BackHandler.removeEventListener('hardwareBackPress', backAction)
        };
    }, [backState.url, backState.canGoBack])

    // WebView 메시지
    const webViewMessage = (response): void => {
        response = JSON.parse(response);
        let key = response.key;
        let data = response.data;
        
        // 나이스인증 열기
        if (key === 'niceidOpen') {
            setModalVisible(true)
            setModalSource({uri: source.uri + '/checkplus_main'})
        // 나이스인증 닫기
        } else if (key === 'niceidClose') {
            setModalVisible(false)
            webview.current.postMessage(JSON.stringify(response))
        // 주소찾기 열기
        } else if (key === 'addrSearchOpen') {
            setModalVisible(true)
            setModalSource({uri: source.uri + '/resources/daum.jsp'})
        // 주소찾기 닫기
        } else if (key === 'addrSearchClose') {
            setModalVisible(false)
            webview.current.postMessage(JSON.stringify(response))
        }
    }

    return (
		<> 
            {/* 기본 웹뷰 */}
            <SafeAreaView style={{ flex: 1 }}>
                <WebView
                    ref={webview}
                    source={source}
                    setSupportMultipleWindows={false}
                    onNavigationStateChange={(navState) => {
                        setBackState({ url: navState.url, canGoBack: navState.canGoBack })
                    }}
                    onMessage={event => {
                        webViewMessage(event.nativeEvent.data)
                    }}
                />
            </SafeAreaView>
            
            <Modal isVisible={modalVisible} transparent={true} animationType="slide" onRequestClose={() => setModalVisible(false)} 
                style={{ position:'absolute', margin: 0, height:'100%', width:'100%', zIndex:1, alignSelf:'center'}}>
                <SafeAreaView style={{ flex: 1 }}>
                    <WebView
                        source={modalSource}
                        onMessage={event => {
                            webViewMessage(event.nativeEvent.data)
                        }}
                    />
                </SafeAreaView>
            </Modal>
		</>
    );
};

export default App;
app.tsx 반영 IOS

react native 코드 추가 후 아래 명령어를 입력해야 xcode에 반영 됨
yarn react-native bundle –entry-file=‘index.js’ –bundle-output=‘./ios/main.jsbundle’ –dev=false –platform=‘ios’

Contents