티스토리 뷰
사용자가 스토어에서 앱을 설치할 때, 안심하고 앱을 다운받는데에 가장 중요한 지표가 되는 것은 단연 스토어 내 리뷰라고 볼 수 있다.
특히 내가 원하는 기능을 하는 앱을 검색하면 적어도 2개 이상은 비슷한 앱 리스트가 나오는데, 이럴 때 스토어 리뷰를 보고 앱 사용성이나 얼마나 많은 사용자들이 최근까지도 앱을 이용하고 있는지 미리 파악하고 다운받게 된다.
그런데 반대로 내가 어떠한 앱을 이용하고 있는 사용자일 때를 생각해보면, 서비스를 이용할 때에만 앱을 사용하지
이 앱을 사용하면서 만족감을 느꼈다 할지라도 스토어까지 찾아가서 리뷰를 작성했던 경험은 없었다.
단, 리뷰를 작성해달라고 자연스럽게 앱 내에서 팝업으로 요청이 오면 기쁜 마음으로 리뷰를 작성해주곤 했다.
자주 사용하는 앱에서 위와 같은 모달 팝업을 자주 경험해봤을 것이다.
이렇게 앱 내에서 스토어 리뷰를 요청하는 것을 인앱 리뷰(In-App Review) 라고 명칭하는데,
이번 포스팅에서는 React Native 환경에서 사용자에게 인앱 리뷰를 요청하는 방법을 알아보도록 하자.
📌 라이브러리 사용하기
인앱 리뷰는 굉장히 많은 서비스에서 사용되다보니 당연히 React Native용 npm 라이브러리가 배포되어있다.
https://www.npmjs.com/package/react-native-in-app-review
https://github.com/oblador/react-native-store-review
위에 링크한 두 라이브러리가 가장 많이 사용되는데,
개인적으로 사용한다면 최근까지도 커밋 내역이 있는 react-native-in-app-review를 추천한다.
라이브러리로 적용하는 방법은 사용법이 매우 간단하기에 README를 보고 참고하길 바란다.
📌 Native Module 작성하기
라이브러리를 사용하는 방법보단 간단하진 않지만,
알고보면 너무 간단하니 괜히 package.json에 한줄 추가하지 말고 이 방법을 따라해보길 추천한다.
1️⃣ Android 세팅하기
1. app/build.gradle dependencies 추가하기
review와 review-ktx 패키지를 사용하기 위해 아래와 같이 추가해준다.
...
dependencies {
...
// 인앱 리뷰
+ implementation 'com.google.android.play:review:2.0.1'
+ implementation 'com.google.android.play:review-ktx:2.0.1'
...
2. 자바 클래스 생성하기
APP 패키지 내, InAppReviewPackage.java 클래스를 생성해준다.
(경로를 헷갈려하시는 분들이 많은데, 패키지명이 com.test라면 app/java/com/test 경로에 생성해준다)
InAppReviewPackage.java
package com.test;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;
public class InAppReviewPackage implements ReactPackage{
@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
return Arrays.<NativeModule>asList(new InAppReviewModule(reactApplicationContext));
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
return Collections.emptyList();
}
}
같은 경로에 InAppReviewModule.java 모듈 클래스도 생성해준다.
InAppReviewModule.java
package com.test;
import android.app.Activity;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
import com.facebook.react.module.annotations.ReactModule;
import com.google.android.play.core.review.ReviewInfo;
import com.google.android.play.core.review.ReviewManager;
import com.google.android.play.core.review.ReviewManagerFactory;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
@ReactModule(name = InAppReviewModule.NAME)
public class InAppReviewModule extends ReactContextBaseJavaModule{
public static final String NAME = "InAppReview";
private final ReactApplicationContext reactContext;
public InAppReviewModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return NAME;
}
@ReactMethod
public void rate(final Callback callback) {
final ReviewManager manager = ReviewManagerFactory.create(this.reactContext);
Task<ReviewInfo> request = manager.requestReviewFlow();
request.addOnCompleteListener(new OnCompleteListener<ReviewInfo>() {
@Override
public void onComplete(@NonNull final Task<ReviewInfo> requestTask) {
if (requestTask.isSuccessful()) {
ReviewInfo reviewInfo = requestTask.getResult();
Activity activity = getCurrentActivity();
if (activity == null) {
callback.invoke(false, "getCurrentActivity() unsuccessful");
return;
}
Task<Void> flow = manager.launchReviewFlow(activity, reviewInfo);
flow.addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> flowTask) {
if (requestTask.isSuccessful()) {
callback.invoke(true);
} else {
callback.invoke(false, "launchReviewFlow() unsuccessful");
}
}
});
} else {
callback.invoke(false, "requestReviewFlow() unsuccessful");
}
}
});
}
}
3. 생성한 모듈 클래스 추가하기
MainApplication.java의 getPackages()에 아래와 같은 코드를 추가하여 생성한 모듈을 RN 패키지에 등록해준다.
MainApplication.java
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// 인앱리뷰 추가
+ packages.add(new InAppReviewPackage());
return packages;
}
여기까지 왔다면 Android 세팅은 끝났다. 이제 iOS 세팅으로 넘어가보자
2️⃣ iOS 세팅하기
1. XCode > 프로젝트 경로 내 InAppReview.h 헤더파일 생성하기
파일명은 InAppReview.h 로 작성해준다
#import <React/RCTBridgeModule.h>
#import <React/RCTConvert.h>
#import <UIKit/UIKit.h>
#import <StoreKit/StoreKit.h>
@interface InAppReview : NSObject <RCTBridgeModule>
@end
2. 같은 방식으로 InAppReview.m 파일을 생성해준다.
1.의 팝업창에서 Objc-C File(m이라고 적혀있는 아이콘) 을 선택해주면 된다
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import "InAppReview.h"
#import "React/RCTConvert.h"
@implementation InAppReview
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(rate: (RCTResponseSenderBlock) callback) {
NSString *AppleAppID = @"Your AppleAppID"; // Your AppleAppID
NSString *AppleNativePrefix = @"itms-apps://itunes.apple.com/app/id";
NSString *suffix = @"?action=write-review";
NSString *url = [NSString stringWithFormat:@"%@%@%@", AppleNativePrefix, AppleAppID, suffix];
if ([SKStoreReviewController class]) {
dispatch_async(dispatch_get_main_queue(), ^{
NSSet *scenes = [[UIApplication sharedApplication] connectedScenes];
NSArray *scenesArray = [scenes allObjects];
UIWindowScene *activeScene;
for (int i = 0; i < scenes.count; i++) {
UIWindowScene *scene = [scenesArray objectAtIndex:i];
NSInteger activationState = scene.activationState;
if (activationState == UISceneActivationStateForegroundActive) {
activeScene = scene;
}
}
if (activeScene != nil) {
[SKStoreReviewController requestReviewInScene:activeScene];
} else {
NSLog(@"No active scenes found. Cannot requestReviewInScene.");
}
});
}
}
@end
중간에 AppleAppID에는 iOS 프로젝트의 APP ID를 넣어주면 된다.
App Store Connect의 앱 정보 > 일반 정보 섹션에서 확인할 수 있다.
3️⃣ React Native 코드 작성하기
작성한 네이티브 모듈을 불러오는 함수를 작성해보자.
나는 utils/functions.ts 파일 내에 선언하였다.
functions.ts
import {Platform, NativeModules} from 'react-native';
type RateAppCallback = (response: any) => void;
export const rateApp = (callback: RateAppCallback): void => {
const {InAppReview} = NativeModules;
if (Platform.OS === 'ios') {
InAppReview?.rate((response: any, error: any) => {
callback({response, error});
});
} else if (Platform.OS === 'android') {
InAppReview?.rate((response: any, error: any) => {
callback({response, error});
});
}
};
이제 불러오고 싶은 화면이나 이벤트 함수에서 위 rateApp을 호출하면 바로 앱 리뷰 요청 팝업이 노출될 것이다.
⚠️ 주의사항
인앱리뷰를 사용자에게 요청할 때 Android, iOS에서는 다음과 같이 정책을 정해두었는데,
Android : 1번 노출 후 1개월 정도의 시간 뒤에 다시 노출 (정확한 시간은 구글의 마음)
iOS : 365일 내 최대 3번 정도 노출
디버깅 할 때에는 iOS는 횟수 제한은 없는 것 같고, 안드로이드는 1번 노출 후 다시 노출이 되지 않으므로 꼭 참고할 것.
그리고 공통적으로, 인앱 리뷰 팝업 내의 디자인을 포함한 레이아웃, 텍스트 등을 수정을 하면 안된다고 한다.
이렇게 NativeModule을 작성해서 In-App Review 요청 기능을 만드는 방법을 알아봤다.
생각보다는 간단했고 재밌던 경험이었다.
부족한 글이지만 꼭 이 글을 보는 모두에게 좋은 경험으로 작용했으면 좋겠다 😄
참고문서 : https://medium.com/@deepanshujain_33606/in-app-review-in-react-native-android-ios-18f06b95ae58
'개발 세상 > React Native' 카테고리의 다른 글
React Native Firebase Analytics Setting (4) | 2024.09.04 |
---|---|
navigation.goBack() 중복 호출 방지하기 (0) | 2024.08.29 |
[React Native] RN 앱에 TDD 적용하기 1 - React Native TDD PoC (2) | 2024.07.16 |
[RN/React Native] flatlist numColumns 로 래핑한 컴포넌트 gap 속성 쉽게 주기! (0) | 2024.06.12 |
[React Native/Mixpanel] RN에서 믹스패널로 손쉽게 사용자를 트래킹 해보자! | How Mixpanel makes it easy to track users (2) | 2024.06.11 |
- Total
- Today
- Yesterday
- vsC
- typeScript
- useEffect
- It
- 코린이
- FlatList
- gradle
- Xcode
- CSS
- 영종도데이트
- Ai
- JavaScript
- REACT
- IMAGE
- TS
- Firebase
- 스파르타코딩클럽
- ReactNative
- React Hooks
- Android
- Mac
- ios
- vscode
- rn
- ChatGPT
- build
- 앱개발
- useState
- app
- React Native
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |