로그인게임하기
구조 읽기

[100턴 구조 읽기 5] 컴파일 심볼로 가르는 여섯 플랫폼 빌드

2026.10.10

졸업까지 100턴 구조 읽기 · 5편

졸업까지 100턴은 Unity 프로젝트를 나누지 않고 빌드만 갈라 Android, iOS, Windows, 웹, CrazyGames 게임 포털, 토스 미니앱에 출시했습니다. 로그인과 결제와 광고가 모두 다른 여섯 플랫폼을 컴파일 심볼로 갈라 빌드하는 방법과 그 비용을 정리합니다.

PROBLEM

플랫폼마다 규칙이 다릅니다

출시처로그인광고 구현
앱 (Android · iOS)Google, AppleGoogle AdMob (EL_ADMOB 심볼을 켠 빌드)
웹Google, Apple 웹 로그인없음
CrazyGames포털 계정, 게스트로 먼저 시작포털 광고
토스 미니앱토스 자동 로그인토스 앱이 제공하는 광고

결제도 갈래가 셋입니다. Google Play 결제는 서버가 영수증으로 검증하고, 웹은 Xsolla 결제창을 쓰고, CrazyGames 는 포털 인증을 거친 뒤 Xsolla 로 결제합니다.

SYMBOL

컴파일 심볼로 가릅니다

우리는 플랫폼 차이를 인터페이스보다 조건부 컴파일로 다뤘습니다. 빌드마다 필요 없는 SDK 코드가 아예 들어가지 않기 때문입니다.

심볼조건문 수뜻
UNITY_WEBGL103웹 빌드 전체
EL_CRAZYGAMES102CrazyGames 포털 빌드
EL_TOSS38토스 미니앱 빌드

로그인은 시작 화면에서 빌드 종류에 따라 길이 갈립니다. 결과는 모두 서버의 로그인 API 로 모입니다.

StartScene.csTDClient/Assets/Sources/StartScene.csr5406 · 499–508줄
#else
#if EL_CRAZYGAMES
        // 크레이지게임스 환경에서는 크레이지게임스 로그인 시도
        HideLoginPanel();
        CheckCrazyGamesUser();
#elif EL_TOSS
        HideLoginPanel();
        TossAutoLogin();
#else
        string lastID = ElCore.GetPlayerPref("LAST_SOCIAL_ID");
ADS

같은 이름, 다른 구현

광고 관리자는 같은 이름의 클래스를 조건부 컴파일로 네 번 정의합니다. 공개 메서드가 같으므로 광고를 부르는 쪽은 지금 어느 플랫폼인지 모릅니다.

ElAdsManager 구조ElFramework/Sources/External/ElAdsManager.cs (633줄을 구조만 남겨 줄임)
// ElAdsManager.cs (줄임). 같은 이름의 클래스를 조건부 컴파일로 네 번 정의한다.
#if EL_ADMOB && !UNITY_WEBGL
public class ElAdsManager : ElBehaviour      // 앱: Google AdMob (EL_ADMOB 을 켠 빌드)
{   public void LoadAds() { ... }   public void ShowAds() { ... }   }
#else
#if EL_CRAZYGAMES
public class ElAdsManager : ElBehaviour      // CrazyGames 포털 광고
{   public void LoadAds() { ... }   public void ShowAds() { ... }   }
#elif EL_TOSS
public class ElAdsManager : ElBehaviour      // 토스 앱의 광고 (자바스크립트 연결)
{   public void LoadAds() { ... }   public void ShowAds() { ... }   }
#else
public class ElAdsManager : MonoBehaviour    // 광고 없는 빌드: 아무것도 하지 않음
{   public void LoadAds() { ... }   public void ShowAds() { ... }   }
#endif
#endif
PAY

같은 상점 버튼, 세 갈래 결제

에드윈이 상점 버튼을 누르면 뒤편의 세 결제 방으로 갈라지고 선물 상자는 모두 가운데 금고에서 나오는 그림
버튼은 같고 결제 길은 셋이지만, 지급은 언제나 서버가 합니다.
UIPackageView.csTDClient/Assets/Sources/UI/Shop/UIPackageView.csr5406 · 475–493줄
    private void OnClickCacheProduct(UIProductSlot slot)
    {
#if EL_CRAZYGAMES
        string sku = slot?.GetExolaProductInfo()?.SKU ?? null;
        if (sku != null)
        {
            ElCrazyGamesManager.Get().AuthenticateUser(
                (_) => PurchaseProduct(sku),
                () => PurchaseProduct(sku),
                (_) => ShowMessage("사용자 인증이 필요합니다."),
                () => ShowMessage("사용자 인증이 취소되었습니다.")
            );
        }
#elif IAP_ENABLED
        TDIAPManager.Get().Purchase(slot.GetProductInfo().StoreId);
#else
        PurchaseProduct(slot.GetExolaProductInfo().SKU);
#endif
    }

어떤 길이든 지급은 서버가 합니다. 클라이언트는 서버가 돌려준 결과로 화면을 갱신합니다.

BUILD

빌드 매트릭스

빌드 메뉴는 플랫폼과 환경(개발 · QA · 운영)을 곱한 28개입니다. 같은 함수가 커맨드라인 인자도 받아서 빌드 서버에서 그대로 씁니다.

  1. 플랫폼AOS, iOS, WIN, WebGL, CrazyGames, 토스
  2. 환경Dev, QA, Product
  3. 심볼 조합채널 심볼 + 환경 심볼
  4. 빌드메뉴 또는 빌드 서버
ElBuildPlayer.csTDClient/Assets/ElFramework/Editor/ElBuildPlayer.csr5406 · 791–807줄
public static void Build_WebGL_CrazyGames(BuildEnvironment buildEnvironment)
{
    string versionString = GetCommandLineArg("-VersionString");

    string definesArg = GetCommandLineArg("-defineSymbols");
    if (!string.IsNullOrEmpty(definesArg))
    {
        PlayerSettings.SetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.WebGL, definesArg);
        Debug.Log("Defines Set: " + definesArg);
    }

    Debug.Log($"[Build_WebGL] VersionString: {versionString}");

    BuildPlayerOptions buildPlayerOptions = InitWebGLBuild(false, versionString, "EL_CRAZYGAMES", buildEnvironment);

    Build(buildPlayerOptions);
}
SDK

외부 SDK 를 길들이는 법

유리 상자 속 작은 기계 요정을 밧줄 울타리, 마법진, 종이 달린 문이 세 겹으로 둘러싸고 알렌이 지켜보는 그림
가드, 컴파일 검사, 빌드 전 검사의 세 겹 울타리

CrazyGames SDK 에는 허용 도메인이 아니면 게임을 강제로 끄는 기능이 있습니다. 이 SDK 는 시작할 때 무조건 실행되므로, 그대로 두면 일반 웹 빌드와 토스 빌드가 꺼집니다.

그래서 SDK 코드에 실행 차단 가드를 넣었습니다. 문제는 SDK 를 갱신하면 가드가 원본으로 덮여 조용히 사라진다는 점입니다. 이를 세 겹으로 막았습니다.

  1. SDK 코드에 가드CrazyGames 빌드가 아니면 실행하지 않음
  2. 컴파일 시점 검사가드와 함께 넣은 표식을 참조
  3. 빌드 전 검사빌드 직전에 파일 내용을 확인
CrazySiteLockGuard.csTDClient/Assets/Sources/Base/CrazySiteLockGuard.csr5406 · 17–30줄
#if !EL_CRAZYGAMES

internal static class CrazySiteLockGuard
{
    // 이 상수 참조가 컴파일 시점 검증의 전부다.
    // SiteLock.ProjectGuardVersion 이 없어지면 여기서 CS0117 등이 발생한다.
    private const int RequiredGuardVersion = 1;

    private const int AppliedGuardVersion = CrazyGames.SiteLock.ProjectGuardVersion;

    // 버전이 어긋나도 걸리도록 배열 크기로 한 번 더 확인한다.
    // (AppliedGuardVersion 이 RequiredGuardVersion 과 다르면 음수 크기가 되어 컴파일 오류)
    private static readonly int[] _versionCheck = new int[AppliedGuardVersion - RequiredGuardVersion + 1];
}

표식이 사라지면 참조가 깨져 컴파일 오류가 납니다. 버전이 어긋나면 배열 크기가 음수가 되어 역시 컴파일 오류가 납니다. 조용한 런타임 사고를 시끄러운 빌드 실패로 바꾼 셈입니다.

RETRO

돌아보며

잘된 점

  • 프로젝트를 여섯 벌로 복사하지 않고 빌드 설정으로 출시처를 늘렸습니다.
  • 광고와 결제를 부르는 쪽은 플랫폼을 모릅니다.
  • 빌드에 필요 없는 SDK 코드가 들어가지 않습니다.
  • 외부 SDK 갱신 사고를 컴파일 오류로 잡아냅니다.

아쉬운 점

  • 조건문이 늘수록 시험해야 할 조합이 빠르게 늘어납니다.
  • 결제 사용 여부 심볼을 프로젝트 전역이 아니라 다섯 파일에 따로 정의해 어긋날 위험이 있습니다.
  • 벤더 코드를 직접 고치는 방식이라 SDK 를 갱신할 때마다 손이 갑니다.

정리

  1. 차이는 빌드 시점에 가릅니다. 조건부 컴파일로 플랫폼마다 필요한 코드만 넣습니다.
  2. 부르는 쪽은 모르게 합니다. 같은 이름과 같은 메서드로 플랫폼별 구현을 숨깁니다.
  3. 조용한 사고를 시끄럽게 만듭니다. 지켜야 할 가드는 컴파일과 빌드 단계에서 검사합니다.

다음 편에서는 실제 장애에서 시작된 두 가지 재설계, 네트워크 재시도와 에셋 배포 이관을 보겠습니다.

← 개발 노트 목록