서론

이번 글에서는 Unity as a Library(UAAL) 방식을 활용하여, 유니티로 개발한 앱을 기존 안드로이드 프로젝트나 iOS(Xcode) 프로젝트에 통합하는 방법을 알아보겠습니다. 이를 통해 유니티에서 구현한 기능을 네이티브 모바일 앱 환경에서 직접 동작하도록 적용할 수 있습니다.

준비

  • Unity Version : Unity 6.2
  • XCode : 16.4
  • Android Studio : 2025.1.3
  • iOS Minimum : iOS 15
  • Android Minimum SDK : API 29 (”Q, Android 10.0”)
  • Android Jetpack Compose 사용 시 :
    • appcompat
    • games-activity

본론


Unity

간단하게 3D 모델링으로 레벨 배치만 되어있는 프로젝트를 샘플로 사용하겠습니다.

Android

Android Project로 Export 하기

유니티에서 Android로 빌드하기 전에 Minimum API Level이 몇인지 확인하고 네이티브 안드로이드 앱과 최대한 맞추는 것을 추천합니다.

주의, 유니티의 Minimum API Level 버전은 높은데, 네이티브 프로젝트의 Mimimum API Level 버전이 낮은 경우, 네이티브 프로젝트에서 세팅 다 하고 빌드해보면 빌드 에러 납니다.

Build Profiles에서 Android로 Export할 준비를 합니다, Export Project를 활성화 하고, Export를 눌러서 Android Project로 추출합니다.

Export가 완료된 프로젝트를 Android Studio에서 열면 다음과 같이 2개의 부분으로 구성되어 있습니다.

  • launcher (Application) : Unity 라이브러리를 실행하기 위한 실제 앱
  • unityLibrary (Module) : Unity 엔진과 콘텐츠가 포함된 핵심 라이브러리

대부분 블로그나 포럼에서는 unityLibrary를 가져다 쓰거나 unityClass.jar과 기타 파일을 수동적으로 욺겨서 네이티브 앱에 연동하는 자료들이 많습니다. 이 방법도 틀린 방법은 아니지만 좀 더 사용성이 편한 방법을 알아봅시다.

Android Module을 aar 형태로 빌드하여 제 3자 고객사나 프로젝트에 aar 파일 하나만 전달하면 되도록 해볼 것입니다.

앱 아이콘 2개 생기는 이슈 막기

unityLibrary/manifests폴더로 이동하여 AndroidManifest.xml을 선택하고 다음 내용을 제거합니다.

제거하지 않을 경우, 네이티브 안드로이드 프로젝트에서 최종 빌드하면 두개의 앱 아이콘이 생성되는 이슈가 발생됩니다.

unityLibrary.aar로 빌드하기

unityLibrary를 먼저 선택한 상태에서 상단에 Build/Assemble Module 'Android.unityLibrary'를 선택합니다.

빌드가 완료되면 unityLibrary폴더 경로에 build/outputs/aar에서 unityLibrary-xxx.aar 파일이 생성된 것을 볼 수 있습니다.

iOS

Build Profiles에서 iOS로 빌드를 진행합니다.

빌드가 완료된 프로젝트를 XCode에서 열면 다음과 같이 Products에 4개의 부분으로 구성되어 있습니다
이 중에서 UnityFramework.framework를 빌드하여 네이티브 프로젝트에 가져갈 것입니다.

Data폴더를 UnityFramework에 포함 시키기

그 전에 Data 폴더를 Framework에 포함시키도록 합니다.

Data 폴더를 클릭하고 Show the file Inspector/Target Membership에서 Unity- iPhone -> UnityFramework로 변경합니다.

이 과정을 통해 UnityFramework를 빌드하면 UnityFramework.framework 안에 Data 폴더가 안에 포함됩니다. (기존은 빌드하면 Unity-iPhone에 Data폴더가 포함 되는 구조.)

UnityFramework.framework 빌드하기

상단에 Scheme을 Unity-iPhone을 UnityFramework로 변경재생 버튼을 클릭하여 빌드를 진행해줍니다.

빌드가 완료되면 Products폴더에서 UnityFramework가 활성화 되는 모습을 볼 수 있습니다.
Show in Finder를 클릭했을 때 UnityFramework.framework가 존재하며 그 안에 Data 폴더가 있으면 준비 끝입니다.

Android

android는 UI를 그리는 방식이 XML 방식과 Jetpack Compose 방식이 있습니다.

공통

unityLibrary.aar 임포트하기

네이티브 안드로이드 프로젝트의 libs 폴더에 빌드한 aar 파일을 추가합니다. (libs폴더가 없다면 생성하면 됩니다.)

aar 파일을 마우스 오른쪽 버튼으로 클릭한 뒤, 나타나는 컨텍스트 메뉴에서 Copy Path/Reference...를 선택합니다.

이 중에서 Path from Content Root를 클릭합니다.

안드로이드 스튜디오 상단에 File/Project Structure...를 선택합니다.

Dependencies에서 Modules에 app을 선택하고 JAR/AAR Dependency를 선택합니다.

여기에 aar파일의 경로를 복사한 "libs/unityLibrary-debug.aar" 를 붙여넣고 OK 버튼을 누릅니다.

unityLibrary-debug.aar파일이 추가된 것을 확인했다면 OK를 눌러서 완료합니다.

App의 build.gradle을 보면 다음과 같이 추가된 것을 확인 할 수 있겠습니다.

Android GameSDK 임포트하기 (UnityPlayerGameActivity를 사용하는 경우)

요즘 유니티는 Android를 타겟팅하는 경우 UnityPlayerGameActivity를 밀고 있는 추세입니다.
UnityPlayerGameActivitycom.google.androidgamesdk.GameActivity를 상속받습니다.
이 말은 즉, 여러분들의 네이티브 안드로이드 프로젝트에 gamesdk가 필요하다는 뜻입니다.

UnityPlayerActivity를 사용하는 경우, 이 파트 내용은 생략해도 됩니다.

App의 build.gradle의 dependency에서 다음과 같이 games-activity를 작성하고 Sync Now해줍니다.

정상적으로 임포트되었다면 다음과 같이 com.google.androidgamesdk.GameActivity에 대해서 반응하는 모습을 볼 수 있습니다.

유니티 컨텐츠를 불러내는 방법

다음과 같이 startActivity를 사용하면 유니티 컨텐츠를 오픈할 수 있습니다.

자바

// UnityPlayerActivity를 사용하는 경우
var intent = new Intent(this, UnityPlayerActivity.class);
startActivity(intent);

// UnityPlayerGameActivity를 사용하는 경우
var intent = new Intent(this, UnityPlayerGameActivity.class);
startActivity(intent);

코틀린

// UnityPlayerActivity를 사용하는 경우
val intent = Intent(this, UnityPlayerActivity::class.java)
startActivity(intent)

// UnityPlayerGameActivity를 사용하는 경우
val intent = Intent(this, UnityPlayerGameActivity::class.java)
startActivity(intent)

XML

버튼 눌러 유니티 컨텐츠 열기

xml방식에서 다음과 같이 버튼을 추가해줍니다.
id를 부여하여 setOnClickListener를 적용해줍시다.

다음과 같이 작성하여 유니티로 만든 것을 불러낼 수 있습니다.

Jetpack Compose

xml 방식과 달리 jetpack compose라고 해서 다른 점은 없습니다. 이것 또한 동일하게 startActivity를 통해서 유니티 컨텐츠를 호출합니다.

appcompat 임포트하기 (UnityPlayerGameActivity를 사용하는 경우)

UnityPlayerGameActivityGameActivity를 상속하고, GameActivity는 다시 AppCompatActivity를 상속합니다.
하지만 Jetpack Compose는 기본적으로 AppCompatActivity를 사용하지 않습니다.
따라서 Jetpack Compose 프로젝트에서 Unity를 연동할 경우, AppCompatActivity를 추가로 임포트해야 합니다.

계층 구조는 다음과 같습니다:

UnityPlayerGameActivity <- GameActivity <- AppCompatActivity

UnityPlayerActivity를 사용하는 경우, 이 파트 내용은 생략해도 됩니다.

App의 build.gradle.kts 파일로 이동하여 appcompat 라이브러리를 추가한 뒤 Sync Now를 클릭해 프로젝트에 반영합니다.

Sync가 완료되면 AppCompatActivity를 정상적으로 인식할 수 있습니다.

버튼 눌러 유니티 컨텐츠 열기

다음과 같이 화면 중앙에 버튼을 위치시키는 간단한 UI를 구성해봅니다.

다음과 같이 작성하여 유니티로 만든 것을 불러낼 수 있습니다.
이때 context는 val context = LocalContext.current을 작성하여 사용할 수 있습니다.

iOS

iOS에는 UI를 그리는 방식이 Storyboard 방식과 SwiftUI 방식이 있습니다.

공통

Supported Platforms - iPhoneos로 제한하기

기본적으로 iOS 앱을 타겟팅하는 프로젝트는 Supported Platforms는 iOS로 되어 있습니다.
iOS라는 뜻은 실제 기기로 빌드할 수 있고, 시뮬레이터로 테스트 해볼 수 있다는 뜻입니다.

유니티의 경우 아쉽게도 이 둘을 모두 지원할 수 없습니다.
Target SDK를 디바이스만 지원하거나, 시뮬레이터만 지원하도록 둘 중 하나를 선택해야합니다.
일반적으로 Device SDK를 선택하여 빌드를 합니다.

Xcode 프로젝트에서 PROJECT -> Build Settings로 이동하여 Supported Platforms를 변경해봅니다. 드롭다운을 클릭하여 Other...을 클릭합니다.

iphonesimulator를 선택하고 - 표시를 클릭하여 제거해줍니다.
최종적으로 Supported Platforms가 iphoneos로 단독 타겟팅 되어있는지 확인합니다.

UnityFramework.framework 임베드 하기

네이티브 프로젝트에 Framework파일을 관리할 폴더를 생성합니다.

UnityFramework.framework를 네이티브 프로젝트에 드래그 & 드롭으로 추가합니다.

네이티브 프로젝트에서 TARGETS을 선택한 후 'General' 탭으로 이동합니다.
'Frameworks, Libraries, and Embedded Content' 섹션에서 UnityFramework.frameworkEmbed 옵션을 설정을 Embed & Sign으로 지정합니다.

Embed & Sign : 프레임워크를 앱 번들에 복사하고, 개발자의 인증서로 코드 서명을 합니다.

Copy Bundle Resources에 Data폴더 추가 하기

TARGETS을 선택한 후 'Build Phases' 탭으로 이동합니다.
UnityFramework.framework 내부에 있는 Data폴더를 Copy Bundle Resources에 드래그 & 드롭으로 추가합니다.

  • Added folders : Create folder references 선택

유니티 컨텐츠를 불러내는 방법

iOS에서는 유니티 컨텐츠를 불러내는 방법이 여러가지가 있겠으나, 제일 안정적인 방법으로 소개하고자 합니다.
몇몇 포럼에서는 iOS만 유독 앱 초기에 로드가 가능한 것처럼 소개하지만 안정성이 우려되는 부분이 있습니다.
식별된 문제들 :

  • Linear Color Space에서는 동작이 안되는 이슈 (Gamma에서는 정상 작동)
  • KeyboardDelegate.h에서 문제가 발생

이러한 안정성만 해결된다면 초기에 로드해서 앱 실행 속도를 절약할 수 있다면 매우 좋을 듯 하여 위 부분이 해결되면 추가 포스팅하여 소개하도록 하겠습니다.


앱의 메인 번들 내 Frameworks 폴더에 있는 UnityFramework.framework를 동적으로 메모리에 로드합니다.
로드된 번들에서 principalClass를 가져옵니다. 유니티의 경우 UnityFramework 라는 클래스 입니다.
principalClass.getInstance()를 반환하여 Unity를 시작하거나, 데이터를 통신하는 등 처리를 할 수 있습니다.

private func loadUnityFramework() -> UnityFramework? {
    let frameworkName = "UnityFramework.framework" // 우리가 새로 정한 이름
    let bundlePath = Bundle.main.bundlePath + "/Frameworks/" + frameworkName

    guard let bundle = Bundle(path: bundlePath),
          let principalClass = bundle.principalClass as? UnityFramework.Type else {
        print("❌ \(frameworkName) 번들을 로드하는데 실패했습니다.")
        return nil
    }
    return principalClass.getInstance()
}

이전에 설명한 loadUnityFramework() 함수를 호출하여, 앱에 내장된 Unity 프레임워크를 메모리에 로드하고 그 인스턴스를 가져옵니다. self는 이 showUnity 함수를 포함하고 있는 클래스의 인스턴스를 의미합니다.
이것을 Unity 프레임워크에 등록(register)함으로써, Unity 쪽에서 발생하는 특정 이벤트나 메시지를 네이티브 iOS 코드에서 받을 수 있습니다. (예 : SendMessage)
runEmbedded(withArgc, argv, appLaunchOpts)를 호출함으로써 Unity 런타임이 초기화되고, 네이티브 iOS 앱의 화면 위에 Unity 컨텐츠가 렌더링됩니다.

public func showUnity() {
    framework = loadUnityFramework()
    framework?.register(self) // Unity로부터 콜백(ex: unityDidLaunch)을 받기 위해 등록
    framework?.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: nil)
}

위에서 소개한 내용들을 토대로 UnityManager.swift라는 싱글턴 클래스를 프로젝트에 추가하여 손쉽게 유니티 컨텐츠를 로드하도록 해봅니다.

import Foundation
import UnityFramework

public class UnityManager: NSObject, UnityFrameworkListener {
    
    public static let shared = UnityManager()
    
    private var framework: UnityFramework?
    
    /// 유니티 컨텐츠를 동작시킵니다.
    public func showUnity() {
        framework = loadUnityFramework()
        framework?.register(self) // Unity로부터 콜백(ex: unityDidLaunch)을 받기 위해 등록
        framework?.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: nil)
    }
    
    private func loadUnityFramework() -> UnityFramework? {
        let frameworkName = "UnityFramework.framework" // 우리가 새로 정한 이름
        let bundlePath = Bundle.main.bundlePath + "/Frameworks/" + frameworkName
        
        guard let bundle = Bundle(path: bundlePath),
              let principalClass = bundle.principalClass as? UnityFramework.Type else {
            print("❌ \(frameworkName) 번들을 로드하는데 실패했습니다.")
            return nil
        }
        return principalClass.getInstance()
    }
}

Storyboard

Main.storyboard에 버튼을 추가하고 버튼 클릭 이벤트를 구성해봅니다.

이 버튼을 터치했을 때 우리가 만든 Unity 콘텐츠가 실행되도록, 버튼의 동작(action) 부분에 UnityManager.shared.showUnity() 코드를 추가해 주기만 하면 됩니다.

SwiftUI

ContentView에 버튼을 추가하고 버튼 클릭 이벤트를 구성 해봅니다.

이 버튼을 터치했을 때 우리가 만든 Unity 콘텐츠가 실행되도록, 버튼의 동작(action) 부분에 UnityManager.shared.showUnity() 코드를 추가해 주기만 하면 됩니다.

제한 사항

공통

  1. 너무나 마이너한 개발 파트이기 때문에 자료가 생각외로 많이 없다.
  2. SDK 배포를 Moven이나 SwiftManager를 통해 하고 싶지만 용량 이슈로 불가한 경우가 있다.

Android

  1. 앱이 실행될 때 초기화 하고, 그 이후에 빠르게 실행 불가
  2. StartActivity를 사용하지 않고 Jetpack Compose 컨셉에 맞게 재 구성하기 어려움

iOS

  1. 앱이 실행될 때 초기화 하고, 그 이후에 빠르게 실행하기 어려움
  2. 인터넷에 여러 방법으로 개발 내용이 소개되어 있어서 현재는 불가능한 방식도 두루 존재.
  3. UnityFramework.framework를 XCode에 가져오는 순간 시뮬레이터는 사용 불가를 염려해야함.
  4. UnityFramework를 XCframework로 만들면 해결될 듯 한데.. 이것도 난이도가 쫌 있고 후다닥 되는 기술이 아니라서 추천하기 어려움.. ㅜ
profile
유니티를 통한 스페셜 테크닉만 다루는 독특한 개발자

3개의 댓글

자료 잘 없는 분야 직접 파보시면서 테스트해보시는 모습 멋지다고 생각합니다

1개의 답글
comment-user-thumbnail
2일 전

야 직접 파보시면서 테스트해보시는 모습 멋지다고 생각 https://convertimageto3d.com/

답글 달기