리뷰 요청 기능을 추가한 이유
앱 출시 후 약 6개월이 지났지만, 아직 앱 스토어에 작성된 리뷰가 없다.
리뷰가 하나라도 있으면 앱의 신뢰도를 높이는 데 도움이 될 것 같다는 생각이 들었다.
그래서 앱 내에 리뷰 요청 기능을 추가해서 적극적으로 리뷰 작성을 유도하기로 했다.
리뷰를 요청하는 두 가지 방법
인앱 리뷰 API 사용하기


안드로이드와 iOS 모두 인앱 리뷰를 요청할 수 있는 API를 제공한다.
UI를 따로 구성할 필요가 없기 때문에 가장 간단하게 구현할 수 있는 방식이지만
OS별로 제한사항이 많기 때문에, 사용자 경험을 커스터마이징 하기 어렵다.
앱은 사용자에게 평점 버튼 또는 카드를 표시하기 전이나 표시하는 동안 사용자 의견 관련 질문 (예: '앱이 마음에 드십니까?') 또는 예측 질문 (예: '이 앱을 별 5개로 평가하시겠습니까?')을 포함하여 어떤 질문도 해서는 안 됩니다.
안드로이드의 경우에는 인앱 리뷰를 요청하기 전에 어떤 질문도 해서는 안 되고, 한 달에 2번 등 할당량이 정해져 있다.
사람들이 앱 또는 게임을 직접 체험하고 난 다음에만 평가를 요청하십시오.
사람들이 작업을 수행 중이거나 게임을 플레이하는 도중에 방해하는 것을 피하십시오.
반복적인 요청을 피하십시오.
가급적 시스템 제공 요청을 사용하십시오.
iOS의 경우에는 1년에 최대 3번까지만 인앱 리뷰를 요청할 수 있고, 상세한 가이드라인을 제시한다.
직접 구현하기

커스텀 다이얼로그를 노출시켜서 앱스토어 링크로 이동시킬 수도 있다.
이 경우에는 리뷰를 요청하기 전에 리뷰 작성을 유도하는 문구를 자유롭게 작성할 수 있지만
리뷰 페이지로 이동해야 한다는 점에서 인 앱 리뷰에 비해서 접근성이 떨어진다.
리뷰 요청 전략 설계 및 주의사항
사용자에게 리뷰를 요청할 때는 다음과 같은 사항을 고려해야 한다.
1. 언제 요청할까?
- 사용자들이 앱을 충분히 사용하지 못한 상황에서 앱 평가를 요청하면 부정적인 평가를 남길 가능성이 크기 때문에 앱 설치 직후에 요청하는 것은 좋지 않다.
- 사용자들이 앱 기능을 충분히 사용한 뒤에 요청하는 것이 좋다.
- 기능 사용 직후 등 사용자가 성취감이나 만족감을 느꼈을 때 요청하는 것이 가장 좋다.
- 사용자가 작업을 수행 중일 때 리뷰 요청을 하는 등 사용자 경험을 방해해서는 안 된다.
2. 얼마나 자주 요청할까?
- 반복되는 평가 요청은 오히려 앱에 대한 사용자들의 의견에 부정적인 영향을 미칠 수 있다.
- 최소 1~2주의 간격을 두고, 사용자들이 추가적으로 앱을 경험한 다음에 요청을 하는 것이 좋다.
리뷰 요청 로직
리뷰 요청 방식은 크게 두 가지로 나눌 수 있다.

첫 번째는 사용자에게 [지금 리뷰 작성하기] 또는 [나중에] 두 가지 선택지만 제공하는 방식이다.
나는 간단하게 구현하기 위해 이 방식을 사용했다.

두 번째는 앱에 대한 만족도를 먼저 질문하고, 사용자의 반응에 따라 각기 다른 행동을 유도하는 방식이다.
예를 들어 "앱이 만족스러우셨나요?"라는 질문에
"네"를 선택하면 리뷰 작성을 요청하고,
"아니오"를 선택하면 피드백 작성 페이지로 안내한다.
이 방식을 사용하면 긍정적인 이용 경험을 가진 사용자에게 높은 평점의 리뷰를 받을 확률이 높아지고,
불편함을 느낀 사용자에게는 직접 피드백을 받아 앱을 개선할 수 있는 기회가 생긴다는 장점이 있다.
실제 기능 구현
리뷰 요청 조건
교정일기 앱에서는 타임라인 탭에서 치료 내역 작성, 앨범 탭에서 치아 사진을 업로드할 수 있는데
타임라인+사진을 더해서 5건 이상 작성했을 때 최초로 리뷰 요청 다이얼로그를 노출하기로 했다.
구현 코드
// 상태 저장 (SharedPreferences)
class ReviewPrefs {
final bool? reviewCompleted; // 리뷰 작성 완료 여부 - [리뷰 작성] 버튼 클릭만 해도 작성한 것으로 간주함
final DateTime? lastRejectedAt; // 마지막으로 [나중에] 버튼 클릭한 시간
ReviewPrefs({
required this.reviewCompleted,
required this.lastRejectedAt,
});
}
`reviewCompleted`가 `true`가 아닌 경우에만 리뷰 요청 다이얼로그를 노출한다.
`lastRejectedAt`은 마지막 다이얼로그 노출 시점으로부터 한 달이 지났는지를 계산하기 위해 사용된다.
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ReviewRequest {
// 글 작성할 때마다 리뷰 요청 조건 확인
Future<bool> shouldShowReviewDialog() async {
final reviewPrefs = await getReviewPrefs();
final reviewCompleted = reviewPrefs.reviewCompleted;
final lastRejectedAt = reviewPrefs.lastRejectedAt;
// 이미 리뷰 작성 완료한 경우
if (reviewCompleted == true) {
return false;
}
// 30일 이내에 '나중에 보기'를 눌렀는지 확인
if (lastRejectedAt != null) {
final daysSince = DateTime.now().difference(lastRejectedAt).inDays;
if (daysSince < 30) return false; // 아직 한 달 안 지남
}
// 일정 횟수 이상 앱 사용 조건 충족 여부 확인
final hasEnoughActivity = await hasEnoughReviewActivity();
if (!hasEnoughActivity) return false;
return true;
}
// 타임라인 + 치아사진 5건 이상 작성했는지 확인
Future<bool> hasEnoughReviewActivity() async {
final timelineCount = Get.find<TimelineController>().timelineData.length;
final photoCount = Get.find<TeethPhotoController>().teethPhotoData.length;
return (timelineCount + photoCount) >= 5;
}
// [리뷰 작성] 버튼 클릭 시 리뷰 작성 완료 처리
Future<void> markReviewCompleted() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('review_completed', true);
}
// [나중에] 버튼 선택 시 마지막 요청 시간 업데이트
Future<void> updateReviewRejecteddDate() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('last_rejected_at', DateTime.now().toIso8601String());
}
}
아직 리뷰를 작성하지 않았고,
마지막 리뷰 요청 시점으로부터 한 달이 지났고,
게시물을 5건 이상 작성했다는 3가지 조건을
모두 충족한 경우에만 리뷰 요청 다이얼로그가 노출된다.
// 리뷰 요청 다이어로그
Future<void> showReviewDialog(BuildContext context) async {
final reviewRequest = ReviewRequest();
return showDialog(
context: navigatorKey.currentContext!,
builder: (BuildContext context) {
return AlertDialog(
title: const Text(
"교정일기가 도움이 되었나요?",
),
content: const Text(
"앱을 이용해 주셔서 감사합니다. 잠시 시간을 내어 리뷰를 남겨 주시면 앱을 지속적으로 업데이트하는 데 큰 힘이 됩니다!",
),
actions: [
ElevatedButton(
// 나중에 버튼 클릭 시 마지막 요청 시간 업데이트
onPressed: () {
reviewRequest.updateReviewRejecteddDate();
Navigator.of(context).pop();
},
child: const Text("나중에"),
),
FilledButton(
// 리뷰 작성 버튼 클릭 시 앱스토어 이동 및 리뷰 작성 완료 처리
onPressed: () async {
final uri = Uri.parse(googleStoreUrl);
await launchUrl(uri);
reviewRequest.markReviewCompleted();
reviewRequest.updateReviewRejecteddDate();
Navigator.of(context).pop();
},
child: const Text("리뷰 작성하기"),
),
],
);
},
);
}
사용자가 [리뷰 작성하기] 버튼을 누르면 앱 스토어 페이지로 이동하고,
리뷰를 작성한 것으로 간주하여 더 이상 다이얼로그를 노출하지 않는다.
(개인 정보 보호와 정책상 이유로 사용자의 리뷰 작성 여부를 확인할 수 있는 API를 제공하지 않는다고 한다...)
[나중에] 버튼을 누르면 마지막 요청 시간을 업데이트하고, 한 달 후에 다시 리뷰 작성을 요청한다.
// 새로 작성하는 경우에만 리뷰 요청 조건 확인: 탭 이동 후 1초 뒤에
if (widget.photo == null) {
Future.delayed(const Duration(seconds: 1), () async {
final reviewRequest = ReviewRequest();
if (await reviewRequest.shouldShowReviewDialog()) {
await showReviewDialog();
}
});
}
사용자가 게시물을 새로 작성할 때마다
`shouldShowReviewDialog()`를 실행해서 다이얼로그 노출 여부를 결정한다.
글 작성이 완료되면 스크린을 이동하고
1초 후에 리뷰 요청 조건을 확인해서 다이얼로그가 노출되도록 했다.
결과물

리뷰 요청 기능이 완성된 모습이다!
(iOS는 리뷰 작성 섹션으로 바로 이동할 수 있는 링크가 있는 반면에 안드로이드는 제공하지 않는 것 같다...)
참고 문서
- [Apple Developer] 평가 및 리뷰
- [Android Developers] Google Play In-App Review API
- 앱 스토어 리뷰 별점 올리는 방법
- [iOS] 유저에게 앱스토어 리뷰 요청을 해보자
- [Android/IOS] 인앱 리뷰 기능 추가하기
- 스토어 리뷰 별점 어떻게 올릴까