토스 앱에서 SafeArea 정보를 받아와서 제가 유니티 UI에 적용한 방법을 정리했습니다.
저처럼 유니티만 아시는 분들을 위해서, 시행착오를 줄여드리고자 작성했습니다.
대부분의 코드는 AI로 작성되었습니다.
먼저 Vite로 Unity WebGL 감싸기까지 완료합니다.
npm list @apps-in-toss/web-framework로 현재 설치된 버전을 확인합니다. 혹시 1.4.7보다 버전이 작다면, SafeAreainsets를 사용하기 위해서
unity-webgl-wrapper 폴더의 package.json에서
“@apps-in-toss/web-framework”: “^1.4.7”,로 바꿔주고 npm install을 하여 버전을 1.4.7 이상으로 맞춥니다.
- SafeArea.tsx을 작성하여 unity-webgl-wrapper/src/ 폴더에 넣습니다.
// src/SafeArea.tsx
import { SafeAreaInsets } from '@apps-in-toss/web-framework';
import { useEffect } from 'react';
/**
* Unity의 jslib가 @apps-in-toss/web-framework의 SafeAreaInsets 객체에
* 접근할 수 있도록 전역 스코프(window)에 노출시킵니다.
*/
function SafeAreaProvider() {
useEffect(() => {
// jslib가 이 이름을 찾아 사용할 것입니다.
(window as any).TossSafeAreaInsets = SafeAreaInsets;
// cleanup 함수: 컴포넌트가 언마운트될 때 전역 객체를 정리할 수 있습니다.
return () => {
(window as any).TossSafeAreaInsets = undefined;
};
}, []);
// 이 컴포넌트는 UI를 렌더링하지 않습니다.
return null;
}
export default SafeAreaProvider;
- App.tsx에 SafeArea 관련 내용을 추가합니다.
// src/App.tsx
import UnityCanvas from './UnityCanvas';
import SafeAreaProvider from './SafeArea'; /* UI Safe Area */
function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<UnityCanvas />
<SafeAreaProvider /> /* UI Safe Area */
</div>
);
}
export default App;
- SafeAreaInsets.jslib을 작성해 유니티 플러그인을 적용합니다.
// SafeAreaInsets.jslib
mergeInto(LibraryManager.library, {
GetSafeAreaInsets: function() {
var returnStr;
// 1. 보낼 문자열 결정
if (typeof window.TossSafeAreaInsets !== 'undefined') {
var insets = window.TossSafeAreaInsets.get();
// 여기서 window 크기를 같이 묶어서 포장합니다
var payload = {
top: insets.top,
bottom: insets.bottom,
left: insets.left,
right: insets.right,
windowWidth: window.innerWidth,
windowHeight: window.innerHeight
};
returnStr = JSON.stringify(payload);
} else {
returnStr = JSON.stringify({
top: 0, bottom: 0, left: 0, right: 0,
windowWidth: window.innerWidth,
windowHeight: window.innerHeight
});
}
// 2. Unity WebGL 메모리 힙에 문자열을 위한 공간 할당 (매우 중요!)
var bufferSize = lengthBytesUTF8(returnStr) + 1;
var buffer = _malloc(bufferSize);
// 3. 할당된 메모리에 문자열 복사
stringToUTF8(returnStr, buffer, bufferSize);
// 4. 메모리 포인터 반환
return buffer;
},
SubscribeSafeArea: function(gameObjectName) {
if (typeof window.TossSafeAreaInsets !== 'undefined') {
var objName = UTF8ToString(gameObjectName);
window.TossSafeAreaInsets.subscribe({
onEvent: function(insets) {
var payload = {
top: insets.top,
bottom: insets.bottom,
left: insets.left,
right: insets.right,
windowWidth: window.innerWidth,
windowHeight: window.innerHeight
};
SendMessage(objName, 'OnSafeAreaChanged', JSON.stringify(payload));
}
});
}
}
});
jslib 적용 방법은 아래의 글을 참고했습니다.
- 유니티 내에서 SafeArea.cs를 작성합니다.
#if UNITY_WEBGL && !UNITY_EDITOR
using System;
using System.Runtime.InteropServices;
using UnityEngine;
using TMPro;
[System.Serializable]
public class SafeAreaPayload
{
public float top;
public float bottom;
public float left;
public float right;
public float windowWidth; // JS에서 보낸 이름과 똑같아야 함
public float windowHeight; // JS에서 보낸 이름과 똑같아야 함
}
public class SafeArea : MonoBehaviour
{
[DllImport("__Internal")]
private static extern string GetSafeAreaInsets();
[DllImport("__Internal")]
private static extern void SubscribeSafeArea(string gameObjectName);
private RectTransform target;
void Start()
{
target = GetComponent<RectTransform>();
ApplySafeArea();
SubscribeSafeArea(gameObject.name);
}
void ApplySafeArea()
{
string json = GetSafeAreaInsets();
SafeAreaPayload d = JsonUtility.FromJson<SafeAreaPayload>(json);
// RectTransform에 Safe Area 적용
Apply(d);
}
private void Apply(SafeAreaPayload d)
{
// 방어 코드: 너비가 0이면 나눗셈 에러 나므로 리턴
if (target == null || d.windowWidth <= 0 || d.windowHeight <= 0) return;
// CSS 픽셀끼리 나누므로 정확한 비율(0.0 ~ 1.0)이 나옴
float axMin = d.left / d.windowWidth;
float ayMin = d.bottom / d.windowHeight;
float axMax = 1f - (d.right / d.windowWidth);
float ayMax = 1f - (d.top / d.windowHeight);
target.anchorMin = new Vector2(axMin, ayMin);
target.anchorMax = new Vector2(axMax, ayMax);
// 오프셋/포지션/스케일은 초기화
target.offsetMin = Vector2.zero;
target.offsetMax = Vector2.zero;
target.anchoredPosition = Vector2.zero;
target.localScale = Vector3.one;
}
// JavaScript에서 호출
public void OnSafeAreaChanged(string json)
{
SafeAreaPayload d = JsonUtility.FromJson<SafeAreaPayload>(json);
Apply(d);
}
}
#endif
SafeArea.cs는 아래의 글과 동일한 방법으로 UI에 적용합니다.
추가로 참고한 내용: