AppInToss SDK
Integrate Unity WebGL builds into Toss app webviews with type-safe bidirectional communication between C# and TypeScript. Built on Protobuf and WebView-RPC, this SDK provides high-level abstractions for ads, in-app purchases, device information, and storage, with automatic code generation via GitHub Actions.
appintoss-unity-webapp 
Install via UPM
Add to Unity Package Manager using this URL
https://www.pkglnk.dev/ait-sdk.git?path=webapp README Markdown
Copy this to your project's README.md
## Installation
Add **AppInToss SDK** to your Unity project via Package Manager:
1. Open **Window > Package Manager**
2. Click **+** > **Add package from git URL**
3. Enter:
```
https://www.pkglnk.dev/ait-sdk.git?path=webapp
```
[](https://www.pkglnk.dev/pkg/ait-sdk)README
AIT Unity SDK (Toss ์ฐ๋ ํ ํ๋ฆฟ)
๐ ๊ฐ์
์ด ํ๋ก์ ํธ๋ Unity WebGL ๋น๋๋ฅผ ํ ์ค ์ฑ ๋ด ์น๋ทฐ์์ ์คํ๋๋ React ์น์ฑ์ ํตํฉํ๊ธฐ ์ํ ํ
ํ๋ฆฟ ๋ฐ ๊ฐ์ด๋์
๋๋ค. app-webview-rpc๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Protobuf๋ฅผ ์ฌ์ฉํ์ฌ Unity(C#)์ React(TypeScript) ๊ฐ์ ํ์
-์ธ์ดํ(type-safe)ํ ์๋ฐฉํฅ ํต์ ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ ์ํฉ๋๋ค.
์ด ์ ์ฅ์๋ ๋ณ๋์ npm ํจํค์ง๋ก ์ ๊ณต๋ ๊ณํ์ ์์ผ๋ฉฐ, ์ ์ฒด๋ฅผ ํด๋ก ํ์ฌ ์ฌ์ฉํ๊ฑฐ๋ ํ์ํ ๋ถ๋ถ์ ์ฐธ๊ณ ํ์ฌ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
โจ ์ฃผ์ ๊ธฐ์
- Unity (C#): ๊ฒ์ ํด๋ผ์ด์ธํธ
- React (TypeScript): ์น ํ๋ก ํธ์๋ ๋ฐ ํ ์ค ์ฑ ๋ธ๋ฆฟ์ง ์ฐ๋
- Protobuf: Unity-React ๊ฐ ํต์ ์ ์ํ ์คํค๋ง ์ ์
- WebView-RPC: Protobuf ๊ธฐ๋ฐ์ RPC ํ๋ ์์ํฌ
- GitHub Actions: Protobuf ํ์ผ ๋ณ๊ฒฝ ์ C# ๋ฐ TypeScript ์ฝ๋๋ฅผ ์๋ ์์ฑ
๐ ์์ํ๊ธฐ
1. ์น์ฑ ์ค์ (React)
ํ๋ก์ ํธ ๊ฐ์ ธ์ค๊ธฐ ์ด ์ ์ฅ์ ์ ์ฒด๋ฅผ ํด๋ก ํ๊ฑฐ๋,
webapp์๋ธ ํด๋๋ง ๋ณต์ฌํ์ฌ ๊ธฐ์กด React ํ๋ก์ ํธ์ ํตํฉํ ์ ์์ต๋๋ค.# webapp ํด๋๋ก ์ด๋ cd webapp # ์์กด์ฑ ์ค์น npm install์ค์ ํ์ผ ์์
webapp/granite.config.tsํ์ผ์ ์ด์ด ์์ ์ ํ ์ค ์ฑ ์ค์ ์ ๋ง๊ฒappId,displayName๋ฑ์ ์์ ํฉ๋๋ค.[์ ํ] UI ์ปค์คํฐ๋ง์ด์ง ํ์์ ๋ฐ๋ผ
webapp/src/App.tsxํ์ผ์ ์์ ํ์ฌ ์ํ๋ UI์ ๊ธฐ๋ฅ์ ๊ตฌ์ฑํ ์ ์์ต๋๋ค.Unity ๋น๋ ๊ฒฐ๊ณผ๋ฌผ ์ฐ๋
- Unity์์ WebGL ๋น๋๋ฅผ ์๋ฃํฉ๋๋ค.
- ๋น๋ ๊ฒฐ๊ณผ๋ฌผ 4๊ฐ (
.data,.framework,.loader,.wasm) ํ์ผ์webapp/public/assets/ํด๋์ ๋ณต์ฌํฉ๋๋ค. webapp/src/App.tsxํ์ผ ๋ด์์ Unity ๋น๋ ํ์ผ๋ช ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ง์ ๋์๋์ง ํ์ธํ๊ณ , ๋ค๋ฅผ ๊ฒฝ์ฐ ์์ ํฉ๋๋ค.
2. Unity SDK ์ค์
NuGetForUnity ์ค์น Unity ํ๋ก์ ํธ์์
GlitchEnzo/NuGetForUnity๋ฅผ Unity Package Manager๋ฅผ ํตํด ์ค์นํฉ๋๋ค.- Package Manager > "Add package from git URL..." ์ ํ
https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src์ ๋ ฅ
Google.Protobuf ์ค์น
- NuGetForUnity ์ค์น ํ ์๋จ ๋ฉ๋ด
NuGet>Manage NuGet Packages๋ฅผ ์ฝ๋๋ค. Google.Protobuf๋ฅผ ๊ฒ์ํ์ฌ ์ต์ ๋ฒ์ ์ ์ค์นํฉ๋๋ค.
- NuGetForUnity ์ค์น ํ ์๋จ ๋ฉ๋ด
AIT-SDK ํจํค์ง ์ค์น
OpenUPM-CLI๊ฐ ์ค์น๋์ด ์์ด์ผ ํฉ๋๋ค. (npm install -g openupm-cli)- ํ๋ก์ ํธ ๋ฃจํธ์์ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์คํํฉ๋๋ค. (์์กด์ฑ์ ์๋์ผ๋ก ํด๊ฒฐํด์ค๋๋ค.)
openupm add com.kwanjoong.ait-sdkAitSdkBridge ์ค์
- Unity์ ์์ ์ฌ(Scene)์ ์ต์์(root)์
AitSdkBridge๋ผ๋ ์ด๋ฆ์ผ๋ก ๋น ๊ฒ์ ์ค๋ธ์ ํธ๋ฅผ ์์ฑํฉ๋๋ค. (์ด๋ฆ์ด ์ ํํด์ผ ํฉ๋๋ค.) - ์์ฑ๋ ๊ฒ์ ์ค๋ธ์ ํธ์
AitRpcBridge์ปดํฌ๋ํธ๋ฅผ ๋ถ์ฐฉํฉ๋๋ค. - ์ด ์ค๋ธ์ ํธ๋ ๊ฒ์ ์์ ์ ์๋์ผ๋ก
DontDestroyOnLoad๋ก ์ ํ๋์ด ๊ฒ์ ์ธ์ ๋์ RPC ํต์ ์ ๊ด๋ฆฌํฉ๋๋ค.
- Unity์ ์์ ์ฌ(Scene)์ ์ต์์(root)์
๐ SDK ์ฌ์ฉ๋ฒ
๋ชจ๋ ๊ธฐ๋ฅ์ AitRpcBridge ์ฑ๊ธํค์ ํตํด ์ ๊ทผํฉ๋๋ค.
// AitRpcBridge๋ ์ฑ๊ธํค ์ธ์คํด์ค๋ฅผ ์ ๊ณตํฉ๋๋ค.
var ads = AitRpcBridge.Instance.Ads; // ๊ณ ์์ค ๊ด๊ณ ์ ์ฆ์ผ์ด์ค
var iaps = AitRpcBridge.Instance.Iaps; // ๊ณ ์์ค IAP ์ ์ฆ์ผ์ด์ค
var storageService = AitRpcBridge.Instance.StorageService; // ์ ์์ค RPC ํด๋ผ์ด์ธํธ (ํ์ ์)
๋ ์ด์ด ์๋ด
- UseCase Layer (๊ถ์ฅ) โ
Ads,Iaps์ฒ๋ผ ํ๋ก์ ํธ์์ ๋ฐ๋ก ํธ์ถํ ์ง์ ์ ์ ๋๋ค. ํ ์ค ์ ์ฑ ์ค์ ๋ก์ง์ด ํฌํจ๋ ๋ชจ๋ฒ ๊ตฌํ์ ๋๋ค.- Generated RPC Layer โ
AdServiceClient,IapServiceClient๋ฑ ์๋ ์์ฑ๋ Stub. ํน์ํ ์ปค์คํฐ๋ง์ด์ง์ด ํ์ํ ๋๋ง ์ฌ์ฉํ์ธ์.- Extension Methods โ RPC ํธ์ถ ํจํด์ ๋จ์ํํ Helper (
ShowAdAsStream๋ฑ). ๊ณ ๊ธ ์น์ ์์ ์๊ฐํฉ๋๋ค.
DeviceService (๊ธฐ๊ธฐ ์ ๋ณด)
Safe Area (์์ ์์ญ) ์ ์ฉ
SafeAreaManager๊ฐ ๊ฒ์ ์์ ์ ์๋์ผ๋ก ๊ธฐ๊ธฐ์ Safe Area ๊ฐ์ ๊ฐ์ ธ์ ์บ์ฑํฉ๋๋ค. ์ค์ UI์ ์ ์ฉํ๋ ค๋ฉด, Safe Area๋ฅผ ์ ์ฉํ UI Panel ๊ฒ์ ์ค๋ธ์ ํธ์ SafeAreaPanel ์ปดํฌ๋ํธ๋ฅผ ๋ถ์ฐฉํ๊ธฐ๋ง ํ๋ฉด ๋ฉ๋๋ค.
- ๋งค๋์ ์ค์ : ์์ ์ฌ์
AitSdkBridge๋๋ ๋ค๋ฅธ ๋งค๋์ ์ค๋ธ์ ํธ์SafeAreaManager์ปดํฌ๋ํธ๋ฅผ ๋ถ์ฐฉํฉ๋๋ค. - ํจ๋ ์ ์ฉ: ์์ ์์ญ์ ์ ์ฉํ ๋ชจ๋ UI Panel์
SafeAreaPanel์ปดํฌ๋ํธ๋ฅผ ๋ถ์ฐฉํฉ๋๋ค.
StorageService (์๊ตฌ ์ ์ฅ์)
ํ ์ค ์ฑ ๋ด์์ Key-Value ๊ธฐ๋ฐ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์๊ตฌ์ ์ผ๋ก ์ ์ฅํ๊ณ ๋ถ๋ฌ์ต๋๋ค.
using AitBridge.RPC;
using AIT.AIT_SDK.ExtensionMethods; // ํ์ฅ ๋ฉ์๋ using ํ์!
using Cysharp.Threading.Tasks;
// ๋ฐ์ดํฐ ์ ์ฅ
await AitRpcBridge.Instance.StorageService.SetItem(new () { Key = "BestScore", Value = "100" });
// ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ
var response = await AitRpcBridge.Instance.StorageService.GetItem(new () { Key = "BestScore" });
string score = response.Value; // "100"
// ๋ฐ์ดํฐ ์ญ์
await AitRpcBridge.Instance.StorageService.RemoveItem(new () { Key = "BestScore" });
// ์ ์ฒด ๋ฐ์ดํฐ ์ญ์
await AitRpcBridge.Instance.StorageService.ClearItems();
IAP (AppsInTossIapUseCase)
AitRpcBridge.Instance.Iaps๋ฅผ ํตํด ์ ๊ทผํฉ๋๋ค. ๋ฆฌ๋ชจํธ ์นดํ๋ก๊ทธ, ์ปค์คํ
Spot ์ ๋ณด, ๊ฒฐ์ ํ๋ก์ฐ๋ฅผ ํ ๊ณณ์์ ์ฑ
์์ง๋๋ค.
using AIT.AIT_SDK.Bridge;
using Cysharp.Threading.Tasks;
public class ShopPanel : MonoBehaviour
{
public async UniTask InitializeAsync()
{
// 1) Toss ๋ฆฌ๋ชจํธ ์นดํ๋ก๊ทธ๋ก ์์ ๋ฆฌ์คํธ ๋ ๋๋ง
var catalog = await AitRpcBridge.Instance.Iaps.GetRemoteCatalogAsync();
RenderStore(catalog);
// 2) ํน์ Spot(์: remove_ads_button) ๋
ธ์ถ
var curated = await AitRpcBridge.Instance.Iaps.GetCuratedProductAsync("remove_ads_button");
if (curated != null)
{
RenderFeaturedTile(curated.Value);
}
}
public async UniTask PurchaseRemoveAdsAsync()
{
var result = await AitRpcBridge.Instance.Iaps.PurchaseCuratedSpotAsync("remove_ads_button");
if (result.IsSuccess)
{
UnlockRemoveAds();
}
else
{
ShowPurchaseError(result.ErrorEvent?.ErrorMessage);
}
}
}
UseCase๋ ๋ด๋ถ์ ์ผ๋ก Toss GetProductItemList / CreateOneTimePurchaseOrder / PollPurchaseEvents๋ฅผ ํธ์ถํ๊ณ , ScriptableObject(AppsInTossMonetizationConfig.asset)์ ๋ฑ๋ก๋ SKU ์ ๋ณด๋ฅผ ์ฐธ๊ณ ํฉ๋๋ค.
๐ง ๊ณ ๊ธ: ๋ก์ฐ ๋ ๋ฒจ RPC ์ง์ ์ฌ์ฉ
using Ait.Iap;
using AitBridge.RPC;
using AIT.AIT_SDK.ExtensionMethods;
using Cysharp.Threading.Tasks;
public class IapTest : MonoBehaviour
{
public async UniTask TestPurchase(string sku)
{
var request = new CreateOneTimePurchaseOrderRequest { Sku = sku };
try
{
await foreach (var ev in AitRpcBridge.Instance.IapService.CreateOrderAsStream(request))
{
switch (ev.EventCase)
{
case PurchaseEvent.EventOneofCase.Success:
Debug.Log($"Purchase Success! Order ID: {ev.Success.OrderId}");
break;
case PurchaseEvent.EventOneofCase.Error:
Debug.LogError($"Purchase Error! Code: {ev.Error.ErrorCode}, Msg: {ev.Error.ErrorMessage}");
break;
}
}
Debug.Log("Purchase flow finished.");
}
catch (Exception e)
{
Debug.LogError($"Purchase stream failed: {e.Message}");
}
}
}
Ads (AppsInTossAdUseCase)
AitRpcBridge.Instance.Ads๊ฐ ๊ด๊ณ ํธ์ถ๊ณผ Toss ๊ท์ ์ค์๋ฅผ ๋ชจ๋ ๋ด๋นํฉ๋๋ค.
using AIT.AIT_SDK.Bridge;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class RewardedButton : MonoBehaviour
{
public async void OnClick()
{
var result = await AitRpcBridge.Instance.Ads.ShowRewardedAsync();
if (result.IsRewardGranted)
{
GrantReward();
}
else if (result.Status == AppsInTossAdStatus.FailedToShow)
{
ShowRetryPopup();
}
}
}
๊ด๊ณ ์ฌ์ ์ค Time.timeScale/AudioListener.pause๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง๋ AppsInTossMonetizationConfig์ ํ ๊ธ์ ๋ฐ๋ผ ์๋์ผ๋ก ๊ฒฐ์ ๋ฉ๋๋ค. Toss WebView๊ฐ ์จ๊ฒจ์ก์ ๋์ ์ฒ๋ฆฌ ์ญ์ ๊ฐ์ ์ค์ ์ ๋ฐ๋ฆ
๋๋ค.
๐ง ๊ณ ๊ธ: ๋ก์ฐ ๋ ๋ฒจ RPC ์ง์ ์ฌ์ฉ
using Ait.Ad;
using AitBridge.RPC;
using AIT.AIT_SDK.ExtensionMethods;
using Cysharp.Threading.Tasks;
public class AdTest : MonoBehaviour
{
public async UniTask TestShowAd(string adGroupId)
{
var request = new ShowAdRequest { AdGroupId = adGroupId };
try
{
await foreach (var ev in AitRpcBridge.Instance.AdService.ShowAdAsStream(request))
{
switch (ev.EventCase)
{
case ShowAdEvent.EventOneofCase.UserEarnedReward:
Debug.Log($"User Earned Reward! Type: {ev.UserEarnedReward.UnitType}, Amount: {ev.UserEarnedReward.UnitAmount}");
break;
case ShowAdEvent.EventOneofCase.Dismissed:
Debug.Log("Ad was dismissed.");
break;
case ShowAdEvent.EventOneofCase.FailedToShow:
Debug.Log("Ad failed to show.");
break;
}
}
Debug.Log("Ad flow finished.");
}
catch (Exception e)
{
Debug.LogError($"ShowAdAsStream failed: {e.Message}");
}
}
}
๐ฐ ์์ตํ ๊ตฌ์ฑ (Ads & IAP)
1. AppsInTossMonetizationConfig (ScriptableObject)
Assets/AppsInTossMonetizationConfig.asset์ ๊ด๊ณ ๊ทธ๋ฃน ID์ ์ปค์คํ
IAP ๋
ธ์ถ์ ํ ๊ณณ์์ ๊ด๋ฆฌํ๋ ์ค์ ์ค์ ์
๋๋ค.
- Ad Group IDs
Interstitial Ad Group Id: ๋ชจ๋ ์ค๊ฐ ๊ด๊ณ ๊ฐ ์ฌ์ฉํ TossadGroupId.Rewarded Ad Group Id: ๋ชจ๋ ๋ณด์ํ ๊ด๊ณ ์adGroupId.
- Curated IAP Spots (Spot ๋ฆฌ์คํธ๋ ์ํ๋ ๋งํผ ์ถ๊ฐ ๊ฐ๋ฅ)
Spot ID: ๊ฒ์ ์ฝ๋๊ฐ ์ฐธ์กฐํ๋ ๋ก์ปฌ ์๋ณ์ (remove_ads_button,gem_shop_featured๋ฑ).Product ID: Toss ๋์๋ณด๋์์ ๋ฐ๊ธ ๋ฐ์ SKU. ๊ฒฐ์ ์์ฒญ ์ ์ด ๊ฐ์ด ์ ๋ฌ๋ฉ๋๋ค.Icon: Toss์์ ๋ด๋ ค์ฃผ๋ ์ด๋ฏธ์ง ๋์ ํด๋ผ์ด์ธํธ๊ฐ ์ฐ๊ณ ์ถ์ ์คํ๋ผ์ดํธ.Title Override: ๋ ธ์ถ๋ช ์ปค์คํฐ๋ง์ด์ง (๋ฏธ์ ๋ ฅ ์ Tossdisplay_name์ฌ์ฉ).Subtitle: ๋ณด์กฐ ์ค๋ช (โ๊ด๊ณ ์ ๊ฑฐโ, โ๋ฒ ์คํธ ๋ฐธ๋ฅโ ๋ฑ).Call To Action Override: ๋ฒํผ ๋ผ๋ฒจ ์ง์ (โ๊ตฌ๋งคโ, โ์ถฉ์ ํ๊ธฐโ ๋ฑ).Highlight Color: ์นด๋/๋ฑ์ง์ ์ฌ์ฉํ ํฌ์ธํธ ์์.
- Playback Behavior Toggles
Pause Time During Ads,Mute Audio During Ads: ๊ด๊ณ ์ฌ์ ๋์Time.timeScale๊ณผ ์ค๋์ค๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง ์ ํ.Pause Time When Host Hidden,Mute Audio When Host Hidden: ์ฌ์ฉ์๊ฐ ํ ๋ฒํผยท์ ๊ธ ๋ฑ์ผ๋ก WebView ํ๋ฉด์ ๋ฒ์ด๋ฌ์ ๋์ ์ฒ๋ฆฌ.
์ฐธ๊ณ : ๊ด๊ณ /IAP์ฉ ์ ์์ค RPC ํด๋ผ์ด์ธํธ(
AdServiceClient,IapServiceClient)๋ ํจํค์ง ๋ด๋ถ์์๋ง ์ฌ์ฉ๋๋ฉฐ, ๊ฒ์ ์ฝ๋๋ ๋ฐ๋์ UseCase ๊ณ์ธต์ ํตํด ์ ๊ทผํฉ๋๋ค.
2. AppsInTossAdUseCase (ํตํฉ ๊ด๊ณ ์ง์ ์ )
AitRpcBridge.Instance.Ads๋ฅผ ํตํด ์ธ์ ๋ ์ ๊ทผํ ์ ์์ต๋๋ค. (์ง์ AppsInTossAdUseCase.Instance๋ฅผ ํธ์ถํ ํ์๊ฐ ์์ต๋๋ค.)
var adResult = await AitRpcBridge.Instance.Ads.ShowRewardedAsync();
if (adResult.IsRewardGranted)
{
GrantRewardToPlayer();
}
ShowInterstitialAsync,ShowRewardedAsync๋ ๋ฉ์๋๋ง ๋ ธ์ถ๋ฉ๋๋ค.- ์ด๋ค
adGroupId๋ฅผ ์ธ์ง, ๊ด๊ณ ์ค์ ์๊ฐ์ ๋ฉ์ถ์ง/์์๊ฑฐํ ์ง๋ ScriptableObject ์ค์ ์ ๊ทธ๋๋ก ๋ฐ๋ฆ ๋๋ค. - ๋ด๋ถ์ ์ผ๋ก
AppsInTossPlaybackPause๋ฅผ ์ฌ์ฉํดTime.timeScale๊ณผAudioListener.pause๋ฅผ ์์ ํ๊ฒ ์ฐธ์กฐ ์นด์ดํธ ๋ฐฉ์์ผ๋ก ๊ด๋ฆฌํ๋ฏ๋ก, ๋ค๋ฅธ ์์คํ ๊ณผ ์ถฉ๋ํ์ง ์์ต๋๋ค.
3. AppsInTossIapUseCase (๋ฆฌ๋ชจํธ ์นดํ๋ก๊ทธ + ์ปค์คํ ๋ ธ์ถ)
AitRpcBridge.Instance.Iaps๋ฅผ ํตํด ์ ๊ทผํฉ๋๋ค.
// 1) ์์ ์ ์ฒด ๋ชฉ๋ก (๋ฆฌ๋ชจํธ ์นดํ๋ก๊ทธ)
var catalog = await AitRpcBridge.Instance.Iaps.GetRemoteCatalogAsync();
// 2) ํน์ Spot (์: remove_ads ๋ฒํผ)
var curated = await AitRpcBridge.Instance.Iaps.GetCuratedProductAsync("remove_ads_button");
if (curated != null)
{
RenderCustomCard(curated.Value);
}
// 3) ๊ตฌ๋งค
var result = await AitRpcBridge.Instance.Iaps.PurchaseCuratedSpotAsync("remove_ads_button");
- ๋ฆฌ๋ชจํธ ์นดํ๋ก๊ทธ: Toss์์ ๋ด๋ ค์ฃผ๋ ์ํ ๋ฆฌ์คํธ/์ด๋ฏธ์ง/๊ฐ๊ฒฉ์ ๊ทธ๋๋ก UI์ ๋ฟ๋ฆด ๋ ์ฌ์ฉํฉ๋๋ค.
- Curated Spot: ํน์ UI ์์น(์: ํ ํ๋ฉด ๊ด๊ณ ์ ๊ฑฐ ๋ฒํผ)์ ๋ํด, ํ์งํยท๊ฐ์กฐ ์์ ๋ฑ์ ์ปค์คํฐ๋ง์ด์งํ๋ฉด์๋ ์ค์ ๊ฒฐ์ ๋ Toss SKU์ 1:1๋ก ์ฐ๊ฒฐ๋ฉ๋๋ค.
- ๊ฒฐ์ ํ๋ฆ:
PurchaseCuratedSpotAsync๊ฐ ์ฃผ๋ฌธ ์์ฑ โ ์ด๋ฒคํธ ํด๋ง โ ์ฑ๊ณต/์คํจ ๋ฐํ๊น์ง ์ ๋ถ ์ฒ๋ฆฌํ๋ฉฐ,PurchaseCompleted์ด๋ฒคํธ๋ก๋ ๊ฒฐ๊ณผ๋ฅผ ์์ ํ ์ ์์ต๋๋ค.
4. WebView ๊ฐ์์ฑ & ์๋ ์ฌ์ ์ ์ด
ํ ์ค ์ ์ฑ ์ WebView๊ฐ ํ๋ฉด์ ๋ณด์ด์ง ์๋ ๋์์๋ ๊ฒ์์ด ์๋์ผ๋ก ๋ฉ์ถ๊ณ ์๋ฆฌ๊ฐ ๋์ง ์์์ผ ํฉ๋๋ค. ์ด๋ฅผ ์ํด React/Unity ์์ชฝ์ ํ ์ ์ถ๊ฐํ์ต๋๋ค.
// webapp/src/App.tsx
useEffect(() => {
const notify = (state: "hidden" | "visible") =>
unityContext.sendMessage?.("AitRpcBridge", "OnHostVisibilityChanged", state);
const handleVisibility = () => notify(document.hidden ? "hidden" : "visible");
document.addEventListener("visibilitychange", handleVisibility);
window.addEventListener("blur", () => notify("hidden"));
window.addEventListener("focus", () => notify("visible"));
handleVisibility(); // ์ด๊ธฐ ์ํ ์ ๋ฌ
return () => {
document.removeEventListener("visibilitychange", handleVisibility);
window.removeEventListener("blur", () => notify("hidden"));
window.removeEventListener("focus", () => notify("visible"));
};
}, [unityContext]);
Unity์ AitRpcBridge.OnHostVisibilityChanged๋ ์ ScriptableObject ์ค์ ์ ๋ฐ๋ผ AppsInTossPlaybackPause๋ฅผ ํธ์ถํ์ฌ Time.timeScale๊ณผ ์ค๋์ค ์ํ๋ฅผ ์๋์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค.
5. Safe Area ๋ชจํน
SafeAreaManager๋ WebGL (์ค ๋๋ฐ์ด์ค)์์๋ Toss RPC๋ก ์์ ์์ญ์ ๋ฐ๊ณ , Unity ์๋ํฐ/Standalone์์๋ Screen.safeArea๋ฅผ ์ด์ฉํด ์ธ์
์ ๊ณ์ฐํฉ๋๋ค. ๋ณ๋์ ๋ชจํน ์ค์ ์์ด๋ ์๋ํฐ์์ UI๋ฅผ ๋ง์ถ ์ ์์ต๋๋ค.
ShareService (๊ณต์ ๋ฐ ๋ฆฌ์๋)
๊ณต์ ํ๊ธฐ ๊ธฐ๋ฅ์ ํธ์ถํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋จ์ผ ์๋ต์ผ๋ก ๋ฐ์ต๋๋ค.
using Ait.Share;
using AitBridge.RPC;
using Cysharp.Threading.Tasks;
public class ShareTest : MonoBehaviour
{
public async UniTask TestShare(string moduleId)
{
var request = new ShowContactsViralRequest { ModuleId = moduleId };
try
{
var response = await AitRpcBridge.Instance.ShareService.ShowContactsViral(request);
switch (response.EventCase)
{
case ShowContactsViralResponse.EventOneofCase.Reward:
Debug.Log($"Share Reward! Amount: {response.Reward.RewardAmount}, Unit: {response.Reward.RewardUnit}");
break;
case ShowContactsViralResponse.EventOneofCase.Close:
Debug.Log($"Share Closed! Reason: {response.Close.CloseReason}, Sent Count: {response.Close.SentRewardsCount}");
break;
case ShowContactsViralResponse.EventOneofCase.Error:
Debug.LogError($"Share Error: {response.Error.Message}");
break;
}
}
catch (Exception e)
{
Debug.LogError($"ShowContactsViral RPC failed: {e.Message}");
}
}
}
๐ ์ํคํ ์ฒ
์ด ํ๋ก์ ํธ๋ Protobuf๋ฅผ ์ด์ฉํ ์ฝ๋ ์์ฑ ๊ธฐ๋ฐ์ RPC ์ํคํ ์ฒ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
proto/ํด๋ ๋ด์.protoํ์ผ์ ์๋น์ค์ ๋ฉ์์ง๋ฅผ ์ ์ํฉ๋๋ค.- ๋ณ๊ฒฝ์ฌํญ์ Git์ Pushํ๋ฉด, GitHub Action์ด
.github/workflows/generate-protobuf.yml์ํฌํ๋ก์ฐ๋ฅผ ์คํํฉ๋๋ค. - ์ํฌํ๋ก์ฐ๋
protoc-gen-webviewrpc์ฝ๋ ์์ฑ๊ธฐ๋ฅผ ์ฌ์ฉํ์ฌ Unity์ฉ C# ์ฝ๋์ React์ฉ TypeScript ์ฝ๋๋ฅผ ์๋์ผ๋ก ์์ฑํ๊ณ ,generated-code๋ธ๋์น์ ์ปค๋ฐํฉ๋๋ค. - ๊ฐ๋ฐ์๋
generated-code๋ธ๋์น์ ๋ณ๊ฒฝ์ฌํญ์ ์์ ์ ๊ฐ๋ฐ ๋ธ๋์น๋ก ๊ฐ์ ธ์(pull๋๋cherry-pick) ๊ตฌํ์ ๊ณ์ํฉ๋๋ค.
๐ค ๊ธฐ์ฌํ๊ธฐ
ํ๋ก์ ํธ์ ๊ธฐ์ฌํ๊ณ ์ถ์ผ์๋ค๋ฉด, ์ด์๋ฅผ ์์ฑํ๊ฑฐ๋ Pull Request๋ฅผ ๋ณด๋ด์ฃผ์ธ์.
๐ ๋ผ์ด์ ์ค
์ด ํ๋ก์ ํธ๋ MIT License๋ฅผ ๋ฐ๋ฆ ๋๋ค.
No comments yet. Be the first!