2026-01-08 20:15:57 +09:00
|
|
|
# Unity 개발 지침 (UI Toolkit · MVVM · 성능 · Nullable)
|
|
|
|
|
|
|
|
|
|
본 지침은 Unity UI Toolkit 기반 프로젝트의 아키텍처, 성능, 코드 품질을 일관되게 유지하기 위한 규칙입니다.
|
|
|
|
|
프로젝트 스타일: `#nullable enable`, UniTask, DOTween, 한국어 주석
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-02-24 20:01:56 +09:00
|
|
|
## 0) 작업 진행 규칙
|
|
|
|
|
|
|
|
|
|
**⚠️ 최우선 규칙: 임의로 진행하지 않고, 반드시 사용자에게 확인 후 진행합니다.**
|
|
|
|
|
|
|
|
|
|
- 코드 수정, 파일 생성/삭제, 리팩토링 등 **모든 변경 작업은 사전에 계획을 설명하고 승인을 받은 후** 진행합니다.
|
|
|
|
|
- 요구사항이 모호하거나 여러 접근 방식이 가능한 경우, **추측하지 말고 질문**합니다.
|
|
|
|
|
- 버그 수정이라도 원인 분석 결과를 먼저 공유하고, 수정 방향에 대해 합의 후 코드를 변경합니다.
|
|
|
|
|
- 단순한 오타 수정, 한 줄 변경 등 **명백하고 사소한 작업**만 즉시 진행할 수 있습니다.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
## 1) 핵심 원칙
|
|
|
|
|
|
|
|
|
|
### UI 프레임워크
|
|
|
|
|
- **UI Toolkit(UIElements) 필수 사용**. uGUI(Canvas 기반)는 레거시로 취급합니다.
|
|
|
|
|
- UXML(구조)과 USS(스타일)를 분리하고, C# 코드에서 인라인 스타일 지정을 지양합니다.
|
|
|
|
|
|
2026-02-03 20:43:36 +09:00
|
|
|
### 이벤트 콜백 등록 규칙
|
|
|
|
|
**⚠️ 중요: `RegisterValueChangedCallback` 대신 `RegisterCallback<ChangeEvent<T>>`를 사용합니다.**
|
|
|
|
|
|
|
|
|
|
`RegisterValueChangedCallback`은 확장 메서드로 `UnregisterCallback`과 대칭이 맞지 않아 이벤트 해제가 어렵습니다.
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
// ❌ 잘못된 예: 해제가 어려운 방식
|
|
|
|
|
field.RegisterValueChangedCallback(OnValueChanged);
|
|
|
|
|
// field.UnregisterValueChangedCallback(OnValueChanged); // 이런 메서드 없음!
|
|
|
|
|
|
|
|
|
|
// ✅ 올바른 예: 대칭적인 등록/해제
|
|
|
|
|
field.RegisterCallback<ChangeEvent<float>>(OnValueChanged);
|
|
|
|
|
field.UnregisterCallback<ChangeEvent<float>>(OnValueChanged);
|
|
|
|
|
|
|
|
|
|
private void OnValueChanged(ChangeEvent<float> evt)
|
|
|
|
|
{
|
|
|
|
|
// evt.newValue, evt.previousValue 사용
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| 메서드 | 권장 | 이유 |
|
|
|
|
|
|--------|------|------|
|
|
|
|
|
| `RegisterValueChangedCallback` | ❌ | 해제용 메서드 없음 |
|
|
|
|
|
| `RegisterCallback<ChangeEvent<T>>` | ✅ | `UnregisterCallback` 대칭 |
|
|
|
|
|
|
2026-01-20 20:18:47 +09:00
|
|
|
### 커스텀 VisualElement (Unity 6)
|
2026-01-21 20:43:54 +09:00
|
|
|
Unity 6에서는 **레거시 `UxmlFactory`/`UxmlTraits` 방식을 사용하지 않고**, 소스 생성기 기반의 `[UxmlElement]`와 `[UxmlAttribute]`를 사용합니다.
|
|
|
|
|
|
|
|
|
|
#### 필수 규칙
|
|
|
|
|
1. **클래스에 `[UxmlElement]` 어트리뷰트 추가**
|
|
|
|
|
2. **클래스를 `partial`로 선언** (소스 생성기 요구사항)
|
|
|
|
|
3. **UXML 속성은 `[UxmlAttribute]`로 케밥 케이스 소문자 명시**
|
2026-01-20 20:18:47 +09:00
|
|
|
|
|
|
|
|
```csharp
|
2026-01-21 20:43:54 +09:00
|
|
|
// ✅ 올바른 예: Unity 6 방식
|
|
|
|
|
[UxmlElement]
|
|
|
|
|
public partial class UTKCodeBlock : VisualElement
|
|
|
|
|
{
|
|
|
|
|
[UxmlAttribute("title")]
|
|
|
|
|
public string Title { get; set; }
|
2026-01-20 20:18:47 +09:00
|
|
|
|
2026-01-21 20:43:54 +09:00
|
|
|
[UxmlAttribute("is-enabled")]
|
|
|
|
|
public bool IsEnabled { get; set; }
|
2026-01-20 20:18:47 +09:00
|
|
|
|
2026-01-21 20:43:54 +09:00
|
|
|
[UxmlAttribute("border-width")]
|
|
|
|
|
public int BorderWidth { get; set; }
|
|
|
|
|
}
|
2026-01-20 20:18:47 +09:00
|
|
|
|
2026-01-21 20:43:54 +09:00
|
|
|
// ❌ 잘못된 예: 레거시 방식 (사용 금지)
|
|
|
|
|
public class UTKCodeBlock : VisualElement
|
|
|
|
|
{
|
|
|
|
|
public new class UxmlFactory : UxmlFactory<UTKCodeBlock, UxmlTraits> { }
|
|
|
|
|
public new class UxmlTraits : VisualElement.UxmlTraits { ... }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ❌ 잘못된 예: 케밥 케이스 미사용
|
2026-01-20 20:18:47 +09:00
|
|
|
[UxmlAttribute] // Unity 6에서 UXML 속성 매핑 실패
|
|
|
|
|
public string Text { get; set; }
|
|
|
|
|
|
|
|
|
|
[UxmlAttribute("Text")] // 대문자는 UXML과 불일치
|
|
|
|
|
public string Text { get; set; }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**UXML 사용 예:**
|
|
|
|
|
```xml
|
|
|
|
|
<utk:UTKButton text="확인" variant="Primary" is-enabled="true" />
|
|
|
|
|
```
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
### 아키텍처 (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 쿼리
|
|
|
|
|
```csharp
|
|
|
|
|
// ❌ 나쁜 예: 매 프레임 쿼리
|
|
|
|
|
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) 메모리 관리
|
|
|
|
|
|
|
|
|
|
### 이벤트 구독/해제
|
|
|
|
|
```csharp
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
// 공개 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)
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
[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 직접 참조 대신 경로 문자열 직렬화
|
|
|
|
|
|
2026-01-22 20:12:22 +09:00
|
|
|
### UXML/USS 파일 네이밍 규칙
|
|
|
|
|
|
|
|
|
|
**⚠️ 중요: UXML과 USS 파일명은 반드시 다르게 지정해야 합니다.**
|
|
|
|
|
|
|
|
|
|
`Resources.Load<T>(path)`는 확장자 없이 경로를 받기 때문에, 동일한 경로에 UXML과 USS가 모두 존재하면 로드 충돌이 발생할 수 있습니다.
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
// ❌ 잘못된 예: 동일한 경로명 사용
|
|
|
|
|
private const string UXML_PATH = "UIToolkit/Window/UTKAccordionListWindow";
|
|
|
|
|
private const string USS_PATH = "UIToolkit/Window/UTKAccordionListWindow";
|
|
|
|
|
|
|
|
|
|
// Resources.Load<VisualTreeAsset>(UXML_PATH); // .uxml 로드
|
|
|
|
|
// Resources.Load<StyleSheet>(USS_PATH); // .uss 로드 실패 가능
|
|
|
|
|
|
|
|
|
|
// ✅ 올바른 예: USS 파일명에 접미사 추가
|
|
|
|
|
private const string UXML_PATH = "UIToolkit/Window/UTKAccordionListWindow";
|
|
|
|
|
private const string USS_PATH = "UIToolkit/Window/UTKAccordionListWindowUss";
|
|
|
|
|
|
|
|
|
|
// 파일 구조:
|
|
|
|
|
// - UTKAccordionListWindow.uxml
|
|
|
|
|
// - UTKAccordionListWindowUss.uss
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**네이밍 규칙:**
|
|
|
|
|
| 파일 유형 | 네이밍 패턴 | 예시 |
|
|
|
|
|
|-----------|-------------|------|
|
|
|
|
|
| UXML | `{ComponentName}.uxml` | `UTKAccordionListWindow.uxml` |
|
|
|
|
|
| USS | `{ComponentName}Uss.uss` | `UTKAccordionListWindowUss.uss` |
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 7) USS 스타일 가이드
|
|
|
|
|
|
|
|
|
|
### 디자인 참조
|
2026-01-09 18:42:17 +09:00
|
|
|
UI Toolkit 개발 시 다음 위치의 스타일 리소스를 참조하세요:
|
|
|
|
|
|
|
|
|
|
**우선 참조**: `Assets/Resources/UIToolkit/Style/`
|
|
|
|
|
- 프로젝트에서 사용 중인 실제 USS 스타일 파일들이 위치합니다.
|
|
|
|
|
- 컴포넌트별로 정리된 스타일을 먼저 확인하세요.
|
|
|
|
|
|
|
|
|
|
**보조 참조**: `StyleGuide/` 폴더
|
|
|
|
|
- `Assets/Resources/UIToolkit/Style/`에 구현되지 않은 스타일만 참조합니다.
|
|
|
|
|
- 새 컴포넌트 개발 시 디자인 가이드로 활용합니다.
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
| 파일 | 설명 |
|
|
|
|
|
|------|------|
|
|
|
|
|
| `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 네이밍
|
|
|
|
|
```css
|
|
|
|
|
.panel { }
|
|
|
|
|
.panel__header { }
|
|
|
|
|
.panel__header--highlighted { }
|
|
|
|
|
.panel__content { }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 규칙
|
|
|
|
|
- 반복 값은 USS 변수 사용: `--color-primary`, `--spacing-md`
|
|
|
|
|
- 라이트/다크 테마는 별도 USS로 분리
|
|
|
|
|
- **새 UI 컴포넌트 개발 시 StyleGuide 이미지와 일치하는 스타일 적용**
|
|
|
|
|
|
2026-01-21 20:43:54 +09:00
|
|
|
### 스타일 우선순위 제어 방법
|
|
|
|
|
|
|
|
|
|
1. **더 높은 특수성(Specificity) 사용**
|
|
|
|
|
- 클래스 이름(`.` 사용)보다 ID(`#` 사용)가 우선순위가 높음
|
|
|
|
|
- ID보다 UXML에 직접 작성된 인라인 스타일이 우선순위가 높음
|
|
|
|
|
- 여러 클래스를 조합하여 특수성을 높이는 방법도 효과적
|
|
|
|
|
- 예시: `H3.title-text`와 같이 선택자를 더 구체적으로 만듦
|
|
|
|
|
|
|
|
|
|
2. **인라인 스타일 활용**
|
|
|
|
|
- C# 스크립트나 UXML 파일 내에 직접 인라인 스타일을 적용하면 USS 파일의 어떤 스타일보다도 우선 적용됨
|
|
|
|
|
|
|
|
|
|
3. **스타일 시트 순서**
|
|
|
|
|
- 동일한 특수성을 가진 스타일의 경우, 나중에 로드된 스타일 시트의 규칙이 우선함
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
---
|
|
|
|
|
|
2026-01-13 20:39:45 +09:00
|
|
|
## 8) 아이콘 사용 가이드 (Icons)
|
|
|
|
|
|
|
|
|
|
### 아이콘 사용 우선순위
|
|
|
|
|
UI 아이콘 적용 시 다음 순서를 반드시 준수해야 합니다.
|
|
|
|
|
|
|
|
|
|
1. **1순위 (Material Icons)**: `UTKMaterialIcons` 클래스 확인
|
|
|
|
|
- 폰트 기반 아이콘(Unicode)을 우선 사용합니다.
|
|
|
|
|
- 예: `UTKButton.SetMaterialIcon(UTKMaterialIcons.Home)`
|
|
|
|
|
|
|
|
|
|
2. **2순위 (Image Icons)**: `UTKImageIcons` 클래스 사용
|
|
|
|
|
- 필요한 아이콘이 `UTKMaterialIcons`에 없는 경우에만 `UTKImageIcons`를 사용합니다.
|
|
|
|
|
- 예: `UTKButton.SetImageIcon(UTKImageIcons.CustomIcon)`
|
|
|
|
|
|
|
|
|
|
> **권장**: 일관된 UI 스타일과 메모리 효율을 위해 가능한 Material Icons 사용을 권장합니다.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 9) 주석 원칙 (C# XML)
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 사용자 데이터를 비동기로 로드합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="userId">사용자 ID.</param>
|
|
|
|
|
/// <param name="ct">취소 토큰.</param>
|
|
|
|
|
/// <returns>사용자 데이터 또는 null.</returns>
|
|
|
|
|
public async UniTask<UserData?> LoadUserAsync(string userId, CancellationToken ct)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- 클래스: 역할/책임/사용 예
|
|
|
|
|
- 메서드: 요약 + 파라미터/반환 (복잡 로직만 상세)
|
|
|
|
|
- 속성: 한 줄 요약
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-01-13 20:39:45 +09:00
|
|
|
## 10) 디자인 패턴 요약
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
| 패턴 | 사용 시점 |
|
|
|
|
|
|------|-----------|
|
|
|
|
|
| **DI/Composition Root** | 서비스/ViewModel 주입 일원화 |
|
|
|
|
|
| **Event Aggregator** | 컴포넌트 간 느슨한 통신 |
|
|
|
|
|
| **Command + Undo** | UI 액션에 되돌리기 필요 시 |
|
|
|
|
|
| **Strategy** | 정렬/필터 규칙 교체 |
|
|
|
|
|
| **State/FSM** | 모드 전환 (편집/선택/드래그) |
|
|
|
|
|
| **Factory** | 뷰/프리팹 생성 캡슐화 |
|
|
|
|
|
| **Object Pool** | 대량 아이템 재사용 |
|
|
|
|
|
| **Repository** | 데이터 소스 추상화 |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-02-03 20:43:36 +09:00
|
|
|
## 11) UTK 컴포넌트 기본 패턴
|
|
|
|
|
|
|
|
|
|
UTK 컴포넌트는 다음 패턴을 따릅니다. `UTKHelpBox`를 예시로 설명합니다.
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
#nullable enable
|
|
|
|
|
using System;
|
2026-02-03 20:43:36 +09:00
|
|
|
using UnityEngine;
|
2026-01-08 20:15:57 +09:00
|
|
|
using UnityEngine.UIElements;
|
|
|
|
|
|
2026-02-03 20:43:36 +09:00
|
|
|
namespace UVC.UIToolkit
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
2026-02-03 20:43:36 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 컴포넌트 설명.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[UxmlElement]
|
|
|
|
|
public partial class UTKExample : VisualElement, IDisposable
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
2026-02-03 20:43:36 +09:00
|
|
|
#region Constants
|
|
|
|
|
private const string UXML_PATH = "UIToolkit/Common/UTKExample";
|
|
|
|
|
private const string USS_PATH = "UIToolkit/Common/UTKExampleUss";
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Fields
|
|
|
|
|
private bool _disposed;
|
|
|
|
|
private Label? _label;
|
|
|
|
|
private string _text = "";
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Properties
|
|
|
|
|
/// <summary>텍스트</summary>
|
|
|
|
|
[UxmlAttribute("text")]
|
|
|
|
|
public string Text
|
|
|
|
|
{
|
|
|
|
|
get => _text;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_text = value;
|
|
|
|
|
if (_label != null)
|
|
|
|
|
_label.text = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Constructor
|
|
|
|
|
public UTKExample() : base()
|
|
|
|
|
{
|
|
|
|
|
// 1. 테마 적용
|
|
|
|
|
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
|
|
|
|
|
|
|
|
|
// 2. USS 로드
|
|
|
|
|
var uss = Resources.Load<StyleSheet>(USS_PATH);
|
|
|
|
|
if (uss != null)
|
|
|
|
|
{
|
|
|
|
|
styleSheets.Add(uss);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. UI 생성 (UXML 또는 Fallback)
|
|
|
|
|
CreateUI();
|
|
|
|
|
|
|
|
|
|
// 4. 테마 변경 구독
|
|
|
|
|
SubscribeToThemeChanges();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public UTKExample(string text) : this()
|
|
|
|
|
{
|
|
|
|
|
Text = text;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Setup
|
|
|
|
|
private void CreateUI()
|
|
|
|
|
{
|
|
|
|
|
AddToClassList("utk-example");
|
|
|
|
|
|
|
|
|
|
var asset = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
|
|
|
|
if (asset != null)
|
|
|
|
|
{
|
|
|
|
|
CreateUIFromUxml(asset);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
CreateUIFallback();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CreateUIFromUxml(VisualTreeAsset asset)
|
|
|
|
|
{
|
|
|
|
|
var root = asset.Instantiate();
|
|
|
|
|
// UXML 요소 참조 가져오기
|
|
|
|
|
_label = root.Q<Label>("label");
|
|
|
|
|
Add(root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CreateUIFallback()
|
|
|
|
|
{
|
|
|
|
|
_label = new Label(_text);
|
|
|
|
|
_label.AddToClassList("utk-example__label");
|
|
|
|
|
Add(_label);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SubscribeToThemeChanges()
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
2026-02-10 20:48:49 +09:00
|
|
|
RegisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
|
|
|
|
RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
|
|
|
|
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt)
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
2026-02-03 20:43:36 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnThemeChanged(UTKTheme theme)
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region IDisposable
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
if (_disposed) return;
|
|
|
|
|
_disposed = true;
|
|
|
|
|
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
2026-02-10 20:48:49 +09:00
|
|
|
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
|
|
|
|
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
|
2026-02-03 20:43:36 +09:00
|
|
|
}
|
|
|
|
|
#endregion
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2026-02-03 20:43:36 +09:00
|
|
|
### 주요 패턴 요약
|
|
|
|
|
|
|
|
|
|
| 항목 | 설명 |
|
|
|
|
|
|------|------|
|
|
|
|
|
| **`[UxmlElement]`** | UXML에서 사용 가능하도록 등록 |
|
|
|
|
|
| **`partial class`** | 소스 생성기 요구사항 |
|
|
|
|
|
| **`[UxmlAttribute]`** | UXML 속성 매핑 (케밥 케이스) |
|
|
|
|
|
| **`IDisposable`** | 리소스 정리 인터페이스 |
|
|
|
|
|
| **`UTKThemeManager`** | 테마 스타일시트 적용 |
|
|
|
|
|
| **`DetachFromPanelEvent`** | 패널에서 분리 시 이벤트 해제 |
|
|
|
|
|
| **UXML 경로** | `Resources.Load<VisualTreeAsset>()` 사용 |
|
|
|
|
|
| **USS 경로** | `Resources.Load<StyleSheet>()` 사용, 파일명에 `Uss` 접미사 |
|
|
|
|
|
| **Fallback 패턴** | UXML 로드 실패 시 코드로 UI 생성 |
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
---
|
|
|
|
|
|
2026-01-13 20:39:45 +09:00
|
|
|
## 12) Unity Nullable 주의
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
// Unity Object는 == null 오버로드됨
|
|
|
|
|
if (gameObject == null) { } // 파괴된 객체도 true
|
|
|
|
|
|
|
|
|
|
// 순수 참조 null 확인 시
|
|
|
|
|
if (ReferenceEquals(obj, null)) { }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- 직렬화 필드에 `?` 표기, 런타임 Null/파괴 상태 방어적 처리
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-01-13 20:39:45 +09:00
|
|
|
## 13) 품질 자동화
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
### .editorconfig 권장
|
|
|
|
|
```ini
|
|
|
|
|
dotnet_diagnostic.CS1591.severity = error # 공개 멤버 문서 주석 필수
|
|
|
|
|
dotnet_analyzer_diagnostic.category-Nullable.severity = error
|
|
|
|
|
dotnet_diagnostic.IDE0060.severity = warning # 미사용 매개변수
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### CI
|
|
|
|
|
- XML 문서 누락/nullable 경고 → 빌드 실패 처리
|
|
|
|
|
- 에디트 모드/플레이 모드 테스트 분리 실행
|