Tauri 2.x iOS 앱에 AdMob 보상형 광고 연동하기: Swift 플러그인 개발 가이드
8 분 소요

Tauri 2.x iOS 앱에 AdMob 보상형 광고 연동하기: Swift 플러그인 개발 가이드

Tauri v2 iOS 앱에 Google AdMob 보상형 광고를 연동하는 전체 과정을 다룹니다. 공식 플러그인이 없어 직접 Swift 플러그인을 개발한 경험을 공유합니다.

공식 플러그인이 없는 상태에서 시작하기

Shadow Dash를 App Store에 올리고 나니 다음 숙제는 수익화였습니다. 보상형 광고를 붙이려고 Tauri 플러그인 생태계를 뒤졌는데, AdMob을 다루는 공식 플러그인이 하나도 없었습니다.

결국 GoogleMobileAds SDK를 감싸는 Swift 플러그인을 직접 만들어야 했습니다. 아래 내용은 그 과정에서 막혔던 지점과 해결 코드를 순서대로 정리한 것입니다. Tauri CLI 업그레이드부터 권한 설정, 링커 에러까지 실제로 부딪힌 순서 그대로 따라갑니다.

이 글은 Tauri + PixiJS로 iOS 게임 개발 포스트의 후속편입니다. Shadow Dash 게임에 광고를 연동한 실제 경험을 바탕으로 작성했습니다.

Shadow Dash 게임오버 화면

왜 AdMob인가?

모바일 게임 수익화 옵션 비교

방식장점단점
보상형 광고사용자 경험 좋음, 높은 eCPM구현 복잡
배너 광고구현 간단낮은 eCPM, UX 저해
인앱 구매높은 수익구현 복잡, 결제 심사

보상형 광고를 선택한 이유

Shadow Dash에서는 컨티뉴 시스템에 보상형 광고를 적용했습니다:

graph LR
    A[게임오버] --> B["광고 보고 계속하기" 버튼]
    B --> C[광고 시청]
    C --> D[게임 재개]

사용자가 자발적으로 광고를 시청하고 보상(게임 계속)을 받는 구조로, 사용자 경험을 해치지 않습니다.

리워드 광고가 맞는 게임, 맞지 않는 게임

광고 SDK를 붙이기 전에 보상형 광고가 내 게임에 어울리는지부터 따져봐야 합니다. 무작정 넣으면 수익도 안 나고 리뷰만 나빠집니다.

리워드 광고가 잘 맞는 경우:

  • 짧은 세션을 반복하는 아케이드/캐주얼 게임. Shadow Dash처럼 게임오버가 자주 발생하고, “한 번 더”라는 욕구가 강한 구조가 이상적입니다.
  • 컨티뉴, 코인 2배, 힌트 공개처럼 광고 시청에 대한 보상이 명확하고 즉각적인 경우.
  • 무과금 유저 비중이 높아 인앱 결제만으로는 수익이 어려운 개인 개발자 앱. 개인 개발자의 AI SaaS 수익화 여정에서 다룬 것처럼, 작은 앱일수록 진입 장벽 없는 수익원이 중요합니다.

피하는 게 나은 경우:

  • 몰입형 내러티브 게임이나 결제 의향이 높은 코어 게임. 전면 광고든 보상형이든 광고가 몰입을 끊으면 오히려 이탈을 부릅니다.
  • DAU가 수백 명 수준인 초기 앱. eCPM이 아무리 높아도 노출 수가 적으면 의미 있는 수익이 나지 않습니다. 이 경우엔 광고 연동보다 유저 확보가 먼저입니다.
  • 광고 SDK가 게임 용량과 빌드 복잡도를 키우는 게 부담되는 미니멀 프로젝트. 광고로 버는 돈보다 유지보수 비용이 클 수 있습니다. AI 도구를 쓰는 프로젝트라면 AI 에이전트 비용의 현실에서 다룬 비용 대비 효과 관점을 그대로 적용해 판단하면 좋습니다.

정리하면, 세션이 짧고 재시도 동기가 강한 게임에 보상형 광고가 가장 잘 맞습니다. Shadow Dash의 구조와 비슷한 게임을 만들고 있다면 Tauri + PixiJS iOS 게임 개발 글에서 게임 루프 설계부터 확인해보세요.

광고 SDK 선택 과정

Tauri 앱에서 사용할 수 있는 광고 SDK를 검토했습니다:

SDK장점단점채택 여부
AdMob높은 eCPM, 다양한 광고 형식네이티브 플러그인 필요✅ 채택
AppLixirJS SDK만으로 연동 가능DAU 5,000명 이상 필요❌ 미채택
H5 Game AdsHTML5 게임 특화베타 서비스 (불안정)❌ 미채택

AppLixir를 선택하지 않은 이유:

  • 최소 일일 활성 유저(DAU) 5,000명 이상을 달성해야 신청 가능
  • 신규 앱에서는 이 조건을 충족하기 어려움

H5 Game Ads를 선택하지 않은 이유:

  • 아직 베타 서비스 단계
  • 안정성과 수익성이 검증되지 않음

AdMob vs AppLixir 상세 비교

AdMob과 AppLixir의 성능을 비교하면:

항목AdMobAppLixir
eCPM (보상형)$20〜30$15〜25
Fill Rate95%+80〜90%
지원 형식보상형, 전면, 배너보상형만
Mediation지원미지원
연동 난이도높음 (네이티브 필요)낮음 (JS SDK)
진입 장벽없음DAU 5,000명 이상

결론: 네이티브 플러그인 개발이 필요하지만, AdMob은 진입 장벽이 없고 높은 eCPM과 Fill Rate를 제공합니다.


Tauri CLI 업그레이드

XCFramework 지원 필요

Tauri v2에서 iOS 플러그인을 개발하려면 Swift Package Manager(SPM)를 사용해야 합니다. GoogleMobileAds SDK처럼 외부 프레임워크 의존성이 필요한 플러그인을 만들려면 XCFramework 지원이 필요합니다.

Tauri CLI 2.9.6+ 업그레이드

# Cargo를 통한 Tauri CLI 업그레이드
cargo install tauri-cli --force

# 버전 확인
cargo tauri --version
# 출력: tauri-cli 2.9.6 (또는 그 이상)

—ios-framework 옵션

플러그인 생성 시 iOS 프레임워크 타입을 지정할 수 있습니다:

# Xcode 프로젝트 방식 (SPM 의존성 추가 용이) - 권장
cargo tauri plugin new admob --ios --ios-framework xcode

# XCFramework 방식
cargo tauri plugin new admob --ios --ios-framework xcframework
옵션설명장점단점
xcodeXcode 프로젝트 생성SPM으로 의존성 추가 용이프로젝트 파일 관리 필요
xcframeworkXCFramework 생성배포 용이외부 의존성 추가 복잡

AdMob 플러그인에는 xcode 옵션 권장: GoogleMobileAds SDK를 SPM으로 쉽게 추가할 수 있습니다.


Tauri 플러그인 구조 이해

디렉토리 구조

tauri-plugin-admob/
├── src/                    # Rust 코드
│   ├── lib.rs             # 플러그인 진입점
│   ├── mobile.rs          # iOS/Android 브릿지
│   ├── desktop.rs         # 데스크톱 스텁
│   ├── commands.rs        # Tauri 커맨드
│   └── models.rs          # 요청/응답 타입
├── ios/                    # iOS 네이티브 코드
│   └── tauri-plugin-admob/
│       └── AdmobPlugin.swift
├── guest-js/              # TypeScript API
│   └── index.ts
├── permissions/           # Tauri 권한 설정
│   └── default.toml
├── build.rs               # 빌드 스크립트
└── Cargo.toml

데이터 흐름

graph TD
    A[TypeScript] -->|invoke| B[Rust Command]
    B -->|run_mobile_plugin| C[Swift Plugin]
    C -->|API Call| D[GoogleMobileAds SDK]
    D -->|Response| C
    C -->|Result| B
    B -->|JSON| A

플러그인 개발 시작

플러그인 스캐폴딩

# Xcode 프로젝트 방식으로 생성
cargo tauri plugin new admob --ios --ios-framework xcode

GoogleMobileAds SDK 추가

Xcode에서 플러그인 프로젝트를 열고 Swift Package Manager로 SDK 추가:

  1. tauri-plugin-admob/ios/tauri-plugin-admob.xcodeproj 열기
  2. File → Add Package Dependencies
  3. URL 입력: https://github.com/googleads/swift-package-manager-google-mobile-ads
  4. GoogleMobileAds 선택 후 추가

Swift 플러그인 구현

AdmobPlugin.swift

import SwiftRs
import Tauri
import UIKit
import WebKit
import GoogleMobileAds

// MARK: - Argument Types
class InitializeArgs: Decodable {}

class LoadRewardedArgs: Decodable {
    let adUnitId: String
}

class ShowRewardedArgs: Decodable {}

// MARK: - AdMob Plugin
class AdmobPlugin: Plugin {
    private var rewardedAd: GADRewardedAd?
    private var isInitialized = false
    private var pendingInvoke: Invoke?

    // 테스트 광고 ID (프로덕션에서는 실제 ID 사용)
    private let testAdUnitId = "ca-app-pub-3940256099942544/1712485313"

    @objc public override func load(webview: WKWebView) {
        NSLog("[AdMob Plugin] Loaded")
    }

    // SDK 초기화
    @objc public func initialize(_ invoke: Invoke) {
        if isInitialized {
            invoke.resolve(["success": true, "message": "Already initialized"])
            return
        }

        GADMobileAds.sharedInstance().start { status in
            self.isInitialized = true
            NSLog("[AdMob Plugin] SDK Initialized")
            invoke.resolve(["success": true, "message": "SDK initialized"])
        }
    }

    // 보상형 광고 로드
    @objc public func loadRewardedAd(_ invoke: Invoke) {
        do {
            let args = try invoke.parseArgs(LoadRewardedArgs.self)
            let adUnitId = args.adUnitId.isEmpty ? testAdUnitId : args.adUnitId

            let request = GADRequest()
            GADRewardedAd.load(withAdUnitID: adUnitId, request: request) { [weak self] ad, error in
                if let error = error {
                    invoke.resolve(["success": false, "error": error.localizedDescription])
                    return
                }

                self?.rewardedAd = ad
                self?.rewardedAd?.fullScreenContentDelegate = self
                invoke.resolve(["success": true])
            }
        } catch {
            invoke.reject(error.localizedDescription)
        }
    }

    // 광고 준비 상태 확인
    @objc public func isRewardedAdReady(_ invoke: Invoke) {
        let isReady = rewardedAd != nil
        invoke.resolve(["ready": isReady])
    }

    // 광고 표시
    @objc public func showRewardedAd(_ invoke: Invoke) {
        guard let rewardedAd = rewardedAd else {
            invoke.resolve(["success": false, "rewarded": false, "error": "No ad loaded"])
            return
        }

        guard let rootViewController = getRootViewController() else {
            invoke.resolve(["success": false, "rewarded": false, "error": "No root view controller"])
            return
        }

        pendingInvoke = invoke

        DispatchQueue.main.async {
            rewardedAd.present(fromRootViewController: rootViewController) { [weak self] in
                let reward = rewardedAd.adReward
                if let pending = self?.pendingInvoke {
                    pending.resolve([
                        "success": true,
                        "rewarded": true,
                        "rewardAmount": reward.amount.intValue,
                        "rewardType": reward.type
                    ])
                    self?.pendingInvoke = nil
                }
            }
        }
    }

    // Root View Controller 가져오기
    private func getRootViewController() -> UIViewController? {
        if let windowScene = UIApplication.shared.connectedScenes
            .compactMap({ $0 as? UIWindowScene })
            .first(where: { $0.activationState == .foregroundActive }),
           let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }),
           let rootVC = keyWindow.rootViewController {
            var topController = rootVC
            while let presented = topController.presentedViewController {
                topController = presented
            }
            return topController
        }
        return nil
    }
}

// MARK: - GADFullScreenContentDelegate
extension AdmobPlugin: GADFullScreenContentDelegate {
    func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
        rewardedAd = nil
        if let pending = pendingInvoke {
            pending.resolve(["success": true, "rewarded": false])
            pendingInvoke = nil
        }
    }

    func ad(_ ad: GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
        rewardedAd = nil
        if let pending = pendingInvoke {
            pending.resolve(["success": false, "rewarded": false, "error": error.localizedDescription])
            pendingInvoke = nil
        }
    }
}

// MARK: - Plugin Export
@_cdecl("init_plugin_admob")
func initPlugin() -> Plugin {
    return AdmobPlugin()
}

핵심 포인트

  1. @objc 어노테이션: Rust에서 호출하려면 Objective-C 런타임에 노출해야 합니다
  2. pendingInvoke 패턴: 비동기 광고 콜백을 Tauri의 동기 invoke 패턴에 맞추기 위해 사용
  3. GADFullScreenContentDelegate: 광고 닫힘/에러 이벤트 처리
  4. @_cdecl("init_plugin_admob"): Rust에서 플러그인을 로드하기 위한 C 함수 내보내기

Rust 브릿지 구현

models.rs

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResponse {
    pub success: bool,
    pub message: Option<String>,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadRewardedAdRequest {
    pub ad_unit_id: String,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadRewardedAdResponse {
    pub success: bool,
    pub error: Option<String>,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IsRewardedAdReadyResponse {
    pub ready: bool,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ShowRewardedAdResponse {
    pub success: bool,
    pub rewarded: bool,
    pub reward_amount: Option<i32>,
    pub reward_type: Option<String>,
    pub error: Option<String>,
}

mobile.rs

use serde::de::DeserializeOwned;
use tauri::{
    plugin::{PluginApi, PluginHandle},
    AppHandle, Runtime,
};
use crate::models::*;

#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_admob);

pub fn init<R: Runtime, C: DeserializeOwned>(
    _app: &AppHandle<R>,
    api: PluginApi<R, C>,
) -> crate::Result<Admob<R>> {
    #[cfg(target_os = "ios")]
    let handle = api.register_ios_plugin(init_plugin_admob)?;
    Ok(Admob(handle))
}

pub struct Admob<R: Runtime>(PluginHandle<R>);

impl<R: Runtime> Admob<R> {
    pub fn initialize(&self) -> crate::Result<InitializeResponse> {
        self.0.run_mobile_plugin("initialize", ()).map_err(Into::into)
    }

    pub fn load_rewarded_ad(&self, ad_unit_id: String) -> crate::Result<LoadRewardedAdResponse> {
        self.0.run_mobile_plugin("loadRewardedAd",
            LoadRewardedAdRequest { ad_unit_id }).map_err(Into::into)
    }

    pub fn is_rewarded_ad_ready(&self) -> crate::Result<IsRewardedAdReadyResponse> {
        self.0.run_mobile_plugin("isRewardedAdReady", ()).map_err(Into::into)
    }

    pub fn show_rewarded_ad(&self) -> crate::Result<ShowRewardedAdResponse> {
        self.0.run_mobile_plugin("showRewardedAd", ()).map_err(Into::into)
    }
}

build.rs (중요!)

const COMMANDS: &[&str] = &[
    "initialize",
    "load_rewarded_ad",
    "is_rewarded_ad_ready",
    "show_rewarded_ad"
];

fn main() {
    // iOS 빌드 시 프레임워크 링크
    let target = std::env::var("TARGET").unwrap_or_default();
    if target.contains("ios") {
        println!("cargo:rustc-link-lib=framework=GoogleMobileAds");
        println!("cargo:rustc-link-lib=framework=UserMessagingPlatform");
    }

    tauri_plugin::Builder::new(COMMANDS)
        .android_path("android")
        .ios_path("ios")
        .build();
}

주의: #[cfg(target_os = "ios")]는 build.rs에서 작동하지 않습니다. build.rs는 호스트 머신(macOS)에서 실행되므로, std::env::var("TARGET")으로 타겟을 확인해야 합니다.


TypeScript API 구현

guest-js/index.ts

import { invoke } from '@tauri-apps/api/core'

export interface InitializeResponse {
  success: boolean;
  message?: string;
}

export interface LoadRewardedAdResponse {
  success: boolean;
  error?: string;
}

export interface IsRewardedAdReadyResponse {
  ready: boolean;
}

export interface ShowRewardedAdResponse {
  success: boolean;
  rewarded: boolean;
  rewardAmount?: number;
  rewardType?: string;
  error?: string;
}

export async function initialize(): Promise<InitializeResponse> {
  return await invoke<InitializeResponse>('plugin:admob|initialize');
}

export async function loadRewardedAd(adUnitId: string = ''): Promise<LoadRewardedAdResponse> {
  return await invoke<LoadRewardedAdResponse>('plugin:admob|load_rewarded_ad', {
    adUnitId,
  });
}

export async function isRewardedAdReady(): Promise<IsRewardedAdReadyResponse> {
  return await invoke<IsRewardedAdReadyResponse>('plugin:admob|is_rewarded_ad_ready');
}

export async function showRewardedAd(): Promise<ShowRewardedAdResponse> {
  return await invoke<ShowRewardedAdResponse>('plugin:admob|show_rewarded_ad');
}

앱에 플러그인 연동

Cargo.toml에 플러그인 추가

[dependencies]
tauri-plugin-admob = { path = "../tauri-plugin-admob" }

lib.rs에 플러그인 등록

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_admob::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

권한 설정

src-tauri/capabilities/default.json:

{
  "permissions": [
    "core:default",
    "admob:default"
  ]
}

tauri-plugin-admob/permissions/default.toml:

[default]
description = "Default permissions for the AdMob plugin"
permissions = [
    "allow-initialize",
    "allow-load-rewarded-ad",
    "allow-is-rewarded-ad-ready",
    "allow-show-rewarded-ad"
]

Info.plist에 앱 ID 추가

src-tauri/gen/apple/앱이름_iOS/Info.plist:

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX</string>
<key>SKAdNetworkItems</key>
<array>
    <dict>
        <key>SKAdNetworkIdentifier</key>
        <string>cstr6suwn9.skadnetwork</string>
    </dict>
</array>

게임에서 광고 사용하기

광고 설정 파일

src/lib/config/admob.ts:

// 광고 활성화 플래그 (승인 전까지 false)
export const ADS_ENABLED = false;

const TEST_AD_UNITS = {
  rewardedAd: 'ca-app-pub-3940256099942544/1712485313',
  appId: 'ca-app-pub-3940256099942544~1458002511',
};

const PRODUCTION_AD_UNITS = {
  rewardedAd: 'ca-app-pub-XXXXXXXX/XXXXXXXXXX',  // 실제 광고 단위 ID
  appId: 'ca-app-pub-XXXXXXXX~XXXXXXXXXX',       // 실제 앱 ID
};

const isDevelopment = import.meta.env.DEV;

export const AD_UNITS = isDevelopment ? TEST_AD_UNITS : PRODUCTION_AD_UNITS;
export const isTestMode = isDevelopment;

게임오버 화면에서 사용

import { AD_UNITS, ADS_ENABLED } from '$lib/config/admob';
import * as admob from 'tauri-plugin-admob-api';

let adLoading = false;
let adError = '';

async function handleWatchAd() {
  if (!ADS_ENABLED) {
    // 광고 비활성화 시 바로 컨티뉴
    game.startContinue();
    return;
  }

  adLoading = true;
  adError = '';

  try {
    // 광고 준비 상태 확인
    const readyCheck = await admob.isRewardedAdReady();

    if (!readyCheck.ready) {
      // 광고가 준비되지 않았으면 로드
      const loadResult = await admob.loadRewardedAd(AD_UNITS.rewardedAd);
      if (!loadResult.success) {
        adError = loadResult.error || 'Failed to load ad';
        adLoading = false;
        return;
      }
    }

    // 광고 표시
    const showResult = await admob.showRewardedAd();

    if (showResult.rewarded) {
      // 보상 받음 - 게임 계속
      game.startContinue();
    } else if (showResult.error) {
      adError = showResult.error;
    }
  } catch (error) {
    adError = String(error);
  } finally {
    adLoading = false;
  }
}

트러블슈팅

Swift 타입 에러: “cannot find type ‘RewardedAd’ in scope”

원인: GoogleMobileAds SDK는 Objective-C 기반으로, Swift에서 사용할 때 GAD 접두사가 필요합니다.

// ❌ 잘못된 코드
private var rewardedAd: RewardedAd?

// ✅ 올바른 코드
private var rewardedAd: GADRewardedAd?

링커 에러: “Undefined symbols for architecture arm64”

원인: GoogleMobileAds 프레임워크가 링크되지 않음

해결 1: 메인 앱 Xcode 프로젝트에도 SDK 추가

  1. src-tauri/gen/apple/앱이름.xcodeproj 열기
  2. File → Add Package Dependencies
  3. GoogleMobileAds SDK 추가

해결 2: build.rs에서 프레임워크 링크

let target = std::env::var("TARGET").unwrap_or_default();
if target.contains("ios") {
    println!("cargo:rustc-link-lib=framework=GoogleMobileAds");
}

Tauri 권한 에러

에러 메시지:

admob.is_rewarded_ad_ready not allowed.
Permissions associated with this command: admob:allow-is-rewarded-ad-ready

해결: permissions/default.tomlcapabilities/default.json에 권한 추가

광고가 표시되지 않음

증상: 버튼 클릭해도 광고 안 나옴

확인 사항:

  1. 테스트 광고 ID 사용 중인지 확인
  2. GADMobileAds.sharedInstance().start() 호출 확인
  3. 실제 기기에서 테스트 (시뮬레이터 지원 제한적)

수익화 전략 고찰

단계별 도입 계획

graph TD
    subgraph 1차["1차 도입 (현재)"]
        A[보상형 컨티뉴 광고만]
    end

    subgraph 2차["2차 도입 (1〜2개월 후)"]
        B[전면 광고 추가]
        C[코인 2배 광고]
    end

    subgraph 3차["3차 도입 (3개월 후)"]
        D[IAP: 광고 제거]
        E[IAP: 코인 패키지]
    end

    1차 --> 2차
    2차 --> 3차

수익화 3대 원칙

  1. 게임 밸런스 보호: 과금은 외형만, 능력치 판매 금지
  2. 무과금 유저 존중: 모든 콘텐츠 무료 획득 가능
  3. 가치 제공 우선: 유저가 “쓸 가치가 있다”고 느끼는 상품

예상 수익 (DAU 10,000명 기준)

단계광고 유형일일 수익월간 수익
1차보상형만~$130~$3,900
2차보상형 + 전면~$425~$12,750
3차광고 + IAP~$600~$18,000

직접 붙여보고 남은 교훈

배운 점

  1. Tauri CLI 최신 버전 유지: XCFramework 지원 등 중요한 기능이 계속 추가됨
  2. Tauri 플러그인 구조: Rust ↔ Swift 브릿지 이해 필수
  3. 크로스 컴파일: build.rs에서 HOST vs TARGET 구분 중요
  4. Tauri v2 권한 시스템: capabilities와 permissions 필수

체크리스트

  • Tauri CLI 최신 버전 업그레이드 (cargo install tauri-cli --force)
  • --ios-framework xcode 옵션으로 플러그인 생성
  • GoogleMobileAds SDK를 메인 앱플러그인 모두에 추가
  • build.rs에서 std::env::var("TARGET")으로 iOS 감지
  • permissions/default.toml에 모든 명령어 권한 정의
  • capabilities/default.jsonadmob:default 추가
  • Info.plist에 GADApplicationIdentifier 추가
  • 테스트/프로덕션 광고 ID 분리 관리

다음 단계

  • Android 지원 추가
  • 전면 광고 구현
  • 배너 광고 구현
  • 인앱 결제 (StoreKit 2) 연동

Shadow Dash 다운로드

이 글에서 다룬 기술로 개발한 Shadow Dash를 직접 플레이해보세요!

Download on the App Store

피드백 환영합니다! 앱을 플레이하시고 개선점이나 버그가 있다면 App Store 리뷰나 이메일로 알려주세요.

📱 Shadow Dash 상세 정보: 포트폴리오 페이지에서 게임의 핵심 메커니즘과 더 많은 스크린샷을 확인하세요.

참고 자료

자주 묻는 질문

Tauri에 공식 AdMob 플러그인이 없는데 직접 만들 수밖에 없나요?
현재로서는 그렇습니다. 공식 플러그인이 없어 GoogleMobileAds SDK를 감싸는 Swift 플러그인을 직접 개발해야 합니다. 이 글의 AdmobPlugin.swift와 Rust 브릿지 코드를 그대로 출발점으로 쓸 수 있습니다.
왜 xcframework 대신 xcode 옵션으로 플러그인을 만드나요?
GoogleMobileAds처럼 외부 SDK 의존성이 있을 때 xcode 옵션이 Swift Package Manager로 의존성을 추가하기 훨씬 쉽기 때문입니다. xcframework는 배포는 편하지만 외부 의존성 연결이 복잡합니다.
build.rs에서 #[cfg(target_os = ios)]를 쓰면 왜 안 되나요?
build.rs는 빌드 대상이 아니라 호스트 머신(macOS)에서 실행되기 때문입니다. iOS 타겟을 감지하려면 std::env::var(TARGET) 값에 ios가 포함되는지 확인해야 프레임워크 링크가 올바르게 동작합니다.
리워드 광고가 표시되지 않을 때 가장 먼저 무엇을 확인해야 하나요?
테스트 광고 ID를 쓰고 있는지, GADMobileAds.sharedInstance().start()가 호출됐는지, 그리고 시뮬레이터가 아닌 실제 기기에서 테스트하고 있는지 순서대로 확인하세요. 시뮬레이터는 광고 지원이 제한적입니다.

다른 언어로 읽기

글이 도움이 되셨나요?

더 나은 콘텐츠를 작성하는 데 힘이 됩니다. 커피 한 잔으로 응원해주세요.

저자 소개

jw

Kim Jangwook

AI/LLM 전문 풀스택 개발자

10년 이상의 웹 개발 경험을 바탕으로 AI 에이전트 시스템, LLM 애플리케이션, 자동화 솔루션을 구축합니다. Claude Code, MCP, RAG 시스템에 대한 실전 경험을 공유합니다.

블로그 목록으로