- 프로젝트 설명
- 기술 스택
- 구현 핵심 기능 1 : 인앱 결제 오류 해결
- 구현 핵심 기능 2 : 플레이 스토어 배포를 위한 빌드 문제 해결
- Google Play Store URL
-
총 3개의 난이도로 수업 레벨이 구성되어 있고, 각 레벨에는 튜터와 각 튜터가 맡은 코스가 있습니다. 사용자는 원하는 코스를 선택하여 코스의 강의를 수강한 후, 퀴즈를 통해 본인의 한국어 능력을 시험해볼 수 있습니다. 또한, Premium 과정을 통해 튜터와의 1대1 Private Class를 수강할 수 있습니다.
-
구글 개발자 계정을 통해 완성된 앱을 플레이스토어의 심사를 거친 후에 배포하였습니다. 인앱 결제 기능을 구현하면서, JavaScript의 콜백함수, async 와 await 의 활용력을 기를 수 있었고, 실제 앱 배포를 통해 안드로이드 앱 개발에 필요한 빌드 능력을 갖추게 되었습니다.
React Native프레임워크를 활용하여 프론트엔드 개발 환경을 구축하였고,Node.js을 사용하여 백엔드 API 을 만들었고 클라이언트 환경과 통신하는 로직을 구현하였습니다.
- 인앱 결제 프로세스 구현을 위해
Google Play Console에Expo라이브러리를 통해 빌드한 앱 결과물을 등록하였습니다. 그 후, 결제 가능한 강의들을인앱 상품에 등록한 후,InAppPurchases기능과 연동하여 결제가 성공적으로 완료되도록 하였습니다.
default.mp4
인앱 결제 시연 영상
...
...
try {
// 구매 정보 가져오기
const { responseCode, results } = await getProductsAsync(itemArray);
if (responseCode === IAPResponseCode.OK) {
setProductId(results[0].productId)
setBottomText(results[0].productId);
} else {
setBottomText('something wrong!');
}
// 구매 내역에 없는 상품일때만 결제 진행
if(purchasedCourseList.indexOf(itemInfo.course_id) === -1) {
purchaseItemAsync(results[0].productId)
} else {
// 아닐떈, 바로 이미 구입한 상품이라고 알려주기
navigation.navigate("PaymentResult", {
user_id: userId,
itemInfo: itemInfo,
isSuccess: false,
returnToClass,
imgUrl: imgUrl,
isBought: true,
});
}
return await new Promise((resolve, reject) => {
setPurchaseListener(async (result) => {
if(result.responseCode === IAPResponseCode.OK){
setBottomText("success")
if(!result.results[0].acknowledged) {
setBottomText('successful purchase')
await finishTransactionAsync(result.results[0], false);
// 계속 구매 가능한지 test
// await finishTransactionAsync(result.results[0], true);
// DB에 저장 - purchasedCourse 에 없는 경우에만!
if(purchasedCourseList.indexOf(itemInfo.course_id) === -1) {
createPurchasedCourse(
{ user_id: userId, course_id: itemInfo.course_id },
(d) => {
navigation.navigate("PaymentResult", {
user_id: userId,
itemInfo: itemInfo,
isSuccess: true,
returnToClass,
imgUrl: imgUrl,
});
},
setIsCoursePurchased,
(e) => {
setIsSuccess(false);
console.log(e.message);
}
);
} else {
navigation.navigate("PaymentResult", {
user_id: userId,
itemInfo: itemInfo,
isSuccess: true,
returnToClass,
imgUrl: imgUrl,
});
}
}
} else if (result.responseCode === IAPResponseCode.USER_CANCELED || result.responseCode === IAPResponseCode.DEFERRED) {
setBottomText('User canceled the transaction');
} else {
setBottomText(`Something went wrong with the purchase. Received errorCode ${result.errorCode}`);
navigation.navigate("PaymentResult", {
user_id: userId,
itemInfo: itemInfo,
isSuccess: false,
returnToClass,
imgUrl: imgUrl,
isBought: result.errorCode === 8 ? true : false,
});
}
})
})
} catch(e) {
disconnectAsync();
setBottomText('error!!!!!');
}
...
...인앱 결제 로직 구현 코드
-
Expo의 InAppPurchases 라이브러리를 사용하여 인앱 결제 로직을 구현하기로 했지만, 구글 개발자 계정내에 등록된 인앱 결제 상품을 연결하는 절차까지만 성공하고 막상 결제는 계속해서 실패하였습니다. API 문서를 다시 한 번 정독하고, 코드 흐름을 꼼꼼하게 살펴본 결과상품을 구매하기 전에 필수적으로 불러야하는 콜백함수의 위치가 특정 화면 단이 아니라 전역 상태로 있어야 하는 사실을 깨달았습니다. -
위치를 옮기고나서, 구매 후의 결과 처리가 제대로 되지 않으면 향후 구매가 취소될 수 있기 때문에 이에 대한 코드를 다시 한 번 확실히 검토하였습니다. 결제 형태가 구독인지, 아니면 일회성 상품인지에 따라서도 다른 처리를 해줘야하기 때문에, 이를 잘 고려하여 최종적으로 기능 구현을 완료하였습니다.
-
인앱 상품 불러오기 부터 어떤 상품인지에 대한 확인 및 결제 후 처리까지 구현해보면서
인앱 결제의 전체적인 프로세스잘 이해할 수 있었고, 향후 결제 관련 API 들을 사용할 때에도 이 경험을 잘 살려서 적용해볼 예정입니다.
- 플레이 스토어에 등록 가능한
안드로이드 앱형태로 빌드하기 위해Expo EAS Build를 통해 프로젝트를 빌드하려 했지만, 다음과 같은 문제가 발생하였습니다.
-
Plugin with id 'maven' not found.:maven plugin이 제거되어,maven-publish플러그인을 대신 사용하도록 코드를 수정하였습니다. ->apply plugin: maven-publish코드를 적용 -
compiledSdkVersion is not specified:build.gradle의buildscript블럭 내의 코드를 수정하였습니다.
...
...
android {
ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId 'com.candykorean.candykoreanapp'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 42
versionName "1.0.3"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
// missingDimensionStrategy "store", "play"
if (isNewArchitectureEnabled()) {
// We configure the CMake build only if you decide to opt-in for the New Architecture.
externalNativeBuild {
cmake {
arguments "-DPROJECT_BUILD_DIR=$buildDir",
"-DREACT_ANDROID_DIR=${reactNativeRoot}/ReactAndroid",
"-DREACT_ANDROID_BUILD_DIR=${reactNativeRoot}/ReactAndroid/build",
"-DNODE_MODULES_DIR=$rootDir/../node_modules",
"-DANDROID_STL=c++_shared"
}
}
if (!enableSeparateBuildPerCPUArchitecture) {
ndk {
abiFilters (*reactNativeArchitectures())
}
}
}
}
...
...build.gradle -> android 코드 블럭 내 compiledSdkVersion 추가



