Vite로 Unity WebGL 빌드 감싸기
이 가이드는 Unity에서 빌드한 WebGL 파일을 Vite(React 기반) 프로젝트로 감싸는 방법을 안내해요.
앱인토스에 배포하려면 @apps-in-toss/web-framework도 함께 설치해야 해요.
1. Vite 프로젝트 생성
아래 명령어 중 사용하는 패키지 매니저에 맞게 선택해 주세요.
React + TypeScript 템플릿으로 프로젝트가 생성돼요.
npm create vite@latest unity-webgl-wrapper -- --template react-ts
cd unity-webgl-wrapper
npm installpnpm create vite unity-webgl-wrapper --template react-ts
cd unity-webgl-wrapper
pnpm installyarn create vite unity-webgl-wrapper --template react-ts
cd unity-webgl-wrapper
yarn install2. 앱인토스 SDK 설치
프로젝트 루트에서 앱인토스 SDK를 설치해 주세요.
앱인토스 환경에 배포하기 위해서 이 SDK가 꼭 필요해요.
npm install @apps-in-toss/web-framework3. Unity WebGL 빌드 결과물 복사
Unity에서 WebGL 빌드를 완료한 후, 출력 폴더의 구조는 보통 다음과 같아요.
Build/
├── index.html
├── Build/
└── TemplateData/Build/ 안의 파일들을 vite 프로젝트의 public/unity 폴더로 복사해주세요.
mkdir -p public/unity
cp -r [UnityBuildPath]/Build/* public/unity/그 다음 index.html, TemplateData 파일도 public/unity 폴더로 복사해주세요.
복사 후 구조 예시는 다음과 같아요.
public/
└── unity/
├── index.html
├── {YourProject}.data.br
├── {YourProject}.framework.js.br
├── {YourProject}.loader.js
├── {YourProject}.wasm.br
└── TemplateData/TIP
public 폴더에 들어 있는 파일들은 Vite dev 서버에서 정적 파일로 서빙돼요. 즉, /unity/index.html로 접근할 수 있어요.
4. Unity 게임을 보여주는 컴포넌트 만들기
Unity WebGL 빌드 파일을 직접 로드해서 <canvas>에 그리는 컴포넌트를 작성해요.
iframe은 사용할 수 없고, Unity의 createUnityInstance()를 통해 DOM에 직접 렌더링해야 해요.
iframe은 사용할 수 없어요
Unity WebGL을 iframe으로 삽입하면 앱인토스 기능(API, SDK 등)이 정상 동작하지 않아요.
또한 보안 심사에서도 반려될 수 있기 때문에, iframe 방식은 지원하지 않습니다.
React 환경에서 Unity WebGL을 동작시키려면, Unity에서 빌드된 JavaScript 런타임을 직접 불러와서 DOM에 렌더링해 주세요.
// src/UnityCanvas.tsx
import React, { useEffect, useRef, useState } from "react";
/**
* Props
* - basePath: public 폴더 내 unity 빌드가 위치한 경로 (ex: /unity)
* - loaderFile: loader 스크립트 파일명 (ex: "test.loader.js")
* - fileBasename: Unity 빌드 파일의 기본 이름 (ex: "test" -> test.data.br, test.wasm.br 등)
*/
type Props = {
basePath?: string;
loaderFile?: string;
fileBasename?: string;
onProgress?: (p: number) => void;
onLoaded?: () => void;
onError?: (e: Error) => void;
style?: React.CSSProperties;
};
export default function UnityCanvas({
basePath = "/unity",
loaderFile = "test.loader.js",
fileBasename = "test",
onProgress,
onLoaded,
onError,
style,
}: Props) {
const containerRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const unityInstanceRef = useRef<any | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let mounted = true;
const scriptUrl = `${basePath}/${loaderFile}`;
// 동적 스크립트 추가
const script = document.createElement("script");
script.src = scriptUrl;
script.async = true;
script.onload = () => {
if (!mounted) return;
// 캔버스 준비
const container = containerRef.current!;
const canvas = document.createElement("canvas");
canvasRef.current = canvas;
canvas.id = "unity-canvas";
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.style.display = "block";
container.appendChild(canvas);
const createOpts = {
dataUrl: `${basePath}/${fileBasename}.data.br`,
frameworkUrl: `${basePath}/${fileBasename}.framework.js.br`,
codeUrl: `${basePath}/${fileBasename}.wasm.br`,
streamingAssetsUrl: "",
companyName: "YourCompany",
productName: fileBasename,
productVersion: "1.0",
};
if (typeof (window as any).createUnityInstance !== "function") {
const err = new Error("createUnityInstance is not available on window. Loader script may have failed to load.");
console.error(err);
onError?.(err);
setLoading(false);
return;
}
(window as any).createUnityInstance(canvas, createOpts, (progress: number) => {
if (!mounted) return;
onProgress?.(progress);
// optional internal state
}).then((inst: any) => {
if (!mounted) {
// 컴포넌트가 언마운트 되었으면 즉시 종료
if (inst && inst.Quit) inst.Quit();
return;
}
unityInstanceRef.current = inst;
setLoading(false);
onLoaded?.();
}).catch((e: any) => {
console.error("createUnityInstance error:", e);
onError?.(e instanceof Error ? e : new Error(String(e)));
setLoading(false);
});
};
script.onerror = (e) => {
const err = new Error("Failed to load Unity loader script: " + scriptUrl);
console.error(err, e);
onError?.(err);
setLoading(false);
};
document.body.appendChild(script);
return () => {
mounted = false;
// Unity 인스턴스 종료
const inst = unityInstanceRef.current;
if (inst && typeof inst.Quit === "function") {
inst.Quit().catch((err: any) => {
// ignore
console.warn("UnityQuit error", err);
});
}
// 캔버스/스크립트 정리
if (canvasRef.current && containerRef.current?.contains(canvasRef.current)) {
containerRef.current.removeChild(canvasRef.current);
}
try {
document.body.removeChild(script);
} catch {}
};
}, [basePath, loaderFile, fileBasename]);
// SendMessage helper
const sendMessage = (gameObject: string, method: string, value?: string | number | boolean) => {
const inst = unityInstanceRef.current;
if (inst && typeof inst.SendMessage === "function") {
inst.SendMessage(gameObject, method, value);
} else {
console.warn("Unity instance not ready or SendMessage missing");
}
};
return (
<div style={{ width: "100vw", height: "100vh", position: "relative", ...style }}>
{loading && (
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none" }}>
<div>Loading Unity...</div>
</div>
)}
<div ref={containerRef} style={{ width: "100%", height: "100%" }} />
{/* 필요 시 외부에서 sendMessage 사용 가능하게 ref 전달 로직 추가 */}
</div>
);
}src/App.tsx에서 해당 컴포넌트를 불러와 사용해요.
// src/App.tsx
import UnityCanvas from './UnityCanvas';
function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<UnityCanvas />
</div>
);
}
export default App;5. 개발 서버 실행
아래 명령어로 Vite 개발 서버를 실행해 보세요.
npm run dev브라우저에서 Vite 개발 서버 주소(http://localhost:5173 등)로 접속하면 React 앱 안에서 Unity 게임이 정상적으로 렌더링되는지 확인할 수 있어요.
6. 앱인토스 배포환경 구성
개발 서버가 정상적으로 실행되는 것을 확인했다면, 다음 명령어로 프로젝트를 앱인토스 배포환경으로 구성해주세요.
npx ait init명령어 실행 후, 아래와 같은 질문에 순서대로 응답해 주세요.
web-framework를 선택하세요.- 앱 이름(
appName)을 입력하세요.- 이 이름은 앱인토스 콘솔에서 앱을 만들 때 사용한 이름과 같아야 해요.
- 앱 이름은 각 앱을 식별하는 고유한 키로 사용돼요.
- appName은
intoss://{appName}/path형태의 딥링크 경로나 테스트·배포 시 사용하는 앱 전용 주소 등에서도 사용돼요. - 샌드박스 앱에서 테스트할 때도
intoss://{appName}으로 접근해요.
단, 출시하기 메뉴의 QR 코드로 테스트할 때는intoss-private://{appName}이 사용돼요.
- 웹 번들러의 dev 명령어를 입력해주세요.
vite- 웹 번들러의 build 명령어를 입력해주세요.
tsc -b && vite build- 사용할 포트 번호를 입력하세요.
5173초기화가 완료되면 granite.config.ts 파일이 생성돼요. 배포하려는 서비스에 맞게 수정해주세요.
7. 정적 사이트 빌드 및 배포
npm run build빌드가 완료되면 .ait 파일이 생성돼요. 이 파일을 콘솔에 업로드하면 미니앱을 배포할 수 있어요.
