10 KiB
10 KiB
Unity 개발 지침 (UI Toolkit · MVVM · 성능 · Nullable)
본 지침은 Unity UI Toolkit 기반 프로젝트의 아키텍처, 성능, 코드 품질을 일관되게 유지하기 위한 규칙입니다.
프로젝트 스타일: #nullable enable, UniTask, DOTween, 한국어 주석
1) 핵심 원칙
UI 프레임워크
- UI Toolkit(UIElements) 필수 사용. uGUI(Canvas 기반)는 레거시로 취급합니다.
- UXML(구조)과 USS(스타일)를 분리하고, C# 코드에서 인라인 스타일 지정을 지양합니다.
아키텍처 (MVVM/MVC)
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ View │◄────│ ViewModel │◄────│ Model │
│ (UXML/USS) │ │ (Presenter) │ │ (Service) │
└─────────────┘ └──────────────┘ └─────────────┘
│ │
└── 이벤트 전달 ──────┘
| 레이어 | 책임 | 금지 사항 |
|---|---|---|
| View | 표시/레이아웃, 이벤트 라우팅 | 비즈니스 로직, 상태 보유 |
| ViewModel/Presenter | 상태 관리, 데이터 변환, 바인딩 속성 | Unity API 직접 호출 (테스트 용이) |
| Model/Service | 도메인 로직, 데이터 접근 | UI 참조 |
- MVVM: UI 상태/양방향 동기화가 많을 때
- MVC: 입력 → 도메인 액션 → UI 반영 흐름이 단순할 때
필수 규약
- 파일 선두에
#nullable enable, 모든 참조형에?명시 - 비동기는
UniTask+CancellationToken사용 (Task/코루틴 지양) - 느슨한 결합: 인터페이스/이벤트로 연결
2) 폴더 구조
Assets/
├── Scripts/
│ ├── {프로젝트명}/ # 프로젝트별 코드
│ │ ├── Config/ # 설정, 상수
│ │ ├── Manager/ # 매니저 클래스
│ │ ├── Command/ # Command 패턴 (Undo/Redo)
│ │ └── ...
│ └── UVC/ # 공통 라이브러리 ⭐
│ ├── Core/ # DI, Injector, Singleton
│ ├── Data/ # DataMapper, MQTT/HTTP 통신
│ ├── Pool/ # 오브젝트 풀링
│ ├── UI/ # uGUI 컴포넌트 (Modal, Tab)
│ └── UIToolkit/ # UI Toolkit 컴포넌트 ⭐
├── Resources/
│ ├── {프로젝트명}/ # 프로젝트별 리소스
│ │ ├── Materials/
│ │ ├── Models/
│ │ └── Prefabs/
│ └── UIToolkit/ # 공통 UI 리소스 ⭐
│ ├── Common/ # 공통 스타일 (USS)
│ ├── List/ # 리스트 컴포넌트 (UXML)
│ ├── Modal/ # 모달 컴포넌트 (UXML)
│ ├── Property/ # 속성 편집기 (UXML)
│ └── Window/ # 윈도우 컴포넌트 (UXML)
├── Plugins/ # 서드파티 (Best.HTTP, DOTween 등)
├── Sample/ # 샘플 씬
└── Scenes/ # 앱 씬
현재 프로젝트:
| 폴더 | 설명 |
|---|---|
Scripts/Factory |
스마트 팩토리 3D 시각화 (MQTT 실시간) |
Scripts/Simulator |
Factory 시뮬레이션 버전 |
Scripts/Studio |
3D 씬 에디터 (Undo/Redo, Gizmo) |
Scripts/SHI |
조선소 공정 모달 (TreeList, Chart) |
Scripts/NHN |
무한 스크롤 컴포넌트 (uGUI 레거시) |
참고: 각 폴더에
CLAUDE.md파일이 있어 모듈별 상세 가이드를 제공합니다.
3) 성능 최적화
VisualElement 쿼리
// ❌ 나쁜 예: 매 프레임 쿼리
void Update() {
rootVisualElement.Q<Label>("title").text = _title;
}
// ✅ 좋은 예: 캐싱
private Label? _titleLabel;
void OnEnable() {
_titleLabel = rootVisualElement.Q<Label>("title");
}
체크리스트
Q<T>(),Query<T>()결과는 필드에 캐싱- 변경된 데이터만 업데이트 (전체 리빌드 지양)
- 대량 목록은
ListView/TreeView가상화 활용 - USS 선택자 복잡도 최소화 (
>중첩, 와일드카드 지양) - 동적 생성/파괴 대신 풀링 또는
display: none토글 - 지연/반복 작업은
schedule.Execute()사용 - Update에서 GC 할당 금지 (LINQ/문자열 연결/클로저 지양)
- DOTween: 핸들 보관, 수명 종료 시
Kill()
4) 메모리 관리
이벤트 구독/해제
private EventCallback<ClickEvent>? _onClick;
void OnEnable() {
_onClick = OnButtonClick;
_button?.RegisterCallback(_onClick);
}
void OnDisable() {
_button?.UnregisterCallback(_onClick);
}
체크리스트
RegisterCallback<T>↔UnregisterCallback<T>대칭 확인CancellationTokenSource는OnDestroy에서Cancel/Dispose- VisualTreeAsset/USS 동일 리소스 반복 로드 방지 (캐싱)
- 클로저/람다 캡처로 인한 누수 점검
- 오브젝트 풀:
IPoolable.OnRent/OnReturn훅, 반환 시DOTween.Kill()
5) 비동기 (UniTask)
// 공개 API는 UniTask 반환, CancellationToken 필수
public async UniTask<Data?> LoadDataAsync(CancellationToken ct)
{
var result = await _repository.FetchAsync().AttachExternalCancellation(ct);
return result;
}
// Fire-and-forget은 예외 로깅 후 .Forget()
LoadDataAsync(_cts.Token).Forget(ex => Debug.LogError(ex));
규칙
async void지양,UniTask/UniTask<T>반환- 토큰 결합:
CreateLinkedTokenSource(parent, local) - 타임아웃:
cts.CancelAfter(TimeSpan.FromSeconds(5)) - CPU 바운드:
UniTask.Run→UniTask.SwitchToMainThread
6) 리소스 로드 (Addressables/Resources)
[Serializable]
public struct AssetRef<T> where T : UnityEngine.Object
{
[SerializeField] private string _path;
public async UniTask<T?> LoadAsync(CancellationToken ct = default)
{
#if USE_ADDRESSABLES
var handle = Addressables.LoadAssetAsync<T>(_path);
await handle.Task.AsUniTask().AttachExternalCancellation(ct);
return handle.Status == AsyncOperationStatus.Succeeded ? handle.Result : null;
#else
var request = Resources.LoadAsync<T>(_path);
await request.AsUniTask(cancellationToken: ct);
return request.asset as T;
#endif
}
}
- Addressables 사용 시
USE_ADDRESSABLES전처리기 정의 LoadAssetAsync핸들은 수명 관리 후Release- GameObject 직접 참조 대신 경로 문자열 직렬화
7) USS 스타일 가이드
디자인 참조
UI Toolkit 개발 시 StyleGuide/ 폴더의 이미지를 참조하세요:
| 파일 | 설명 |
|---|---|
style_guide_Colors.png |
색상 팔레트 |
style_guide_Typography.png |
타이포그래피 (폰트, 크기) |
style_guide_Buttons.png |
버튼 스타일 |
style_guide_Text Field.png |
텍스트 필드 |
style_guide_Dropdowns.png |
드롭다운 |
style_guide_Checkbox.png |
체크박스 |
style_guide_List.png |
리스트 |
style_guide_Tabs.png |
탭 |
style_guide_Panel.png |
패널 |
style_guide_Sidebar.png |
사이드바 |
style_guide_Menu.png |
메뉴 |
style_guide_Dialogs.png |
다이얼로그/모달 |
style_guide_Notifications.png |
알림 |
style_guide_Status Bar.png |
상태 바 |
style_guide_Extensions.png |
확장 컴포넌트 |
style_guide_Templates.png |
템플릿 |
BEM 네이밍
.panel { }
.panel__header { }
.panel__header--highlighted { }
.panel__content { }
규칙
- 반복 값은 USS 변수 사용:
--color-primary,--spacing-md - 라이트/다크 테마는 별도 USS로 분리
- 새 UI 컴포넌트 개발 시 StyleGuide 이미지와 일치하는 스타일 적용
8) 주석 원칙 (C# XML)
/// <summary>
/// 사용자 데이터를 비동기로 로드합니다.
/// </summary>
/// <param name="userId">사용자 ID.</param>
/// <param name="ct">취소 토큰.</param>
/// <returns>사용자 데이터 또는 null.</returns>
public async UniTask<UserData?> LoadUserAsync(string userId, CancellationToken ct)
- 클래스: 역할/책임/사용 예
- 메서드: 요약 + 파라미터/반환 (복잡 로직만 상세)
- 속성: 한 줄 요약
9) 디자인 패턴 요약
| 패턴 | 사용 시점 |
|---|---|
| DI/Composition Root | 서비스/ViewModel 주입 일원화 |
| Event Aggregator | 컴포넌트 간 느슨한 통신 |
| Command + Undo | UI 액션에 되돌리기 필요 시 |
| Strategy | 정렬/필터 규칙 교체 |
| State/FSM | 모드 전환 (편집/선택/드래그) |
| Factory | 뷰/프리팹 생성 캡슐화 |
| Object Pool | 대량 아이템 재사용 |
| Repository | 데이터 소스 추상화 |
10) View 기본 패턴
#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
/// <summary>
/// UI Toolkit View 기본 구조.
/// </summary>
public abstract class UIViewBase : IDisposable
{
protected VisualElement Root { get; private set; } = null!;
protected CancellationTokenSource? Cts { get; private set; }
public virtual void Initialize(VisualElement root)
{
Root = root ?? throw new ArgumentNullException(nameof(root));
Cts = new CancellationTokenSource();
QueryElements();
RegisterEvents();
}
protected abstract void QueryElements();
protected abstract void RegisterEvents();
protected abstract void UnregisterEvents();
public virtual void Dispose()
{
UnregisterEvents();
Cts?.Cancel();
Cts?.Dispose();
Cts = null;
}
}
11) Unity Nullable 주의
// Unity Object는 == null 오버로드됨
if (gameObject == null) { } // 파괴된 객체도 true
// 순수 참조 null 확인 시
if (ReferenceEquals(obj, null)) { }
- 직렬화 필드에
?표기, 런타임 Null/파괴 상태 방어적 처리
12) 품질 자동화
.editorconfig 권장
dotnet_diagnostic.CS1591.severity = error # 공개 멤버 문서 주석 필수
dotnet_analyzer_diagnostic.category-Nullable.severity = error
dotnet_diagnostic.IDE0060.severity = warning # 미사용 매개변수
CI
- XML 문서 누락/nullable 경고 → 빌드 실패 처리
- 에디트 모드/플레이 모드 테스트 분리 실행