유니티 SafeArea 적용

토스 앱에서 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 이상으로 맞춥니다.

  1. 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;
  1. 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;
  1. 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 적용 방법은 아래의 글을 참고했습니다.

  1. 유니티 내에서 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에 적용합니다.

추가로 참고한 내용:

7개의 좋아요

:open_mouth: 팁 공유 너무너무 감사합니다!

많은 도움이 됐습니다.
감사합니다. ^^ @kangjui