들어가며: 왜 플러터(Flutter)와 유니티(Unity)를 함께 사용해야 할까?
애플리케이션 개발의 세계는 끊임없이 진화하고 있습니다. 사용자들은 더 이상 단순히 기능만 갖춘 앱에 만족하지 않습니다. 아름답고 직관적인 UI(사용자 인터페이스)와 더불어, 몰입감 넘치는 인터랙티브 경험을 원합니다. 바로 이 지점에서 두 거인, 플러터와 유니티의 만남이 필연적으로 떠오릅니다.
플러터(Flutter)는 구글이 개발한 UI 툴킷으로, 단일 코드베이스로 iOS, Android, 웹, 데스크톱에서 네이티브 수준의 성능과 아름다운 UI를 구현하는 데 독보적인 강점을 가집니다. 빠르고 유연하며, 생산성이 매우 높죠. 하지만 복잡한 3D 그래픽, 물리 엔진, 고사양 게임과 같은 콘텐츠를 직접 구현하기에는 한계가 명확합니다.
반면, 유니티(Unity)는 세계 최고의 리얼타임 3D 개발 플랫폼입니다. 게임 개발은 물론, 건축 시각화, AR(증강현실), VR(가상현실), 디지털 트윈 등 몰입형 콘텐츠 제작에 있어서는 대체 불가능한 존재입니다. 하지만 유니티의 기본 UI 시스템(UGUI)은 일반적인 애플리케이션의 복잡하고 동적인 UI를 만드는 데 있어 플러터만큼 유연하거나 효율적이지 못합니다.
이 둘을 연동한다는 것은, 각자의 단점을 보완하고 장점만을 극대화하는 전략입니다. 즉, 앱의 전체적인 뼈대와 UI는 플러터로 빠르고 세련되게 구축하고, 3D 모델 뷰어, 미니 게임, AR 기능 등 고도의 그래픽 처리가 필요한 부분만 유니티로 제작하여 플러터 앱 안에 '위젯'처럼 삽입하는 것입니다. 이는 마치 잘 지어진 아파트(플러터 앱)에 최첨단 홈 시네마(유니티 뷰)를 설치하는 것과 같습니다.
핵심 원리와 적용 시나리오
어떻게 연동되는가?
플러터와 유니티 연동의 핵심은 '네이티브 통합'에 있습니다. 직접적으로 두 프레임워크가 소통하는 것이 아니라, 각 플랫폼(Android, iOS)의 네이티브 영역을 경유하여 다리(Bridge)를 놓는 방식입니다.
- 플러터 앱이 주가 됩니다. 사용자는 플러터로 만들어진 UI를 통해 앱과 상호작용합니다.
- 특정 화면이나 위젯이 필요한 시점에, 플러터는 네이티브 코드(Android의 경우 Java/Kotlin, iOS의 경우 Objective-C/Swift)를 호출하여 유니티 '뷰(View)'를 띄워달라고 요청합니다.
- 유니티 프로젝트는 일반적인 게임 앱이 아닌, 네이티브 라이브러리(Android의 경우 .AAR, iOS의 경우 Framework) 형태로 빌드됩니다.
- 네이티브 코드는 이 라이브러리를 로드하여 화면의 특정 영역에 유니티 씬(Scene)을 렌더링합니다. 이 렌더링된 화면이 플러터 위젯 트리 상에 표시됩니다.
- 데이터 통신은 이 네이티브 다리를 통해 양방향으로 이루어집니다. 예를 들어 플러터의 버튼을 누르면, `플러터 → 네이티브 → 유니티` 순서로 메시지가 전달되어 유니티 씬의 3D 모델 색상을 바꿀 수 있습니다. 반대로, 유니티 씬에서 특정 오브젝트를 터치하면 `유니티 → 네이티브 → 플러터` 순서로 이벤트가 전달되어 플러터의 텍스트 위젯 내용을 업데이트할 수 있습니다.
이 복잡한 과정을 쉽게 구현할 수 있도록 도와주는 것이 바로 flutter_unity_widget
같은 오픈소스 패키지입니다. 이 패키지는 위에서 설명한 네이티브 브릿지 코드를 추상화하여, 개발자가 플러터 코드 상에서 `UnityWidget`이라는 위젯을 사용하는 것만으로 간단히 유니티 뷰를 임베드하고 통신할 수 있게 해줍니다.
주요 적용 시나리오
- 이커머스 앱의 3D 제품 뷰어: 가구, 자동차, 신발 등 제품을 360도 돌려보고, 색상을 바꿔보는 기능을 유니티로 구현하여 상품 상세 페이지에 삽입합니다.
- 가구/인테리어 앱의 AR 배치 기능: 플러터로 만든 앱에서 'AR로 보기' 버튼을 누르면 유니티의 AR Foundation 기반 뷰가 활성화되어, 현실 공간에 가구를 배치해볼 수 있습니다.
- 교육용 앱의 인터랙티브 콘텐츠: 인체 해부도, 행성 모델, 공룡 등을 3D로 보여주며 사용자가 직접 조작하고 학습할 수 있는 모듈을 유니티로 제작합니다.
- 기업용 앱의 설비/건물 디지털 트윈: 공장 설비나 건물의 데이터를 3D 모델과 연동하여 시각화하고, 특정 부품을 클릭하면 플러터 UI에 상세 정보가 표시되도록 합니다.
- 일반 앱 속의 미니 게임: 앱의 주요 기능과는 별개로, 사용자 참여를 유도하기 위한 간단한 3D 미니 게임을 유니티로 만들어 이벤트 페이지 등에 포함시킬 수 있습니다.
실전 연동 과정 (flutter_unity_widget 기준)
이론은 충분히 알았으니, 이제 실제 구현 과정을 간략하게 살펴보겠습니다. 상세한 설정은 패키지 버전에 따라 달라질 수 있으므로 공식 문서를 항상 참조하는 것이 좋습니다.
1. 플러터 프로젝트 설정
먼저, 플러터 프로젝트의 `pubspec.yaml` 파일에 `flutter_unity_widget` 의존성을 추가합니다.
dependencies:
flutter:
sdk: flutter
flutter_unity_widget: ^2022.2.0
그 후 `flutter pub get` 명령어로 패키지를 설치합니다.
2. 유니티 프로젝트 설정 및 빌드
- 유니티 허브에서 새 3D 프로젝트를 생성합니다.
- `flutter_unity_widget` 패키지의 Unity 소스를 다운로드받아 유니티 프로젝트의 `Assets` 폴더에 `unity-v2` 또는 유사한 이름의 폴더를 생성하고 그 안에 넣습니다. 이 폴더에는 플러터와의 통신을 위한 스크립트와 빌드 설정이 포함되어 있습니다.
- `Tools/Flutter/Export (Android)` 또는 `Export (iOS)` 메뉴를 사용하여 프로젝트를 네이티브 라이브러리 형태로 빌드합니다.
- Android: 빌드가 완료되면 플러터 프로젝트의 `android/unityLibrary` 와 같은 경로에 .AAR 파일과 관련 리소스가 생성됩니다.
- iOS: 빌드가 완료되면 `ios/UnityLibrary` 와 같은 경로에 Xcode 프로젝트가 생성됩니다.
이 과정은 패키지가 제공하는 자동화 스크립트에 의해 대부분 처리됩니다.
3. 플러터 위젯에 유니티 뷰 추가하기
이제 플러터 코드에서 유니티 뷰를 위젯으로 사용할 수 있습니다. `UnityWidget`을 화면에 배치하고, 컨트롤러를 통해 상호작용합니다.
import 'package:flutter/material.dart';
import 'package:flutter_unity_widget/flutter_unity_widget.dart';
class UnityDemoScreen extends StatefulWidget {
@override
_UnityDemoScreenState createState() => _UnityDemoScreenState();
}
class _UnityDemoScreenState extends State<UnityDemoScreen> {
static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
UnityWidgetController? _unityWidgetController;
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(title: Text('Flutter & Unity Demo')),
body: Card(
margin: const EdgeInsets.all(8),
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
child: Stack(
children: <Widget>[
UnityWidget(
onUnityCreated: onUnityCreated,
onUnityMessage: onUnityMessage,
onUnitySceneLoaded: onUnitySceneLoaded,
),
Positioned(
bottom: 20,
right: 20,
child: ElevatedButton(
onPressed: () {
// 플러터에서 유니티로 메시지 전송
changeCubeColor();
},
child: Text('Change Color'),
),
),
],
),
),
);
}
// 유니티 씬 로드가 완료되면 호출
void onUnityCreated(controller) {
this._unityWidgetController = controller;
}
// 유니티로부터 메시지를 수신하면 호출
void onUnityMessage(message) {
print('Received message from Unity: ${message.toString()}');
// 예를 들어, 유니티에서 보낸 점수를 플러터 UI에 표시
}
// 유니티 씬 로드 상태 변경 시 호출
void onUnitySceneLoaded(SceneLoaded? scene) {
if (scene != null) {
print('Received scene loaded from Unity: ${scene.name}');
}
}
// 유니티로 메시지를 보내는 함수 예시
void changeCubeColor() {
_unityWidgetController?.postMessage(
'Cube', // 유니티 내 GameObject 이름
'ChangeColor', // 호출할 C# 스크립트의 메서드 이름
'#FF0000', // 전달할 파라미터
);
}
}
4. 유니티에서 플러터와 통신하기
유니티에서는 플러터로부터 메시지를 수신하고, 플러터로 메시지를 보낼 수 있는 C# 스크립트를 작성해야 합니다. `flutter_unity_widget`에서 제공하는 `UnityMessageManager`를 사용합니다.
using UnityEngine;
// flutter_unity_widget에서 제공하는 통신 스크립트 사용
using FlutterUnityIntegration;
public class CubeController : MonoBehaviour
{
// 이 메서드는 플러터의 postMessage를 통해 호출됩니다.
public void ChangeColor(string colorCode)
{
// 색상 코드를 Color 객체로 변환
Color newColor;
if (ColorUtility.TryParseHtmlString(colorCode, out newColor))
{
GetComponent<Renderer>().material.color = newColor;
}
// 작업 완료 후 플러터로 메시지 전송
SendStateToFlutter();
}
// 마우스 클릭 시 플러터로 이벤트 전송
private void OnMouseDown()
{
UnityMessageManager.Instance.SendMessageToFlutter("CubeClicked");
}
// 현재 큐브 색상 정보를 플러터로 전송하는 예시
private void SendStateToFlutter() {
string currentColor = "#" + ColorUtility.ToHtmlStringRGB(GetComponent<Renderer>().material.color);
UnityMessageManager.Instance.SendMessageToFlutter("Cube color is now " + currentColor);
}
}
위 예시처럼, 플러터와 유니티는 GameObject 이름과 메서드 이름을 키(key)로 삼아 문자열 데이터를 주고받으며 긴밀하게 상호작용할 수 있습니다.
반드시 고려해야 할 사항들
플러터와 유니티 연동은 강력한 만큼, 신중하게 접근해야 할 몇 가지 과제가 있습니다.
- 앱 용량 증가: 유니티 엔진과 3D 에셋들이 포함되므로 순수 플러터 앱에 비해 최종 빌드된 앱의 크기가 상당히 커집니다. 모바일 환경에서는 민감한 문제일 수 있습니다.
- 성능 및 메모리 관리: 두 개의 고성능 프레임워크가 동시에 실행되는 것이므로, 특히 저사양 기기에서는 메모리 사용량과 배터리 소모가 많아질 수 있습니다. 유니티 씬의 최적화가 필수적이며, 유니티 뷰가 화면에 보이지 않을 때는 일시정지(pause)시키는 등 생명주기(Lifecycle) 관리가 중요합니다.
- 빌드 복잡성: 플러터와 유니티라는 두 개의 다른 생태계의 빌드 파이프라인을 모두 관리해야 합니다. 버전 호환성 문제나 빌드 설정 오류가 발생할 가능성이 더 높습니다.
- 디버깅의 어려움: 문제가 발생했을 때, 이것이 플러터의 문제인지, 유니티의 문제인지, 아니면 둘 사이의 통신(브릿지) 문제인지 파악하기가 더 까다로울 수 있습니다.
결론: 현명한 선택과 집중
플러터와 유니티를 함께 사용하는 것은 '모든 문제를 해결하는 만능 열쇠'가 아닙니다. 이는 분명 '고급 기술'에 속하며, 프로젝트의 요구사항이 이 기술을 사용했을 때 얻는 이점이 앞서 언급한 단점들(용량, 성능, 복잡성)을 감수할 만큼 충분히 클 때 선택해야 하는 전략적 카드입니다.
단순히 3D 모델 하나를 보여주는 것이 목적이라면, 플러터에서 직접 3D 렌더링을 지원하는 `model_viewer_plus`와 같은 가벼운 패키지를 사용하는 것이 더 현명할 수 있습니다.
하지만 사용자와의 실시간 상호작용이 필요한 복잡한 3D 환경, AR 기능, 물리 시뮬레이션 등이 앱의 핵심적인 경험이라면, 플러터와 유니티의 조합은 다른 어떤 기술로도 대체하기 어려운 강력한 시너지를 발휘할 것입니다. 이 조합을 통해 개발자는 빠르고 아름다운 UI와 몰입감 넘치는 3D 경험이라는 두 마리 토끼를 모두 잡고, 사용자에게 전에 없던 새로운 가치를 제공하는 애플리케이션을 만들어낼 수 있습니다.
프로젝트의 본질을 꿰뚫고, 기술의 장단점을 명확히 이해하여 가장 적합한 도구를 선택하는 것, 그것이 바로 뛰어난 개발자의 역량일 것입니다. 플러터와 유니티 연동은 그 선택지 중 하나로서 당신의 개발 무기고를 더욱 풍성하게 만들어 줄 것입니다.
0 개의 댓글:
Post a Comment