처음 며칠은 Android 실기기에 빌드를 깔 준비가 안 돼 있었다. 그래도 폰 화면에서 어떻게 보이는지는 빨리 확인하고 싶었다. 그래서 WebGL로 빌드해서 개발 PC의 IIS에 올리고, PC와 폰 브라우저로 열어 보기로 했다.
9월 17일 자정 무렵 첫 WebGL 배포를 커밋했다. 열어 보니 한글이 전부 빈칸이었고, 폰 LTE에서는 아예 열리지 않았다. 두 문제는 그날 오전에 차례로 고쳤다. 이 글은 그 하루의 기록이다.
배치 빌드 스크립트
WebGL 빌드는 에디터 메뉴로도 할 수 있지만, 빌드할 때마다 메뉴를 누르고 결과 폴더를 IIS 경로로 복사하는 일은 번거롭다. 그래서 명령 한 줄로 빌드부터 배포까지 끝나게 했다.
Unity는 -batchmode -executeMethod 클래스.메서드로 에디터 창 없이 정적 메서드 하나를 실행할 수 있다. 진입점은 BuildScript.BuildWebGL이고, 출력 경로를 IIS 물리 경로(D:\app.publish\whopays)로 바로 준다.
$lock = Join-Path $proj "Temp\UnityLockfile"
if (Test-Path $lock) {
if (Get-Process Unity -ErrorAction SilentlyContinue) {
Write-Error "Unity 에디터에 프로젝트가 열려 있습니다(Temp\UnityLockfile). 에디터를 닫고 다시 실행하세요."
}
# Unity 프로세스가 없는데 락 파일만 남은 경우(이전 배치 빌드가 오류로 종료) → 정리
Remove-Item $lock -Force
}
$unityArgs = @(
"-batchmode", "-nographics", "-quit",
"-projectPath", "`"$proj`"",
"-buildTarget", "WebGL",
"-executeMethod", "BuildScript.BuildWebGL",
"-outputPath", "`"$OutputPath`"",
"-logFile", "`"$log`""
)
$p = Start-Process -FilePath $UnityPath -ArgumentList $unityArgs -Wait -PassThru
Select-String -Path $log -Pattern "\[WhoPays\]|error CS|Build completed with a result|BuildFailedException|Aborting batchmode" |
ForEach-Object { $_.Line }
신경 쓴 점은 세 가지다.
- 에디터가 열려 있으면 빌드할 수 없다. 같은 프로젝트를 두 Unity 프로세스가 동시에 열 수 없기 때문이다. 에디터가 떠 있으면 바로 실패시키고 닫으라고 안내한다.
- 잠금 파일만 남는 경우가 있다. 배치 빌드가 오류로 끝나면
Temp\UnityLockfile이 남아 다음 빌드를 막는다. Unity 프로세스가 없는데 잠금 파일만 있으면 지운다. - 로그는 필요한 줄만 뽑는다. 배치 빌드 로그는 길다. 빌드 스크립트가 남기는
[WhoPays]줄, 컴파일 오류(error CS), 빌드 결과 줄만 화면에 보여 주고 종료 코드와 걸린 시간을 찍는다.
빌드가 성공하면 BuildScript가 Tools/webgl.web.config를 출력 폴더에 web.config로 복사한다. IIS가 wasm 같은 확장자를 모르면 파일을 내주지 않기 때문에 MIME을 등록하고, 테스트 중에는 새 빌드가 바로 보이도록 캐시를 끈다.
<staticContent>
<remove fileExtension=".wasm" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
<remove fileExtension=".data" />
<mimeMap fileExtension=".data" mimeType="application/octet-stream" />
<remove fileExtension=".unityweb" />
<mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
<!-- 테스트용: 새 빌드가 바로 반영되도록 캐시 비활성 -->
<clientCache cacheControlMode="DisableCache" />
</staticContent>
IIS 사이트는 setup-iis.ps1이 한 번 만든다. 사이트와 앱 풀(관리 코드 없음), 포트 8091, 「개인」·「도메인」 프로필의 방화벽 인바운드 규칙까지 넣는다. 같은 날 setup-ssl.ps1로 game.addsoft.co.kr 바인딩을 추가하고 win-acme로 Let's Encrypt 인증서를 받아 https로도 열리게 했다. 갱신은 다른 addsoft 사이트와 같은 win-acme 예약 작업이 맡는다.
한글이 전부 빈칸
브라우저로 처음 열어 본 타이틀 화면은 이랬다. 캐릭터와 버튼 배경은 나오는데 글자가 하나도 없다. 제목 자리에는 물음표 하나만 남았다.

원인은 폰트였다. 2편에서 쓴 대로 이 프로젝트는 TextMeshPro 대신 레거시 uGUI Text를 쓰고, 폰트를 따로 지정하지 않으면 Unity 내장 LegacyRuntime.ttf가 쓰인다. 이 폰트에는 한글 글리프가 없다.
에디터나 데스크톱·모바일 네이티브 빌드에서는 이게 드러나지 않았다. 폰트에 없는 글자는 OS 폰트로 대체돼 그려지기 때문이다. WebGL에서는 이 대체가 일어나지 않는다. 그래서 한글은 빈칸이 되고, 내장 폰트에 있는 ?와 v0.1 같은 글자만 남았다.
해결은 한글 폰트를 빌드 안에 넣는 것이다. Noto Sans KR(OFL)을 Regular 굵기로 고정하고 한글 완성형 11,172자와 자모, ASCII, 자주 쓰는 기호만 남긴 서브셋(2.4MB)을 Assets/Resources/Fonts/NotoSansKR.ttf로 만들었다. 이 폰트를 Python으로 생성한 방법은 3편에서 다뤘다.
UI의 모든 텍스트는 UIFactory.Font 하나를 거치므로 고칠 곳도 여기 하나였다.
static Font _font;
public static Font Font
{
get
{
if (_font == null)
{
_font = Resources.Load<Font>("Fonts/NotoSansKR");
if (_font == null)
{
Debug.LogWarning("[UIFactory] Resources/Fonts/NotoSansKR.ttf 가 없어 내장 폰트를 사용합니다. WebGL 에서는 한글이 표시되지 않습니다. (python Tools/gen_font.py)");
_font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
}
}
return _font;
}
}
폰트 파일이 없으면 예전처럼 내장 폰트로 돌아가되, 경고에 원인과 생성 명령을 같이 남긴다. 에디터에서는 폰트가 빠져도 화면이 멀쩡해 보이기 때문에, 경고가 없으면 다시 WebGL에서 빈칸을 보고 나서야 알게 된다. 재빌드 후 localhost:8091에서 제목과 버튼의 한글이 나오는 것을 확인했다.
폰 LTE에서는 아예 열리지 않았다
PC 브라우저에서 잘 되는 것을 보고 폰 LTE로 https://game.addsoft.co.kr을 열었다. 로딩 막대가 한참 차다가 다음 오류가 떴다.

both async and sync fetching of the wasm failed는 wasm 파일을 받지 못했다는 뜻이다. IIS 로그를 보니 브라우저가 40MB짜리 무압축 wasm을 받다가 8MB쯤에서 연결을 끊었다. 첫 빌드는 압축도, 크기 최적화도 하지 않은 상태였다.
그래서 두 방향으로 줄였다. 전송할 때 압축하고, wasm 자체도 작게 만든다.
// 전송 크기: Brotli 압축(.unityweb) + 로더가 직접 압축 해제 → 서버 설정(Content-Encoding) 불필요
PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Brotli;
PlayerSettings.WebGL.decompressionFallback = true;
// wasm 크기: 코드 생성은 속도보다 크기 우선, 안 쓰는 관리 코드·엔진 모듈 제거
PlayerSettings.SetIl2CppCodeGeneration(NamedBuildTarget.WebGL, Il2CppCodeGeneration.OptimizeSize);
PlayerSettings.SetIl2CppCompilerConfiguration(NamedBuildTarget.WebGL, Il2CppCompilerConfiguration.Release);
PlayerSettings.SetManagedStrippingLevel(NamedBuildTarget.WebGL, ManagedStrippingLevel.High);
PlayerSettings.stripEngineCode = true;
// ...
// 이전 빌드의 Build/ 산출물(압축 방식이 바뀌면 파일명이 달라져 남는다) 정리
var oldBuild = Path.Combine(output, "Build");
if (Directory.Exists(oldBuild)) Directory.Delete(oldBuild, true);
결과는 wasm 40MB → 5MB, data 9MB → 2MB였다. 수정 후 다시 올려 로드와 한글 표시를 확인했다.
Build/ 폴더를 지우는 줄은 이 과정에서 생겼다. 압축 방식을 바꾸면 산출물 파일 이름의 확장자가 달라진다. 출력 폴더를 그대로 두고 덮어쓰면 옛 무압축 파일이 그대로 남는다. 빌드마다 Build/를 비우고 새로 만든다.
Content-Encoding 대신 압축 해제 폴백을 고른 이유
Brotli로 압축한 Unity WebGL 빌드를 브라우저에 보내는 방법은 두 가지다.
- 서버가
Content-Encoding: br헤더를 붙인다. 브라우저가 받으면서 스스로 압축을 푼다. 대신 서버에 압축 파일 확장자별로 헤더를 붙이는 설정이 필요하다. 그리고 브라우저는 Brotli 인코딩을 https 연결에서만 받는다. - 압축 해제 폴백(
decompressionFallback)을 켠다. 파일은.unityweb라는 이름으로 나오고, Unity 로더가 받은 뒤 JavaScript로 직접 푼다. 서버는 그냥application/octet-stream으로 내주면 된다.
이 프로젝트는 2번을 골랐다. 테스트 주소가 http://PC IP:8091과 https://game.addsoft.co.kr 두 가지였고, 둘 다 같은 빌드로 열려야 했다. 폴백을 쓰면 http에서도 https에서도 똑같이 동작하고, web.config에는 MIME 한 줄만 있으면 된다.
비용은 있다. 압축 해제를 브라우저 내장 기능이 아니라 로더의 JavaScript가 하므로, 그만큼 로딩 시간에 해제 작업이 더해진다. 테스트용 서버에서는 설정을 줄이는 쪽이 낫다고 판단했다. 해제에 실제로 얼마나 걸리는지는 따로 재지 않았다.
WebGL로 안 되는 것들
WebGL 빌드는 화면과 게임 진행을 확인하는 데는 충분했다. 하지만 브라우저라서 안 되는 일도 분명했다.
- 같이 하기(LAN)가 안 된다. 이 게임의 여러 폰 대전은 방장 폰이 TCP 소켓 서버를 열고 UDP 브로드캐스트로 방을 알린다. 브라우저 안에서는 소켓 서버를 열 수도, UDP를 보낼 수도 없다. 코드는 컴파일되지만 동작하지 않는다.
Application.Quit이 무시된다. 타이틀 화면에서 뒤로 가기를 누르면 앱을 끄는 코드가 있는데, 브라우저 탭은 닫히지 않는다.- 진동이 없다. 진동은 Android에서만 동작하도록
#if UNITY_ANDROID로 감쌌다. - 결과 이미지 공유가 안 된다. 브라우저 전용 분기는
#if UNITY_WEBGL로 나눴다. 공유 버튼을 누르면 "브라우저에서는 이미지 공유를 지원하지 않아요"라고 알린다.
#if UNITY_WEBGL && !UNITY_EDITOR
done(false, "브라우저에서는 이미지 공유를 지원하지 않아요");
#elif UNITY_ANDROID && !UNITY_EDITOR
// MediaStore 저장 후 공유 창
#else
// 에디터·Windows: 파일로 저장
#endif
PC 브라우저에서는 마우스 클릭이 터치 하나로만 들어가서, 눈치 게임처럼 여러 손가락으로 동시에 누르는 게임은 어차피 폰에서 확인해야 했다. 여러 폰 대전을 검토하던 날 배포는 Android 앱으로만 하기로 정했다. WebGL은 이후 개발 중 빠르게 화면을 확인하는 용도로만 남았다.
1시간 38분째 끝나지 않던 빌드
9월 21일에는 WebGL 빌드 하나가 끝나지 않았다. 「눈치게임 색 수정 후 재빌드」라는 이름으로 돌던 build-webgl.ps1 작업이 1시간 38분째 빌드 시작 줄만 찍고 있었다.

평소 몇 분이면 끝나는 빌드라 멈춘 것으로 판단하고 종료했다. 왜 멈췄는지는 기록이 남아 있지 않다.
참고로 10월 4일에 같은 프로젝트를 다시 WebGL로 빌드했을 때는 2분 17초가 걸렸고, 빌드 결과는 7MB였다.
정리
- 배치 빌드는
-batchmode -executeMethod로 돌리고, 에디터 실행 여부와 남은 잠금 파일을 먼저 확인한다. 로그는 필요한 줄만 뽑아 본다. - IIS에는
web.config로 wasm·unityweb MIME을 등록하고, 테스트 중에는 캐시를 끈다. - 레거시 uGUI 내장 폰트에는 한글이 없다. 네이티브에서는 OS 폰트가 대신 그려 주지만 WebGL에서는 빈칸이 된다. 한글 폰트를
Resources에 넣고, 없으면 경고를 남긴다. - 모바일 회선에서 40MB wasm은 무리였다. Brotli + 압축 해제 폴백, IL2CPP 크기 최적화, 스트리핑 High로 wasm을 5MB까지 줄였다. 폴백은 서버 설정이 필요 없는 대신 해제를 로더가 맡는다.
- 소켓 서버와 UDP 브로드캐스트가 필요한 기능은 브라우저에서 안 된다. 그래서 배포는 Android만 한다.
다음 글 5편에서는 등수별 커피값 분담 공식을 다룬다.