웹사이트로 앱 만들기: 하이브리드앱 인앱 적용?
하이브리드앱을 통해 웹사이트를 앱으로 변환할 때,
인앱 구매 기능을 적용할 수 있는지에 대한 궁금증은 많은 개발자들과 웹사이트 운영자들이 갖고 있는 중요한 질문입니다.
-왜 하이브리드앱을 선택해야 하는지
-인앱 구매 기능의 필요성
-쉽게 인앱 구매 기능을 적용하는 방법에 대해 알아보겠습니다.
왜 하이브리드앱 인가요?
1.빠른 개발
하이브리드앱은 기존의 웹 기술을 활용하여 빠르게 앱을 개발할 수 있는 장점이 있습니다.
이는 특히 시간과 비용을 절약하려는 중소기업이나 개인 개발자들에게 매우 유리합니다.
예제 코드:
<!-- 기본적인 하이브리드앱 구조 -->
<!DOCTYPE html>
<html>
<head>
<title>My Hybrid App</title>
</head>
<body>
<h1>Hello, World!</h1>
<button onclick="showAlert()">Click Me!</button>
<script>
function showAlert() {
alert('This is a web alert!');
}
</script>
</body>
</html>
2. 다양한 웹 개발툴 및 빌더 활용 가능
하이브리드앱은 HTML, CSS, JavaScript 등 다양한 웹 개발툴을 활용할 수 있어 개발자들이 익숙한 환경에서 작업할 수 있습니다.
또한, 기존의 웹사이트를 재활용할 수 있어 작업의 효율성을 극대화할 수 있습니다.
3. 스토어 정책으로 인한 인앱 필수 적용
인앱을 꼭 적용해야 하는가? PG를 사용하면 되지 않는가?
구글 플레이스토어와 애플 앱스토어는 특정 디지털 상품의 판매에 대해 인앱 구매를 반드시 적용하도록 정책을 규정하고 있습니다.
이러한 정책을 준수하지 않을 경우, 앱이 스토어에서 거절되거나 삭제될 수 있습니다.
예제 코드:
// 예제: 인앱 구매 초기화
document.addEventListener('DOMContentLoaded', function() {
const products = ['product1', 'product2'];
products.forEach(product => {
console.log(`Product available: ${product}`);
});
});
어떻게 인앱을 쉽게 적용할 수 있을까요?
인앱은 반드시 네이티브로 구현해야 한다
인앱 구매 기능은 반드시 네이티브 코드로 구현되어야 합니다.
이는 보안성과 안정성 측면에서 중요한 요소로 작용합니다.
웹과 네이티브를 연동하는 방법
웹과 네이티브를 연동하는 방법에는 여러 가지가 있지만,
가장 일반적인 방법은 JavaScript와 네이티브 코드 간의 브릿지를 활용하는 것입니다.
이를 통해 웹 코드와 네이티브 코드 간의 통신을 원활하게 할 수 있습니다.
웹 코드 예제:
// 예제: 인앱 구매 실행
function buySwing2AppProduct(productId) {
// 스윙투앱 네이티브 인터페이스를 통한 인앱 구매 처리
if (window.SwAndroid) {
// 안드로이드 인앱 구매
window.SwAndroid.requestPay(productId);
} else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.iapHandler) {
// iOS 인앱 구매
window.webkit.messageHandlers.iapHandler.postMessage({productId: productId});
} else {
console.log('Swing2App interface not available.');
}
}
function onSuccessPurchase(data) {
console.log('Purchase successful:', data);
}
function onFailPurchase(error) {
console.log('Purchase failed:', error);
안드로이드 네이티브 코드 예제 (Java):
public class MainActivity extends AppCompatActivity implements PurchasesUpdatedListener {
private BillingClient billingClient;
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.addJavascriptInterface(new WebAppInterface(), "SwAndroid");
webView.loadUrl("file:///android_asset/index.html");
billingClient = BillingClient.newBuilder(this)
.setListener(this)
.enablePendingPurchases()
.build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
List<String> skuList = Arrays.asList("product_id_consume");
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
for (SkuDetails skuDetails : skuDetailsList) {
String sku = skuDetails.getSku();
String price = skuDetails.getPrice();
Log.d("Billing", "Sku: " + sku + " Price: " + price);
}
}
});
}
}
@Override
public void onBillingServiceDisconnected() {
}
});
}
@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
for (Purchase purchase : purchases) {
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
Log.d("Billing", "Consume successful");
runOnUiThread(() -> webView.evaluateJavascript("onSuccessPurchase('Purchase successful');", null));
}
}
});
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
runOnUiThread(() -> webView.evaluateJavascript("onFailPurchase('Purchase canceled');", null));
} else {
runOnUiThread(() -> webView.evaluateJavascript("onFailPurchase('Error: " + billingResult.getDebugMessage() + "');", null));
}
}
public class WebAppInterface {
@JavascriptInterface
public void requestPay(String productId) {
List<String> skuList = Arrays.asList(productId);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
for (SkuDetails skuDetails : skuDetailsList) {
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
billingClient.launchBillingFlow(MainActivity.this, flowParams);
}
}
}
});
}
}
}
iOS 네이티브 코드 예제 (Swift):
import UIKit
import WebKit
import StoreKit
class ViewController: UIViewController, WKScriptMessageHandler, SKProductsRequestDelegate, SKPaymentTransactionObserver {
var productID = "product_id_consume"
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
contentController.add(self, name: "iapHandler")
let config = WKWebViewConfiguration()
config.userContentController = contentController
webView = WKWebView(frame: self.view.bounds, configuration: config)
self.view.addSubview(webView)
if let url = Bundle.main.url(forResource: "index", withExtension: "html") {
webView.loadFileURL(url, allowingReadAccessTo: url)
}
SKPaymentQueue.default().add(self)
}
@objc func buyProduct(productId: String) {
if SKPaymentQueue.canMakePayments() {
let productRequest = SKProductsRequest(productIdentifiers: Set([productId]))
productRequest.delegate = self
productRequest.start()
} else {
print("User cannot make payments")
}
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
if let product = response.products.first {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction)
consumePurchase(transaction: transaction)
case .failed:
if let error = transaction.error as NSError? {
print("Transaction Failed: \(error.localizedDescription)")
webView.evaluateJavaScript("onFailPurchase('Purchase failed: \(error.localizedDescription)');", completionHandler: nil)
}
SKPaymentQueue.default().finishTransaction(transaction)
default:
break
}
}
}
func consumePurchase(transaction: SKPaymentTransaction) {
print("Consume successful for product: \(transaction.payment.productIdentifier)")
webView.evaluateJavaScript("onSuccessPurchase('Purchase successful');", completionHandler: nil)
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "iapHandler", let productId = message.body as? String {
buyProduct(productId: productId)
}
}
}
하이브리드앱으로 인앱을 구현하면 앱 심사에서는 문제 없을까요?
문제되지 않습니다.
구글과 애플은 앱 심사 과정에서 사용된 기술보다는 앱의 수익 모델과 결제 수단이 정책에 부합하는지에 중점을 둡니다.
따라서, 적절히 인앱 구매 기능이 구현되었다면 기술적인 부분에서는 큰 문제가 되지 않습니다.
하이브리드앱에서 인앱 구현에 대한 현실적인 어려움
위의 코드는 일회성 상품(consume) 에 대한 단순 구현 코드 입니다.
만약 구독 상품 및 실제 앱에 적용하려면 많은 내용이 추가되어야 합니다.
따라서 네이티브 코드를 구현할 수 있어야 하며 Native Language에 대한 지식과 Native Framework 에 대한 지식이 반드시 필요합니다.
하지만 대 부분의 하이브리드앱을 구현하는 사용자들은 웹개발자분들이기 때문에 이 부분에서 많은 어려움을 겪을 것으로 생각됩니다.
따라서 스윙투앱을 활용한 인앱 구현이 웹 개발자들에게는 좋은 대안이 될 수 있습니다.
스윙투앱 하이브리드 프로토타입에서는 인앱을 제공하나요?
네 제공합니다.
스윙투앱의 하이브리드 프로토타입은 인앱 구매 기능을 지원합니다.
JavaScript API를 통해 손쉽게 인앱 구매 기능을 구현할 수 있으며, 이는 웹 개발자들이 쉽게 접근할 수 있도록 설계되었습니다.
가장 중요한점은 네이티브코드를 전혀 구현할 필요가 없습니다.
웹코드 만으로 인앱을 구현할 수 있어, 웹 개발자들에게 높은 접근성을 제공하기 때문에 인앱 구현을 쉽게 적용할 수 있습니다.
스윙투앱에서 제공하는 API 를 활용한 인앱 구현 웹 코드 예시:
swingWebViewPlugin.app.inapp.buyAndType('인앱상품 아이디','상품유형',
function(responseCode,data) {
console.log('responseCode : ' + responseCode + ', ret : ' + JSON.stringify(data));
if( responseCode == 0 ) // 성공
{
var purchaseToken = data[0].purchaseToken;
var productId = data[0].productId;
}
else if( responseCode == 1 ) // 취소
{
// todo ( 사용자가 취소한 경우 처리하시면 됩니다. )
}
else // 기타 에러
{
// int SERVICE_TIMEOUT = -3;
// int FEATURE_NOT_SUPPORTED = -2;
// int SERVICE_DISCONNECTED = -1;
// int SERVICE_UNAVAILABLE = 2;
// int BILLING_UNAVAILABLE = 3;
// int ITEM_UNAVAILABLE = 4;
// int DEVELOPER_ERROR = 5;
// int ERROR = 6;
// int ITEM_ALREADY_OWNED = 7;
// int ITEM_NOT_OWNED = 8;
// todo ( 위의 에러 코드에 맞게 처리하시면 됩니다. )
}
})
결 론
하이브리드앱은 웹사이트를 앱으로 빠르게 변환할 수 있는 효율적인 방법입니다.
특히, 스윙투앱을 통해 인앱 구매 기능을 손쉽게 구현할 수 있기 때문에 개발자와 웹사이트 운영자들에게 큰 도움이 될 것입니다.
웹사이트를 앱으로 변환하면서 인앱 구매 기능을 추가하고자 한다면, 스윙투앱을 통해 쉽고 빠르게 구현해보세요.