반응형

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을 구현하는 방향으로

돛을 돌렸다.

 

 

반응형
반응형

>>> RNFirebase 백그라운드 알림이 왜 수신이 안돼?

>>> RNFirebase 아이콘이 왜 안나와?

 

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

https://honeystorage.tistory.com/306

 

 

푸시 알림이

ㄱ) foreground

ㄴ) background

ㄷ) quit state

에서 모두 정상적으로 수신되는것을

분명히 확인했었다

 

라고 착각을 하고있었는데

앱 배포 직전

검토 과정에서

iOS에 알림이 수신은 되지만

진동이 울리지 않음을 발견하였다.

 

위 문제를 해결하기 위해

아래와 같은 방법으로 접근하였다.

 

1. 내가 설정을 덜 한것은 없는지

2. rnfirebase, github에 올라온 issues중에 같은 내용은 없는지

3. vibration 기능의 접근 권한이 문제인지

4. setBackgounrMessageHandler 수신 부분이 문제인지

5. 푸시 발송관련 서버 코드에 문제가 있는것인지

 

1. 내가 설정을 덜 한것은 없는지

rnfirebase docs를 통해 체크한 결과

headless 상태에 대한 처리가 미흡해 보였다.

// index.js
import { AppRegistry } from 'react-native';
import messaging from '@react-native-firebase/messaging';

messaging().setBackgroundMessageHandler(async remoteMessage => {
  console.log('Message handled in the background!', remoteMessage);
});

messaging()
  .getIsHeadless()
  .then(isHeadless => {
    // do sth with isHeadless
  });
  
function HeadlessCheck({ isHeadless }) {
  if (isHeadless) {
    // App has been launched in the background by iOS, ignore
    return null;
  }

  return <App />;
}

function App() {
  // Your application
}

AppRegistry.registerComponent('app', () => HeadlessCheck);
// appDelegate.m

// add this import statement at the top of your `AppDelegate.m` file
#import "RNFBMessagingModule.h"

// in "(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions" method
// Use `addCustomPropsToUserProps` to pass in props for initialization of your app
// Or pass in `nil` if you have none as per below example
// For `withLaunchOptions` please pass in `launchOptions` object
NSDictionary *appProperties = [RNFBMessagingModule addCustomPropsToUserProps:nil withLaunchOptions:launchOptions];

// Find the `RCTRootView` instance and update the `initialProperties` with your `appProperties` instance
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                             moduleName:@"nameOfYourApp"
                                             initialProperties:appProperties];

위 세가지 설정들에 대해서 다시 한번

검토를 하였다.

 

2. rnfirebase, github에 올라온 issues중에 같은 내용은 없는지

제법 많은 비슷한 내용들이 있었다.

그 중에서 내가 찾은 솔루션은

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

위의 방법인데

이것은  3,4,5의 과정을 걸쳐서 찾은 솔루션이다.

 

3. vibration 기능의 접근 권한이 문제인지

react-native에 vibration이라는 기능이 내포되어있다.

이 마저도 해보려고하니 잘못된 정보들이 너무나도 많았다.

임시 버튼을 만들어

기능이 잘 동작하는지만 확인하였다.

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

// test code
const umm = () => {
  Platform.OS === 'ios' && Vibration.vibrate([400]);
}

 

4. setBackgounrdMessageHandler 수신 부분이 문제인지

화면을 열어보면 푸시가 와있어서

setBackgounrdMessageHandler 리스너에

문제가 있으리라고는 생각해보지 않았었다.

 

하지만, 디바이스에서는 수신했더라도

앱 내에 setBackgounrdMessageHandler 리스너가

실제로 잘 동작하고 있는지는 검토해볼 필요가 있었다.

messaging().setBackgroundMessageHandler(async remoteMessage => {
  umm(); // vibration
  console.log('Message handled in the background!', remoteMessage);
});

const umm = () => {
  Platform.OS === 'ios' && Vibration.vibrate([400]);
}

 

위와 같이 임시 코드를 작성해서

진동이 오는지 체크해본 결과

위의 리스너가 제대로 동작하고 있지 않음을 발견했다.

 

리스너가 제대로 동작하지 않는다면

설정의 문제 혹은 발송의 문제

둘중에 하나라고 결론을 냈다.

 

그런데 아까 분명히

설정의 문제는 확인을 완료했다.

그러면 발송의 문제일 것이다.

 

5. 푸시 발송관련 서버 코드에 문제가 있는것인지

2번에서 언급했던

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

위의 링크에서 알맞는 발송 포맷을 발견했다.

 

추후, 가이드를 다시보니 업데이트가 된것인지

위 github 링크와 동일한 내용으로 발송 포맷이 나와있었다.

 

github 마지막 코멘트 업데이트 일자가

patels9-vf commented 7 hours ago

겨우 7시간전이 아닌가...

위 이슈가 최근까지도 이어지고있었나보다.

 

공식 가이드에서는

아래와 같이 푸시메시지를 발송하라고 한다.

admin.messaging().send({
  data: {
    //some data
  },
  apns: {
    payload: {
      aps: {
        contentAvailable: true,
      },
    },
    headers: {
      'apns-push-type': 'background',
      'apns-priority': '5',
      'apns-topic': '', // your app bundle identifier
    },
  },
  //must include token, topic, or condition
  //token: //device token
  //topic: //notification topic
  //condition: //notification condition
});

 

다만, 위의 메시지는

data-only에 해당하는 것이기도 하고

보다 많은 케이스에 대비하고자

github의 가이드를 따랐다.

 

const message: any = {
  token,
  notification: {
    title,
    body,
  },
  data: {
    title,
    body,
  },
  android: {
    priority: 'high',
    notification: {
      title,
      body,
      sound: 'default'
    }
  },
  apns: {
    headers: {
      'apns-priority': '5',
    },
    payload: {
      aps: {
        sound: 'default',
        // contentAvailable: 1,
        'content-available': 1,
      }
    }
  }
};
admin.messaging().send(message);

 

위 방법으로 문제없이 메시지가 수신됨을 확인하였다.

 

 

** 번외 **

나는 애플워치를 사용하는데

핸드폰에서 알림이 안울리고

계속 애플워치에서 알림이 울려서

블루투스를 끊고 핸드폰에서 알림이 오는지에 대한

확인이 필요하였다.

 

테스트 결과 수신은 됐다.

다만, 어느정도의 딜레이가 발생했다.

 

가이드에 따르면 priority 설정된

푸시 메시지는

8seconds 정도의 간격으로만

발송이 가능한듯하다.

 

 

>>> RNFirebase 백그라운드 알림이 왜 수신이 안돼?

>>> RNFirebase 아이콘이 왜 안나와?

 

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

https://honeystorage.tistory.com/306

반응형
반응형

[ react-native-code-push 적용하기 ]

https://honeystorage.tistory.com/290

 

[ MS, appcenter 공식 Document ]

https://docs.microsoft.com/ko-kr/appcenter/distribution/codepush/

 

지난 포스팅에 이어

실무에서 사용하기 위한

몇가지 업데이트 사항을 알아보고자 한다

 

1. Mandatory (필수 적용 옵션)

배포 내용에 따라 주요 업데이트때와 마찬가지로

필수적으로 업데이트를 했으면 하는

경우가 발생할 수 있다.

 

배포간에 Mandatory옵션을 사용하면

"업데이트 / 나중에"와 같이 선택지를 주는게 아니라

"업데이트" 선택지 하나만 줄 수 있다.

 

// 일반적인 배포
appcenter codepush release-react -a username/appname -d Production

// 필수사항 배포
appcenter codepush release-react true -a username/appname -m -d Production

"-m" 부분이 추가되었다.

 

코드상에서 updateDialog를 수정해서

사용자에 보여줄 텍스트를 커스텀 할 수도있다.

codePush.sync({
 updateDialog: {
   titie: '업데이트 하세요!',
   mandatoryUpdateMessage: '필수 업데이트입니다. 진행해주세요.',
   mandatoryContinueButtonLabel: '업데이트',
 },
 mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
});

 

 

*** 기타 내용 업데이트 예정

반응형
반응형

패키지를 추가하거나

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-빌드해서 배포테스트를 해보자.

반응형
반응형

pm2를 이용해서

앱서버를 백그라운드 환경으로 운영이 가능하다.

 

여기서 한발 더 나아가

cluster mode로 실행중인 node.js서버를

pm2를 이용해 돌리곤 했었다.

 

하지만 배포할때마다 일시적으로 중단되어버리는 서버.

원인은 restart로 배포하기때문.

 

reload 명령어를 통해

kill -> restart

reset 형식으로 서버 중단없이

무중단 배포가 가능하다.

pm2 reload index.js

 

근데 이를 제대로 활용하려면

몇가지 설정이 필요하다

 


 

pm2 ecosystem

 

위 명령어로 ecosystem.config.js 파일을 얻을수 있다.

다양한 설정들이 기본으로 세팅되어있는데

docs를 보면서 입맛에 맞게 설정하면 된다.

 

진짜 간단히는

module.exports = {
    apps: [
        {
            name: 'server',
            script: './index.js',
            watch: '.',
            instances: -1, // 클러스터 모드
            // exec_mode: 'cluster', <-- 이것도 클러스터 모드
        },
    ],
};

이렇게만 설정해도 된다

 

package.json을 수정하면

npm 명령어로 서버에서

간편히 실행 혹은 reload 하는것도 가능하다.

scripts: {
  "start": "pm2 start ./ecosystem.config.js --only server",
  "reload": "pm2 reload server"
}

 

고객에게 불편을 주지말자

 


여기서 더 나아간다면

실행중인 process가 덮어씌워지는 경우를

방지하고

reload할 수 있다.

 

// server.js

const app = express();


let disableKeepAlive = false;

// 중단이 감지되면 Keep 상태인 요청 닫음
app.use((req, res, next) => {
    if (disableKeepAlive) {
        res.set('Connection', 'close');
    }
    next();
});

const service = app.listen(process.env.PORT, () => {
    console.log(`The application is listening on port ${process.env.PORT}`);
    if (process.send) {
        console.log('send')
        process.send('ready');
    }
});

process.on('SIGINT', async () => {
    disableKeepAlive = true;
    service.close();
    process.exit(0);
});

이런식으로 구성해주면 된다.

반응형
반응형

>>> RNFirebase 백그라운드 알림이 왜 수신이 안돼?

>>> RNFirebase 왜 진동이 안울려?

 

 

[Android에 해당하는 내용]

 

따로 설정하지 않으면

notification icon으로

기본 앱 icon으로 설정된다는 내용부터

 

transparent 이미지만

사용 가능하다는 등

다양한 솔루션을 거쳐

 

결론적으로 어떻게해야

아이콘을 display 할수있는지

알아냈다.

 

먼저, 아래 링크에서

이미지를 규격별로 만들것이다.

링크: 이미지 만들기

 

다음으로는, 여기

공식 가이드라인에서 안내하는데로

firebase.json을 수정해줄것이다.

 

그전에, /android/src/main/res/values/colors.xml에 

나의 앱에 맞는 custom-color를 만들고오자.

<resources>
  <color name='white'>#ffffff</color>
  <color name='custom'>#eeeee</color>
</resources>

 

다시 돌아가

firebase.json에 아래 링크를 참조해서

추가 설정을 해본다.

참고자료: 링크

// <projectRoot>/firebase.json
{
  "react-native": {
    "messaging_android_notification_color": "@color/custom"
  }
}

 

설정은 반쯤 되었다.

이번에는

아까 만든 규격별 이미지를 

/android/src/main/res/

아래에 모두 넣어주자.

이런느낌?

 

마지막으로

AndroidManifest.xml를 수정하고

실행해보면

정상적으로 notification 아이콘이 뜨는걸 볼수있다.

<application
 ...
>
  ...
  <meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@mipmap/이미지명" 
  />
  <meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/custom"
  /> <-- color 지정할때 반드시 firebase.json에서 설정한것과 같은 값으로 할것
  ...
</application>

 

실행 혹은 빌드한뒤

푸시를 보내보자

정상적으로 뜬다면 행복해하면된다.

 

>>> RNFirebase 백그라운드 알림이 왜 수신이 안돼?

>>> RNFirebase 왜 진동이 안울려?

반응형
반응형

수치의 최소, 최대치를 정하기위한

UI적 요소로 Multi slider를 고려하곤 한다.

(Multi slider란 이런식으로 양방향 슬라이딩을 하는것을 말함)

 

많은 라이브러리를 검토해본 결과

아래의 라이브러리를 선정하였으며

관련하여 짤막한 정리를 하고자한다.

 

npm i @ptomasroos/react-native-multi-slider@2.2.2

설치때는 2.2.2 버전으로 받기를 권장한다.

최신 버전에서는 오류를 경험했었다.

github의 issue를 트랙킹하여

2.2.2에서는 문제 없이 돌릴수 있음을 확인하였다.

 

Docs에 예제코드는 물론 자세한 설명이 나와있지만

나의 예제 코드를 본다면

<MultiSlider
  values={values}
  min={20}
  max={100}
  step={20}
  containerStyle={{alignItems: 'center', zIndex: 5}}
  trackStyle={{backgroundColor: 'blue', height: 2.5}}
  selectedStyle={{backgroundColor: 'red'}}
  sliderLength={screenWidth - 52}
  snapped
  customMarker={() => (
    <FastImage style={{width: 28, height: 28}} source={image} />
  )}
  onValuesChange={updateValue}
/>

 

참고로 위 이미지에서

칸칸이 표시한것은 별도의 컴포넌트를

슬라이더 뒤에 깔아준것이다.

 

반응형
반응형

구현간에 어처구니없는 이슈로

삽질을 1시간이나해서 남기는 기록...

 

목표

1. animation 처리할 박스의 크기를 구한다.

2. 박스 크기 측정이 완료되면 Collapsible 느낌으로 나타나게한다.

* 조건: 처리할 박스 하단에는 다른 컴포넌트는 없음

 

어떻게 처리할지 고민이 되었다.

박스의 크기를 구하려면

이미 박스가 렌더링 되어있어야하는데

그러면 Collapsible 느낌을 낼수가 없다는 역설

 

import React, { ReactNode, useEffect, useState } from 'react';
import { View } from 'react-native';
import Animated, { useAnimatedStyle, withTiming, useSharedValue } from 'react-native-reanimated';

function AnimateHoc() {
  return (
    <>
      <View>
        { children } <-- 이 내용물이 Collapsible 느낌으로 나타나는게 목표이다
      </View>
    </>
  )
}

Higher Order Component를 만들어 원할때

언제든지 쓰게할것이다

따라서 이름을 AnimateHoc라고 지어보았다.

 

그럼 박스의 크기를 일단 구해본다.

import React, { ReactNode, useEffect, useState } from 'react';
import { View } from 'react-native';
import Animated, { useAnimatedStyle, withTiming, useSharedValue } from 'react-native-reanimated';

function AnimateHoc({ duration, children }) {
  const [_height, setHeight] = useState(0);
  
  const onLayout = e => {
    const { height } = e.nativeEvent.layout;
    setHeight(height);
  }

  return (
    <>
      <View onLayout={onLayout}> <-- 박스크기 측정
        { children }
      </View>
    </>
  )
}

 

이러면 쉽게 박스의 크기를 구할수있는데,

이것이 사용자에게 노출되면 안된다.

꼼수로 opacity로 투명도를 낮춰주자.

 

import React, { ReactNode, useEffect, useState } from 'react';
import { View } from 'react-native';
import Animated, { useAnimatedStyle, withTiming, useSharedValue } from 'react-native-reanimated';

function AnimateHoc({ duration, children }) {
  const [_height, setHeight] = useState(0);
  const onLayout = e => {
    const { height } = e.nativeEvent.layout;
    setHeight(height);
  }

  return (
    <>
      <View onLayout={onLayout} style={{ opacity: 0 }}> <-- 꼼수
        { children } <-- 이 내용물이 Collapsible 느낌으로 나타나는게 목표이다
      </View>
    </>
  )
}

투명도를 낮추더라도 공간은 차지하니

아 아래에 다른 무언가가 있다면 이 HOC는

쓰기에 적합하지 않다. 다른 방법을 찾아야한다.

아니면 약간의 수정을

 

 

다음은, children이 서서히 나타나며 보이게하는 부분이다

(마치, Collapsible 처럼)

 

import React, { ReactNode, useEffect, useState } from 'react';
import { View } from 'react-native';
import Animated, { useAnimatedStyle, withTiming, useSharedValue } from 'react-native-reanimated';

function AnimateHoc({ duration, children }) {
  const [init, setInit] = useState(false);
  const [_height, setHeight] = useState(0);
  const viewHeight = useSharedValue(0);
  
  useEffect(() => {
    if (!init && _height > 0) {
      viewHeight.value = _height;
      setInit(true); <-- 굳이 init이라는 장치를 한번 더 적용한 이유는 잘못된 리렌더링을 방지하기 위함
    }
  }, [_height])

  const onLayout = e => {
    const { height } = e.nativeEvent.layout;
    setHeight(height);
  } 
  
  const fadeDown = useAnimatedStyle(() => {
    return {
      height: withTiming(viewHeight.value, {
        duration: duration || 750
      }),
    }
  }, [init])

  return (
    <>
      <Animated.View <-- 서서히 나타나야될 부분
        style={[{ height: 0 }, fadeDown]}
      >
        { children }
      </Animated.View>
      
      { <-- 크기 측정을 위해서 opacity: 0 상태로 렌더링하다가 측정완료되면 렌더링 하지않음
        !_height &&
        <View onLayout={onLayout} style={{ opacity: 0 }}> <-- 꼼수
          { children }
        </View>
      }
    </>
  )
}

 

이제는 완벽하다 생각했다

문제없이 동작할것이라 생각했는데

duration이 없기라도한듯

한번에 animated view가 나타났다

 

왜지, 아무리 구글링을해도

Docs를 살펴봐도

라이브러리 사용면에서는 문제는 없어보였다.

 

그래서 animated view에  backgroundColor를 넣어서

정상적으로 서서히 크기가 커지나 살펴보니

아주 정상적이었다!

다만, 그 내용물이 한번에 나타났을뿐!

박스는 천천히, 내용물은 한번에...

 

그래서overflow hidden을 설정해줌으로써 완벽히 정리되었다.

최종 코드

import React, { ReactNode, useEffect, useState } from 'react';
import { View } from 'react-native';
import Animated, { useAnimatedStyle, withTiming, useSharedValue } from 'react-native-reanimated';

function AnimateHoc({ duration, children }) {
  const [init, setInit] = useState(false);
  const [_height, setHeight] = useState(0);
  const viewHeight = useSharedValue(0);
  
  useEffect(() => {
    if (!init && _height > 0) {
      viewHeight.value = _height;
      setInit(true);
    }
  }, [_height])

  const onLayout = e => {
    const { height } = e.nativeEvent.layout;
    setHeight(height);
  } 
  
  const fadeDown = useAnimatedStyle(() => {
    return {
      height: withTiming(viewHeight.value, {
        duration: duration || 750
      }),
    }
  }, [init])

  return (
    <>
      <Animated.View <-- overflow 멈춰!!!
        style={[{ height: 0, overflow: 'hidden' }, fadeDown]}
      >
        { children }
      </Animated.View>
      
      {
        !_height &&
        <View onLayout={onLayout} style={{ opacity: 0 }}>
          { children }
        </View>
      }
    </>
  )
}

 

이제 이걸 활용한다면 이렇게 할수있을것이다.

// 기본 사용법
return (
  <View>
    <View>
     ... 
    </View>
    <AnimatedHoc>
      <View>
        <Text>1. 커져라 커져!</Text>
      </View>
      <View>
        <Text>2. 커져라 커져!</Text>
      </View>
      <View>
        <Text>3. 커져라 커져!</Text>
      </View>
    </AnimatedHoc>
  </View>
)

// 시간 조절 사용법
return (
  <View>
    <View>
     ... 
    </View>
    <AnimatedHoc duration={ 1500 }>
      <View>
        <Text>1. 천천히 커져라 커져!</Text>
      </View>
      <View>
        <Text>2. 천천히 커져라 커져!</Text>
      </View>
      <View>
        <Text>3. 천천히 커져라 커져!</Text>
      </View>
    </AnimatedHoc>
  </View>
)
반응형
반응형

1. 설치 관련

Docs에 따르면 앱을 설치한 뒤

babel.config.js를 수정해주어야 한다.

이 때 잘못 설정하면 에러가 발생한다.

module.exports = {
    presets: ['module:metro-react-native-babel-preset'],
    plugins: [
        'babel-plugin-styled-components',
        [
            'module-resolver',
            {
                root: ['.'],
                extensions: [
                    '.ios.ts',
                    '.android.ts',
                    '.ts',
                    '.ios.tsx',
                    '.android.tsx',
                    '.tsx',
                    '.jsx',
                    '.js',
                    '.json',
                ],
                alias: {
                    '@components': './src/components',
                    '@utils': './src/utils',
                    '@interface': './src/interface',
                    '@stack': './src/stack',
                    '@api': './src/api',
                    '@containers': './src/containers',
                    '@libs': './src/libs',
                    '@screens': './src/screens',
                    '@images': './src/images',
                },
            },
            // 'react-native-reanimated/plugin', <-- 여기 아님, 절대 아님
        ],
        'react-native-reanimated/plugin',
    ],
};

추가시 위치에 조심하자

특히, 반드시 맨 뒤에 삽입해야한다.

 

 

2. Freezing 현상

설치하고나니 랜덤하게 앱이 멈춰버리는 현상이 나타났다.

이슈를 뒤져보니 Freezing 관련한 글이 있었다.

https://github.com/software-mansion/react-native-reanimated/issues/1875

 

해결을 위해서는 

설치된 버전을 제거하고

react-native-reanimated@2.2.3 버전을 설치해주어야한다.

이 에러는 react-native-modal이나 react-navigation등의 라이브러리들과

충돌로인해 발생한다고한다.

반응형
반응형

 

위와 같은 UI는 사용자 경험에 도움이 되기 때문에

많은 상용앱에서 볼수있다.

 

이는 아이폰에서도 x계열에서 특별한 처리가 필요한데

하단에 여백이 발생하기 때문이다.

 

어떻게 처리할수있는지 살펴보자.

개인적으로는 꽤나 시행착오를 걸쳤다.

 

키워드는 3가지이다.

1. KeyboardAvoidingView

2. SafeAreaView

3. react-native-iphone-x-helper

 

3번 라이브러리는 설치를 해주도록하자.

npm i react-native-iphone-x-helper --save

 

이제 구현을 할건데

KeyboardAvoidingView는 항상

제일 껍데기에 둔다고 생각하면 편하다.

 

Docs에서 보면

안드로이드와 ios에 동작 방식을 다루게 주기도 하는데

여기서는 동일하게 처리하도록 한다.

 

import { KeyboardAvoidingView, SafeAreaView, View, StatusBar, Platform } from 'react-native';
import { isIhponeX } from 'react-native-iphone-x-helper';


const statusBarHeight = StatusBar.currentHeight;
const isIphone = isIphoneX() && Platform.OS === 'ios';
const paddingBottom = isIphone ? 34 : 0;
const offset = isIphone ? -34 : statusBarHeight;

...

return (
<KeyboardAvoidingView
  behavior='height'
  style={{ flex: 1, backgroundColor: '#00cebe', paddingBottom }}
  keyboardVerticalOffset={}
>
  <SafeAreaView style={{ backgroundColor: '#000000' }}>
    <View style={{ flex: 1 }}>
      ...
      <CustomInput 
        keyboardType='number-pad'
        placeholder='인증 코드를 입력해주세요'
      />
    </View>
    <CustomButton>다음</CustomButton>
  </SafeAreaView>
</KeyboardAvoidingView>
)

 

input이나 button은 만들기 나름인데

버튼은 아래와 같이 만들었다.

const BottomButtonContainer = styled.TouchableWithoutFeedback`
  width: 100%;
  height: 56px;
`;

const BottomButtonWrapper = styled.View`
  width: 100%;
  height: 56px;

  align-items: center;
  justify-content: center;
  background-color: #00cebe;
`;

const BottomButtonText = styled.Text`
  color: white;
  text-align: center;
  font-size: 16px;
  line-height: 30px;
  font-weight: bold;
  letter-spacing: -0.48px;
`;

export const BottomButton = ({ name, onPress }: { name: string; onPress: Function | any; }) => (
  <BottomButtonContainer onPress={onPress}>
    <BottomButtonWrapper>
      <BottomButtonText>{name}</BottomButtonText>
    </BottomButtonWrapper>
  </BottomButtonContainer>
);

 

 

 

**************************

**** 21. 11. 30 업데이트 ****

**************************

 

문제상황

behavior를 height로 잡았더니

TextInput의 focus나 스크롤 위치 등

상황이 변화함에따라 버튼이

키보드 아래로 숨어버리거나

반만 보이거나 하는 등의 문제상황이 발견되었다.

 

따라서,

아래와 같이 수정하였으며

UX적인 측면에서 훌륭하게 동작함을 확인하였다.

// app.ts

import { StatusBar } from 'react-native';

... 생략
return (
  <>
    <StatusBar 
      barStyle='light-content'
      backgroundColor: 'black'
    />
  </>
)
// auth.code.ts

import { KeyboardAvoidingView, SafeAreaView, View, StatusBar, Platform } from 'react-native';
import { isIhponeX, getStatusBarHeight } from 'react-native-iphone-x-helper';


...(생략)
const _inputValiation = ():boolean = () => {
  let valid = true;
  ...(생략)
 
  return valid;
}

const isIphonexStyle = isIhponeX() && Platform.OS === 'ios';
const statusBarHeight = isIphonexStyle ? getStatusBarHeight(true) +3 : StatusBar.currentHeight;

return (
<KeyboardAvoidingView
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
  style={{ 
    flex: 1, 
    backgroundColor: _inputValiation() ? 'red' : 'gray', 
    marginTop: isIphonexStyle ? statusBarHeight : 0
  }}
>
  <SafeAreaView style={isIphonexStyle ? { backgroundColor: 'transparent' } : {}}>
    <ScrollView style={{ flex: 1, backgroundColor: 'black' }} keyboardShouldPersistTaps='never'>
      ...(생략)
      
      <CustomInput 
        keyboardType='number-pad'
        placeholder='인증 코드를 입력해주세요'
      />
    </ScrollView>
    <CustomButton style={{ backgroundColor: _inputValiation() ? 'red' : 'gray' }}>다음</CustomButton>
  </SafeAreaView>
</KeyboardAvoidingView>
)

 

핵심은 behavior 부분의 변화이다.

안드로이드에서는 AndroidManifest.xml에

...

<activity 
  ...
  android:windowSoftInputMode="adjustResize" <-- 여기
>
...
</activity>

위의 한줄만 있으면 keyboardavoidingview에서

따로 처리를 안해도 적절하게 동작한다.

 

따라서 android에서는 undefined를.

ios에서는 권장사양이자 적절하게 동작하는

padding으로 설정을 해주었다.

 

나머지는 약간의 트릭을 통해

버튼이나 배경색 등을 컨트롤하는 부분이다.

반응형