졸업까지 100턴은 Unity 프로젝트를 나누지 않고 빌드만 갈라 Android, iOS, Windows, 웹, CrazyGames 게임 포털, 토스 미니앱에 출시했습니다. 로그인과 결제와 광고가 모두 다른 여섯 플랫폼을 컴파일 심볼로 갈라 빌드하는 방법과 그 비용을 정리합니다.
플랫폼마다 규칙이 다릅니다
| 출시처 | 로그인 | 광고 구현 |
|---|---|---|
| 앱 (Android · iOS) | Google, Apple | Google AdMob (EL_ADMOB 심볼을 켠 빌드) |
| 웹 | Google, Apple 웹 로그인 | 없음 |
| CrazyGames | 포털 계정, 게스트로 먼저 시작 | 포털 광고 |
| 토스 미니앱 | 토스 자동 로그인 | 토스 앱이 제공하는 광고 |
결제도 갈래가 셋입니다. Google Play 결제는 서버가 영수증으로 검증하고, 웹은 Xsolla 결제창을 쓰고, CrazyGames 는 포털 인증을 거친 뒤 Xsolla 로 결제합니다.
컴파일 심볼로 가릅니다
우리는 플랫폼 차이를 인터페이스보다 조건부 컴파일로 다뤘습니다. 빌드마다 필요 없는 SDK 코드가 아예 들어가지 않기 때문입니다.
| 심볼 | 조건문 수 | 뜻 |
|---|---|---|
| UNITY_WEBGL | 103 | 웹 빌드 전체 |
| EL_CRAZYGAMES | 102 | CrazyGames 포털 빌드 |
| EL_TOSS | 38 | 토스 미니앱 빌드 |
로그인은 시작 화면에서 빌드 종류에 따라 길이 갈립니다. 결과는 모두 서버의 로그인 API 로 모입니다.
#else
#if EL_CRAZYGAMES
// 크레이지게임스 환경에서는 크레이지게임스 로그인 시도
HideLoginPanel();
CheckCrazyGamesUser();
#elif EL_TOSS
HideLoginPanel();
TossAutoLogin();
#else
string lastID = ElCore.GetPlayerPref("LAST_SOCIAL_ID");같은 이름, 다른 구현
광고 관리자는 같은 이름의 클래스를 조건부 컴파일로 네 번 정의합니다. 공개 메서드가 같으므로 광고를 부르는 쪽은 지금 어느 플랫폼인지 모릅니다.
// 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같은 상점 버튼, 세 갈래 결제

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
}어떤 길이든 지급은 서버가 합니다. 클라이언트는 서버가 돌려준 결과로 화면을 갱신합니다.
빌드 매트릭스
빌드 메뉴는 플랫폼과 환경(개발 · QA · 운영)을 곱한 28개입니다. 같은 함수가 커맨드라인 인자도 받아서 빌드 서버에서 그대로 씁니다.
- 플랫폼AOS, iOS, WIN, WebGL, CrazyGames, 토스
- 환경Dev, QA, Product
- 심볼 조합채널 심볼 + 환경 심볼
- 빌드메뉴 또는 빌드 서버
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 를 길들이는 법

CrazyGames SDK 에는 허용 도메인이 아니면 게임을 강제로 끄는 기능이 있습니다. 이 SDK 는 시작할 때 무조건 실행되므로, 그대로 두면 일반 웹 빌드와 토스 빌드가 꺼집니다.
그래서 SDK 코드에 실행 차단 가드를 넣었습니다. 문제는 SDK 를 갱신하면 가드가 원본으로 덮여 조용히 사라진다는 점입니다. 이를 세 겹으로 막았습니다.
- SDK 코드에 가드CrazyGames 빌드가 아니면 실행하지 않음
- 컴파일 시점 검사가드와 함께 넣은 표식을 참조
- 빌드 전 검사빌드 직전에 파일 내용을 확인
#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];
}표식이 사라지면 참조가 깨져 컴파일 오류가 납니다. 버전이 어긋나면 배열 크기가 음수가 되어 역시 컴파일 오류가 납니다. 조용한 런타임 사고를 시끄러운 빌드 실패로 바꾼 셈입니다.
돌아보며
잘된 점
- 프로젝트를 여섯 벌로 복사하지 않고 빌드 설정으로 출시처를 늘렸습니다.
- 광고와 결제를 부르는 쪽은 플랫폼을 모릅니다.
- 빌드에 필요 없는 SDK 코드가 들어가지 않습니다.
- 외부 SDK 갱신 사고를 컴파일 오류로 잡아냅니다.
아쉬운 점
- 조건문이 늘수록 시험해야 할 조합이 빠르게 늘어납니다.
- 결제 사용 여부 심볼을 프로젝트 전역이 아니라 다섯 파일에 따로 정의해 어긋날 위험이 있습니다.
- 벤더 코드를 직접 고치는 방식이라 SDK 를 갱신할 때마다 손이 갑니다.
정리
- 차이는 빌드 시점에 가릅니다. 조건부 컴파일로 플랫폼마다 필요한 코드만 넣습니다.
- 부르는 쪽은 모르게 합니다. 같은 이름과 같은 메서드로 플랫폼별 구현을 숨깁니다.
- 조용한 사고를 시끄럽게 만듭니다. 지켜야 할 가드는 컴파일과 빌드 단계에서 검사합니다.
다음 편에서는 실제 장애에서 시작된 두 가지 재설계, 네트워크 재시도와 에셋 배포 이관을 보겠습니다.