27 KiB
27 KiB
Unity 개발 지침(MVVM/MVC · 성능 · 주석 · 직렬화 · Nullable)
본 지침은 Unity에서 디자인(View)과 로직(Model/Service/ViewModel)을 분리하고, 성능과 주석 품질을 일관되게 유지하기 위한 실전 규칙과 샘플 코드를 제공합니다. 현 프로젝트 스타일(#nullable enable, UniTask, DOTween, 한국어 주석)에 맞춰 작성되었습니다.
1) 권장 아키텍처(요약)
- View(UI): 데이터 바인딩/이벤트 라우팅만 담당. 연산/상태 보유 금지.
- Model/Service/ViewModel: 상태, 연산, 데이터 흐름을 담당. MonoBehaviour 비의존(테스트 용이).
- 느슨한 결합: 인터페이스/이벤트/메시지로 연결해 교체·테스트 쉬움.
- Prefab 경로 직렬화: GameObject 직접 참조 대신 문자열 경로 보관, 런타임 로드.
- C# Nullable: 파일 선두에
#nullable enable, 모든 참조형에?를 명시하고 널 처리. - 비동기: 전역적으로
Cysharp.Threading.Tasks.UniTask사용.Task/코루틴 대신 UniTask 우선,CancellationToken전달. - 패턴 선택:
- MVVM: UI 상태/양방향 동기화가 많을 때 권장.
- MVC: 입력 → 도메인 액션 → UI 반영 흐름이 단순할 때 권장.
2) 폴더 구조(예시)
Assets/Scripts/ArchitectureModel,View,ViewModel(or Controller),Services(Loader/Repository),Serialization
Assets/Prefabs,Assets/Resources(또는 Addressables)- Scene 수와 무관하게 ViewModel은 Scene에 종속되지 않게 유지.
3) 주석·문서화 원칙(C# XML 스타일)
- 클래스 주석: 역할/책임/사용 예를 자세히.
- 메서드 주석: 요약 + 파라미터/반환/예외. 복잡 로직만 상세, 단순 로직은 한 줄.
- 속성 주석: 한 줄 요약.
- 코드와 불일치한 주석은 즉시 수정(“왜 변경했는지”를 남김).
- 인텔리센스 문서 출력:
- 프로젝트 속성 > Build > XML documentation file
- 솔루션 전체 분석: Tools > Options > Text Editor > C# > Advanced > Enable full solution analysis
- 정적 분석: Analyze > Run Code Analysis
- 경고 엄격: 프로젝트 속성 > Build > Treat warnings as errors
4) 성능 체크리스트(UI/게임 루프)
- Update에서 GC 할당 금지: LINQ/문자열 연결/클로저 생성 지양, 캐시·풀 사용.
- DOTween: 트윈 핸들 보관, 수명 종료 시
Kill(),SetUpdate(true)사용 여부 명확화. - RectTransform 픽셀 정렬: 비정수 스케일/서브픽셀로 인한 블러 방지.
- 텍스처 Import:
- 과도 축소/회전 시
MipMaps+Trilinear, 압축 품질 조정. - 초소형 아이콘은 전용 해상도 제작(예: 10/16/24px).
- 과도 축소/회전 시
- 비동기 처리: UniTask +
CancellationToken지원. - 대량 목록: 가상화(Recycler), 청크 처리(예: 100개 처리마다
await UniTask.Yield()). - Canvas Rebuild 최소화: 대량 활성/비활성 시 부모를 잠시 비활성 후 일괄 적용, 마지막에 활성.
- Layout 강제 재빌드 사용 절제:
LayoutRebuilder.ForceRebuildLayoutImmediate는 배치/프레임 단위 최소 호출. - TextMeshPro: 기본은 오토 사이즈 비활성, 필요한 화면만 전용 폰트/사이즈 프리셋 사용.
5) 직렬화 가이드(Prefab 경로 기반)
- ScriptableObject/MonoBehaviour에는 GameObject 참조 대신 “경로 문자열”을 직렬화.
- 로더 레이어는 Addressables(권장) 또는
Resources.Load로 추상화. - 경로 변환/검증 유틸은 한 곳에서 관리하고, 런타임 Null을 안전하게 처리.
6) MVVM/MVC 선택 가이드
- MVVM:
- ViewModel은 순수 C#(MonoBehaviour 비의존).
- 다수의 UI 상태/양방향 바인딩, 테스트·모킹 용이.
- MVC:
- 입력 흐름 단순, 컨트롤러가 명령을 조정하고 View는 표시만 수행.
7) 샘플 코드
7.1 Prefab 경로 직렬화 유틸(Nullable, 예제 포함)
#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace Architecture.Serialization
{
/// <summary>
/// 프리팹 참조를 직렬화 친화적인 문자열 경로로 보관합니다.
///
/// 사용 예:
/// <example>
/// <![CDATA[
/// [SerializeField] private PrefabRef spinnerPrefab;
/// private async UniTask<GameObject?> CreateSpinnerAsync(CancellationToken ct)
/// {
/// var prefab = await spinnerPrefab.LoadAsync(ct);
/// return prefab != null ? GameObject.Instantiate(prefab) : null;
/// }
/// ]]>
/// </example>
/// </summary>
[Serializable]
public struct PrefabRef
{
[SerializeField] private string _path; // "UI/Spinner" (Resources) 또는 Addressables 키
/// <summary>프리팹 경로(읽기 전용).</summary>
public string Path => _path ?? string.Empty;
/// <summary>비어있는지 여부.</summary>
public bool IsEmpty => string.IsNullOrWhiteSpace(_path);
/// <summary>
/// 프리팹을 동기 로드(Resources). Addressables는 LoadAsync 사용 권장.
/// </summary>
public GameObject? Load()
{
#if USE_ADDRESSABLES
Debug.LogWarning("Addressables 사용 시 LoadAsync를 사용하세요.");
return null;
#else
return string.IsNullOrEmpty(_path) ? null : Resources.Load<GameObject>(_path);
#endif
}
/// <summary>
/// 프리팹을 비동기 로드합니다.
/// </summary>
/// <param name="ct">취소 토큰.</param>
public async UniTask<GameObject?> LoadAsync(CancellationToken ct = default)
{
#if USE_ADDRESSABLES
if (string.IsNullOrEmpty(_path)) return null;
var handle = UnityEngine.AddressableAssets.Addressables.LoadAssetAsync<GameObject>(_path);
try
{
await handle.Task.AsUniTask().AttachExternalCancellation(ct);
return handle.Status == UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationStatus.Succeeded
? handle.Result
: null;
}
finally
{
// 필요 시 Release는 Instantiate 후에 호출합니다.
// Addressables.Release(handle);
}
#else
if (string.IsNullOrEmpty(_path)) return null;
var request = Resources.LoadAsync<GameObject>(_path);
await request.AsUniTask(cancellationToken: ct);
return request.asset as GameObject;
#endif
}
/// <summary>경로를 명시적으로 설정합니다.</summary>
public PrefabRef(string path) => _path = path;
}
}
7.2 Model + ViewModel(초보자용 주석 포함, 단순 로직은 간단히)
#nullable enable
using System;
using Architecture.Serialization;
using UnityEngine;
namespace Architecture.Model
{
/// <summary>
/// 할 일 아이템 모델.
///
/// 책임:
/// - 데이터 보유(Name/완료 여부/아이콘 프리팹)
/// - 변경 통지 이벤트 제공
///
/// 사용 예:
/// <example>
/// <![CDATA[
/// var item = new TodoItem("텍스처 임포트 최적화") { IsCompleted = false };
/// item.OnChanged += i => Debug.Log($"Changed: {i.Name}");
/// ]]>
/// </example>
/// </summary>
[Serializable]
public sealed class TodoItem
{
/// <summary>표시 이름.</summary>
public string Name { get; private set; }
/// <summary>완료 여부.</summary>
public bool IsCompleted { get; private set; }
/// <summary>아이콘 프리팹 경로.</summary>
public PrefabRef IconPrefab { get; private set; }
/// <summary>모델 변경 이벤트.</summary>
public event Action<TodoItem>? OnChanged;
public TodoItem(string name, bool isCompleted = false, PrefabRef iconPrefab = default)
{
Name = name;
IsCompleted = isCompleted;
IconPrefab = iconPrefab;
}
/// <summary>
/// 이름을 변경합니다.
/// </summary>
/// <param name="name">새 이름.</param>
public void Rename(string name)
{
if (Name == name) return; // 단순 가드
Name = name;
OnChanged?.Invoke(this);
}
/// <summary>완료/미완료 토글.</summary>
public void ToggleCompleted()
{
IsCompleted = !IsCompleted;
OnChanged?.Invoke(this);
}
}
}
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Architecture.Model;
namespace Architecture.ViewModel
{
/// <summary>
/// Todo 목록의 프레젠테이션 상태를 관리하는 ViewModel.
///
/// 책임:
/// - 모델 컬렉션 보유/필터/정렬
/// - View에 필요한 이벤트(추가/제거/갱신) 제공
/// - MonoBehaviour에 의존하지 않음(테스트 용이)
///
/// 사용 예:
/// <example>
/// <![CDATA[
/// var vm = new TodoListViewModel();
/// vm.OnItemAdded += item => Debug.Log(item.Name);
/// vm.Add(new TodoItem("씬 로딩 최적화"));
/// ]]>
/// </example>
/// </summary>
public sealed class TodoListViewModel
{
private readonly List<TodoItem> _items = new();
/// <summary>현재 아이템 스냅샷.</summary>
public IReadOnlyList<TodoItem> Items => _items;
public event Action<TodoItem>? OnItemAdded;
public event Action<TodoItem>? OnItemRemoved;
public event Action<TodoItem>? OnItemChanged;
/// <summary>
/// 아이템을 추가합니다.
/// </summary>
/// <param name="item">추가할 모델.</param>
public void Add(TodoItem item)
{
_items.Add(item);
item.OnChanged += HandleItemChanged;
OnItemAdded?.Invoke(item);
}
/// <summary>
/// 아이템을 제거합니다.
/// </summary>
/// <param name="item">제거할 모델.</param>
public void Remove(TodoItem item)
{
if (_items.Remove(item))
{
item.OnChanged -= HandleItemChanged;
OnItemRemoved?.Invoke(item);
}
}
/// <summary>
/// 간단한 텍스트 필터를 적용한 결과를 반환합니다.
/// </summary>
/// <param name="keyword">포함할 텍스트(대소문자 무시).</param>
public IEnumerable<TodoItem> Filter(string keyword)
{
if (string.IsNullOrWhiteSpace(keyword)) return _items;
var lower = keyword.ToLowerInvariant();
return _items.Where(i => i.Name.ToLowerInvariant().Contains(lower));
}
private void HandleItemChanged(TodoItem item) => OnItemChanged?.Invoke(item);
}
}
7.3 View(MVVM 바인딩 예, 간단·상세 주석 구분)
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
using Architecture.Model;
using Architecture.Serialization;
using Architecture.ViewModel;
using Cysharp.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Architecture.View
{
/// <summary>
/// TodoList의 View.
///
/// 책임:
/// - ViewModel 이벤트를 구독해 UI 생성/갱신
/// - 사용자 입력을 ViewModel에 전달
/// - PrefabRef를 사용해 아이템 프리팹을 런타임 로드/생성
///
/// 개선 사항:
/// - Model-View 매핑을 위해 Dictionary를 사용, O(1) 검색으로 성능 향상.
/// - 명시적 핸들러 메서드로 이벤트 구독/해제 대칭성 확보.
/// - async void 대신 UniTask 반환 및 .Forget() 패턴으로 예외 처리 강화.
///
/// 주의:
/// - 모든 구독/비동기 작업은 OnDestroy에서 해제/취소합니다.
/// </summary>
public sealed class TodoListView : MonoBehaviour
{
[Header("UI")]
[SerializeField] private RectTransform _content = null!;
[SerializeField] private TMP_InputField _filterInput = null!;
[SerializeField] private Button _addButton = null!;
[SerializeField] private PrefabRef _itemPrefab; // "UI/TodoItem" 등
private readonly CancellationTokenSource _cts = new();
private readonly Dictionary<TodoItem, TodoRowView> _rowViews = new();
private TodoListViewModel _vm = null!;
private void Awake()
{
// Composition Root에서 주입받는 것을 권장. 여기서는 예시로 직접 생성.
Init(new TodoListViewModel());
}
/// <summary>
/// ViewModel을 주입하고 UI를 초기화합니다.
/// </summary>
public void Init(TodoListViewModel viewModel)
{
_vm = viewModel;
// 이벤트 구독 (명시적 핸들러 메서드 사용)
_vm.OnItemAdded += OnItemAddedHandler;
_vm.OnItemRemoved += HandleItemRemoved;
_vm.OnItemChanged += HandleItemChanged;
// 입력 이벤트
_addButton.onClick.AddListener(OnAddButtonClicked);
_filterInput.onValueChanged.AddListener(OnFilterInputChanged);
}
/// <summary>ViewModel의 OnItemAdded 이벤트 핸들러.</summary>
private void OnItemAddedHandler(TodoItem item)
{
HandleItemAdded(item).Forget(ex => Debug.LogError($"[TodoListView] Failed to add item: {ex}"));
}
/// <summary>Add 버튼 클릭 핸들러.</summary>
private void OnAddButtonClicked()
{
_vm.Add(new TodoItem("New Task"));
}
/// <summary>필터 입력 변경 핸들러.</summary>
private void OnFilterInputChanged(string value)
{
RebuildFiltered();
}
private async UniTask HandleItemAdded(TodoItem item)
{
var prefab = await _itemPrefab.LoadAsync(_cts.Token);
if (prefab == null || _cts.IsCancellationRequested) return;
var go = Instantiate(prefab, _content);
var row = go.GetComponent<TodoRowView>();
if (row != null)
{
row.Bind(item);
_rowViews[item] = row; // 딕셔너리에 추가
}
}
private void HandleItemRemoved(TodoItem item)
{
if (_rowViews.TryGetValue(item, out var row))
{
Destroy(row.gameObject);
_rowViews.Remove(item); // 딕셔너리에서 제거
}
}
private void HandleItemChanged(TodoItem item)
{
if (_rowViews.TryGetValue(item, out var row))
{
row.Refresh();
}
}
private void RebuildFiltered()
{
var keyword = _filterInput.text.ToLowerInvariant();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
foreach (var (item, row) in _rowViews)
{
var visible = !hasKeyword || item.Name.ToLowerInvariant().Contains(keyword);
row.gameObject.SetActive(visible);
}
}
private void OnDestroy()
{
_cts.Cancel();
_cts.Dispose();
// ViewModel 이벤트 구독 해제 (등록과 대칭)
if (_vm != null)
{
_vm.OnItemAdded -= OnItemAddedHandler;
_vm.OnItemRemoved -= HandleItemRemoved;
_vm.OnItemChanged -= HandleItemChanged;
}
// UI 이벤트 구독 해제
_addButton.onClick.RemoveListener(OnAddButtonClicked);
_filterInput.onValueChanged.RemoveListener(OnFilterInputChanged);
}
}
/// <summary>
/// 개별 Row(View) 컴포넌트.
/// - 간단 로직: 바인딩/리프레시만 담당
/// </summary>
public sealed class TodoRowView : MonoBehaviour
{
[SerializeField] private TMP_Text _nameText = null!;
[SerializeField] private Toggle _completedToggle = null!;
/// <summary>현재 바인딩된 모델(읽기 전용).</summary>
public TodoItem? BoundItem { get; private set; }
/// <summary>
/// Row를 모델에 바인딩합니다.
/// </summary>
/// <param name="item">바인딩할 모델.</param>
public void Bind(TodoItem item)
{
BoundItem = item;
_completedToggle.onValueChanged.AddListener(OnToggleChanged);
Refresh();
}
/// <summary>UI를 현재 모델 값으로 갱신합니다.</summary>
public void Refresh()
{
if (BoundItem == null) return;
_nameText.text = BoundItem.Name;
_completedToggle.SetIsOnWithoutNotify(BoundItem.IsCompleted);
}
private void OnToggleChanged(bool value)
{
// 단순 전달: 모델 토글
BoundItem?.ToggleCompleted();
}
private void OnDestroy()
{
_completedToggle.onValueChanged.RemoveListener(OnToggleChanged);
}
}
}
8) 현 코드베이스(HierarchyWindow 등) 적용 팁
- 긴 작업(검색 등)은 현재처럼 청크 처리 +
await UniTask.Yield()로 UI 프리즈 방지. - 로딩 아이콘/초소형 UI:
- 과도 축소 시 MipMap + Trilinear를 켜거나, 전용 해상도 스프라이트 사용.
- 회전보다는 Image
Filled+fillAmount애니메이션이 초소형에서 더 깔끔.
- DOTween 트윈: 멤버로 보관,
OnDestroy/취소 시Kill()처리(현재 코드가 모범 사례). #nullable enable유지,UnityEngine.Object계열도?표기 후 Null 체크.- 입력/검색 UI는 View, 검색 로직/필터는 ViewModel로 추출하면 테스트·교체 용이.
9) 품질 자동화(선택)
- StyleCop.Analyzers, Roslynator로 XML 주석/네이밍 규칙 검사.
.editorconfig에 문서 주석 필수 규칙 설정.- VS에서 정기 점검: Analyze > Run Code Analysis, 빌드에서 Treat warnings as errors.
- CI에서 XML 문서 누락/nullable 경고를 실패로 처리해 조기 차단.
- 권장
.editorconfig요약:dotnet_diagnostic.CS1591.severity = error(공개 멤버 문서 주석 필수)dotnet_analyzer_diagnostic.category-Style.severity = warningdotnet_analyzer_diagnostic.category-Nullable.severity = errordotnet_diagnostic.IDE0060.severity = warning(사용 안 하는 매개변수)dotnet_style_qualification_for_field = true:suggestion
10) 권장 디자인 패턴
- DI/Composition Root
- 언제: 서비스/로더/뷰모델 주입이 필요할 때 초기화 지점을 일원화합니다.
- 팁: 경량 수동 DI 권장(프레임워크 미사용). 씬 부팅 지점에 구성 루트를 두고 팩토리/인터페이스를 통해 의존성을 명시적으로 연결합니다.
- Event Aggregator/Mediator
- 언제: 윈도우/패널/리스트 간 통신을 느슨하게 연결하고 싶을 때.
- 팁: 강타입 메시지 구조체를 사용하고 구독 해제를 일원화합니다. 남용 시 디버깅이 어려울 수 있습니다.
- Command (+ Undo/Memento)
- 언제: UI 액션(삭제/이동/이름 변경 등)에 되돌리기가 필요할 때.
- 팁:
ExecuteAsync(CancellationToken)/UndoAsync(...)는UniTask를 반환하고, 스택으로 관리합니다.
- Strategy
- 언제: 정렬/필터/드롭 배치 규칙을 교체 가능하게 설계할 때.
- 팁:
ISortStrategy/IFilterStrategy등 인터페이스를 정의하고 Update 중 할당을 피합니다.
- State/Finite State Machine
- 언제: 편집/선택/드래그 등 모드 전환이 분명할 때.
- 팁:
EnterAsync/ExitAsync는UniTask를 사용하고CancellationToken을 전파합니다.
- Factory/Abstract Factory
- 언제: 프리팹/뷰 생성 경로와 정책을 캡슐화할 때.
- 팁:
PrefabRef(+Addressables)를IViewFactory로 감싸고 토큰을 전달합니다.
- Object Pool
- 언제: 다량의 리스트 아이템/툴팁/이펙트를 재사용할 때.
- 팁:
IPoolable.OnRent/OnReturn훅을 제공하고DOTween.Kill()등 수명 종료 처리를 확실히 합니다.
- Repository + Unit of Work
- 언제: 데이터 소스(파일/원격/메모리)를 교체 가능하게 할 때.
- 팁: VM은
ITodoRepository에 의존하고 배치 저장은 UoW로 묶습니다.
- Facade/Adapter
- 언제: 서드파티/플랫폼 API를 감싸서 단순화할 때.
- 팁: 테스트 더블을 쉽게 주입할 수 있습니다.
- Composite
- 언제: 트리 구조를 모델링할 때.
- 팁: 노드 공통 인터페이스와 자식 컬렉션만 노출하고 뷰는 Composite만 알도록 합니다.
- Visitor
- 언제: 트리 순회 작업(검색/선택 동기화/통계)을 추가할 때.
- 팁: 순회 로직을 방문자로 분리해 재사용성을 높입니다.
- Template Method
- 언제: 리스트 아이템 바인딩/갱신 단계를 표준화할 때.
- 팁: 공통 베이스
ListItemView에 훅 메서드를 제공합니다.
- Builder
- 언제: 복잡한 UI 구성이나
DOTween시퀀스 생성을 읽기 쉽게 만들 때. - 팁: 체이닝으로 가독성을 높이고 재사용 가능한 빌더를 제공합니다.
- 언제: 복잡한 UI 구성이나
- Flyweight/Prototype (ScriptableObject)
- 언제: 공유 불변 데이터/기본 설정을 여러 객체에서 공용으로 사용할 때.
- 팁: ScriptableObject로 메모리를 공유하고 런타임 인스턴스는 얕은 복제를 사용합니다.
- Null Object
- 언제: 선택 없음/루트 노드 등에서 null 분기를 제거하고 싶을 때.
- 팁:
INode의 빈 구현을 제공해 분기 삭제와 안정성을 높입니다.
11) 비동기(UniTask) 규약
- 공개 비동기 API는
UniTask/UniTask<T>를 반환합니다.async void는 지양합니다. - 모든 비동기 메서드에
CancellationToken인자를 제공하고AttachExternalCancellation로 연결합니다. - Fire-and-forget은 예외 로깅 후
.Forget()을 제한적으로 사용합니다. - 메인 스레드가 필요한 경우
UniTask.SwitchToMainThread()를 명시적으로 호출합니다.
12) Addressables/Resources 규약
- 전처리기 기호: Addressables 사용 시
Player Settings > Scripting Define Symbols에USE_ADDRESSABLES를 설정합니다. - 로드/수명:
LoadAssetAsync로 얻은 핸들은 참조 수명 관리 후Addressables.Release(handle)로 해제합니다. 인스턴스 생성 후에는 에셋 참조를 유지할 필요가 없으면 해제합니다. - Instantiate 정책: 다중 인스턴스가 필요하면
Addressables.InstantiateAsync사용을 우선 고려합니다(완료 후Addressables.ReleaseInstance). - Resources 경로 규약: 경로는
Resources/하위 루트 기준 상대 경로를 사용합니다. Addressables 전환 시 키/그룹/레이블 매핑 표를 준비합니다. - 전환 체크리스트: 키 네임 규칙, 그룹 빌드/로드 모드, 레이블 기반 로드, Release 누락 검토, 메모리 프로파일 확인.
13) UniTask 취소/예외 처리 상세
CancellationTokenSource소유권은 생성한 컴포넌트가 갖습니다.MonoBehaviour는OnDestroy에서Cancel/Dispose를 호출합니다.- 토큰 결합:
CancellationTokenSource.CreateLinkedTokenSource(parent, this._cts.Token)를 사용해 상위/로컬 토큰을 결합합니다. - 타임아웃:
using var timeout = new CancellationTokenSource(); timeout.CancelAfter(TimeSpan.FromSeconds(5)); - 예외 정책: 공개 API는 예외를 전파하고, fire-and-forget은 로깅 후
.Forget()을 사용합니다.
14) 스레딩 모델
- CPU 바운드 작업은
UniTask.Run으로 워커 스레드에서 처리하고, Unity API 접근 전UniTask.SwitchToMainThread를 호출합니다. - IO 바운드는 가능한 비동기 API를 우선 사용합니다(파일/네트워크/주소가능).
15) 이벤트/구독 수명 주기
- 모든 구독은 등록 지점과 해제 지점을 명시합니다. 컬렉션/모델 이벤트는 명시적으로 해제합니다.
- UI 이벤트(
Button.onClick,Toggle.onValueChanged)는OnDestroy에서 일괄 해제합니다. Bind/Unbind패턴을 사용해 View의 수명과 구독 수명을 일치시킵니다.
16) 오브젝트 풀 정책
- 대량 생성/파괴 대상은 풀을 사용합니다. 항목은
IPoolable.OnRent/OnReturn훅을 구현합니다. - 트윈/코루틴 등 외부 작업은 반환 시
DOTween.Kill()또는DOKill()로 종료합니다. - 부모 재설정 최적화: 반환 시 풀 컨테이너로 재부모, 좌표/스케일 초기화 최소화.
- 풀 용량 정책: 최대치/증가 단위 정의, 초과 시 로그 경고 옵션 제공.
17) 수동 DI(Composition Root) 예시
- 씬 부팅 스크립트에서 인터페이스 구현체를 생성하고 주입합니다. 프레임워크 없이 팩토리/생성자 주입을 사용합니다.
public sealed class AppBootstrap : MonoBehaviour
{
private void Awake()
{
// ILogger logger = new UnityLogger();
// ITodoRepository repo = new FileTodoRepository(logger);
// IViewFactory viewFactory = new AddressablesViewFactory(logger);
// 1. ViewModel 생성
var vm = new Architecture.ViewModel.TodoListViewModel();
// 2. View 인스턴스 찾기
var window = FindObjectOfType<Architecture.View.TodoListView>();
// 3. 의존성 주입
if (window != null)
{
// window.Init(vm, viewFactory, logger); // 더 많은 의존성이 있다면 Init 메서드를 확장
window.Init(vm);
}
}
}
- 네이밍 컨벤션: 인터페이스
I*, 팩토리*Factory, 리포지토리*Repository.
18) 테스트 가이드
- ViewModel 단위 테스트(NUnit) 권장: MonoBehaviour에 의존하지 않게 유지합니다.
- 에디트 모드/플레이 모드 테스트를 분리하고 CI에서 두 모드를 실행합니다.
- 기본 템플릿(요약): 생성/추가/삭제/이벤트 발행 검증, 필터 로직 검증.
19) 대규모 리스트 가상화
ScrollRect+ Recycler 패턴을 권장합니다.- 핵심 인터페이스 예시:
IItemProvider(데이터 크기/아이템 생성),IItemRenderer(Bind/Unbind/Refresh). - 스크롤 오프셋 ↔ 인덱스 매핑 규약을 문서화하고, 배치 업데이트 시
await UniTask.Yield()로 프리즈를 방지합니다.
20) UnityEngine.Object와 Nullable 주의
- Unity는
== null연산자를 오버로드하므로 순수 참조 null 확인 시ReferenceEquals(obj, null)사용을 고려합니다. UnityEngine.Object파괴된 인스턴스는null처럼 동작할 수 있으므로 사용 전 유효성 검사를 수행합니다.- 직렬화 필드에
?를 표기하고, 런타임 Null/파괴 상태를 방어적으로 처리합니다.
본 문서는 템플릿으로 재사용 가능하며, 신규 컴포넌트(클래스/메서드/속성)마다 동일한 주석·패턴을 유지하면 초보자도 구조와 의도를 빠르게 파악할 수 있습니다.