Skip to content

Commit ea5501b

Browse files
Nekoya-JinNekoya-Jin
authored andcommitted
v1.0.0: UniFP 구조 재정비 및 예제/테스트 전면 개편
0 parents  commit ea5501b

File tree

111 files changed

+9669
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+9669
-0
lines changed

.github/copilot-instructions.md

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
# UniFP AI 코딩 가이드
2+
3+
이 문서는 AI 코딩 에이전트가 UniFP 프로젝트를 효과적으로 작업할 수 있도록 가이드를 제공합니다.
4+
5+
## 프로젝트 개요
6+
7+
**UniFP**는 Unity를 위한 함수형 프로그래밍 라이브러리입니다. Result 모나드와 파이프라인 패턴을 통해 안전하고 명시적인 에러 처리를 제공합니다.
8+
9+
### 핵심 컨셉트
10+
- **Result 모나드**: 성공(Success) 또는 실패(Failure)를 타입으로 표현
11+
- **파이프라인 체이닝**: Bind, Map을 통한 함수형 조합
12+
- **명시적 에러 처리**: 예외 대신 Result로 에러를 전파
13+
- **Zero Allocation**: 구조체 기반 설계로 GC 부담 최소화
14+
15+
## 프로젝트 구조
16+
17+
```
18+
.
19+
├── src/
20+
│ └── UniFP/
21+
│ └── Assets/
22+
│ └── Plugins/
23+
│ └── UniFP/ # 핵심 라이브러리 코드
24+
│ ├── Result.cs # Result<T> 모나드 구현
25+
│ ├── Pipe.cs # 파이프라인 유틸리티
26+
│ ├── package.json # UPM 패키지 정의
27+
│ └── UniFP.asmdef # Assembly Definition
28+
├── Assets/ # Unity 테스트 프로젝트
29+
│ ├── Test.cs # 예제 및 테스트 코드
30+
│ └── ...
31+
├── docs/ # 문서
32+
│ ├── getting-started.md
33+
│ ├── api-reference.md
34+
│ ├── best-practices.md
35+
│ └── examples.md
36+
├── README.md
37+
├── CHANGELOG.md
38+
└── LICENSE
39+
```
40+
41+
## 아키텍처 원칙
42+
43+
### 1. 구조체 기반 설계
44+
Result<T>는 구조체로 설계되어 스택에 할당됩니다. 이는 GC 부담을 최소화하기 위함입니다.
45+
46+
```csharp
47+
public readonly struct Result<T> // struct, not class!
48+
```
49+
50+
### 2. 불변성 (Immutability)
51+
모든 연산은 새로운 Result를 반환하며, 기존 Result를 변경하지 않습니다.
52+
53+
```csharp
54+
// 각 연산은 새로운 Result를 반환
55+
var result1 = Result<int>.Success(10);
56+
var result2 = result1.Map(x => x * 2); // result1은 변경되지 않음
57+
```
58+
59+
### 3. 널 체크 최소화
60+
가독성과 성능을 위해 불필요한 널 체크는 하지 않습니다. 대신 Filter를 사용합니다.
61+
62+
```csharp
63+
// ❌ 피할 것
64+
if (input == null) return Failure("null");
65+
if (input.Length == 0) return Failure("empty");
66+
67+
// ✅ 권장
68+
Pipe.Start(input)
69+
.Filter(x => x?.Length > 0, "입력이 유효하지 않습니다")
70+
```
71+
72+
## 코드 작성 규칙
73+
74+
### 1. 함수 시그니처
75+
- 에러가 발생할 수 있는 함수는 `Result<T>` 반환
76+
- 순수 변환 함수는 `T` 반환 (Map에서 사용)
77+
78+
```csharp
79+
// Result를 반환 (Bind용)
80+
Result<int> ParseInt(string s)
81+
{
82+
return int.TryParse(s, out var value)
83+
? Result<int>.Success(value)
84+
: Result<int>.Failure("파싱 실패");
85+
}
86+
87+
// 값을 반환 (Map용)
88+
string FormatAge(int age) => $"{age}세";
89+
```
90+
91+
### 2. 에러 메시지
92+
명확하고 구체적인 에러 메시지를 작성합니다.
93+
94+
```csharp
95+
// ❌ 피할 것
96+
Result<T>.Failure("Error")
97+
Result<T>.Failure("Invalid")
98+
99+
// ✅ 권장
100+
Result<T>.Failure("파일을 찾을 수 없습니다: {path}")
101+
Result<T>.Failure("나이는 0보다 커야 합니다")
102+
```
103+
104+
### 3. 주석 작성
105+
모든 public 메서드에는 XML 문서 주석을 작성합니다.
106+
107+
```csharp
108+
/// <summary>
109+
/// 문자열을 정수로 파싱합니다.
110+
/// </summary>
111+
/// <param name="s">파싱할 문자열</param>
112+
/// <returns>
113+
/// Success: 파싱된 정수
114+
/// Failure: 파싱 실패 메시지
115+
/// </returns>
116+
public Result<int> ParseInt(string s)
117+
```
118+
119+
### 4. Region 사용
120+
생성자는 `#region Construction`으로 그룹화합니다.
121+
122+
```csharp
123+
#region Construction
124+
private Result(bool isSuccess, T value, string error)
125+
{
126+
_isSuccess = isSuccess;
127+
_value = value;
128+
_error = error;
129+
}
130+
#endregion
131+
```
132+
133+
## 일반적인 패턴
134+
135+
### 파이프라인 구성
136+
137+
```csharp
138+
// 기본 패턴
139+
var result = Pipe.Start(initialValue)
140+
.Bind(Step1) // Result<T> 반환
141+
.Map(Step2) // T 반환
142+
.Bind(Step3);
143+
144+
// 디버깅 패턴
145+
var result = Pipe.Start(initialValue)
146+
.Do(x => Debug.Log($"단계 1: {x}"))
147+
.Bind(Process)
148+
.Do(x => Debug.Log($"단계 2: {x}"));
149+
150+
// 검증 패턴
151+
var result = Pipe.Start(input)
152+
.Filter(x => x > 0, "양수여야 합니다")
153+
.Filter(x => x < 100, "100보다 작아야 합니다");
154+
155+
// 복구 패턴
156+
var result = Pipe.Start(LoadConfig())
157+
.Recover(error => defaultConfig);
158+
```
159+
160+
### 예외 처리
161+
162+
```csharp
163+
// Try로 예외를 Result로 변환
164+
var result = Pipe.Try(() =>
165+
{
166+
return JsonUtility.FromJson<Data>(json);
167+
});
168+
169+
// 파이프라인에서 사용
170+
var result = Pipe.Try(() => LoadFile(path))
171+
.Bind(ValidateData)
172+
.Bind(ProcessData);
173+
```
174+
175+
## DI 통합 (VContainer)
176+
177+
### 일반 클래스
178+
생성자 주입을 사용하며, 생성자는 Region으로 감쌉니다.
179+
180+
```csharp
181+
public class UserService
182+
{
183+
private readonly IUserRepository _repository;
184+
185+
#region Construction
186+
public UserService(IUserRepository repository)
187+
{
188+
_repository = repository;
189+
}
190+
#endregion
191+
192+
public Result<User> GetUser(int id)
193+
{
194+
return Pipe.Start(id)
195+
.Filter(x => x > 0, "ID는 양수여야 합니다")
196+
.Bind(_repository.FindById);
197+
}
198+
}
199+
```
200+
201+
### MonoBehaviour
202+
메서드 주입을 사용합니다.
203+
204+
```csharp
205+
public class UserController : MonoBehaviour
206+
{
207+
private UserService _userService;
208+
209+
[Inject]
210+
public void Construct(UserService userService)
211+
{
212+
_userService = userService;
213+
}
214+
}
215+
```
216+
217+
## 테스트 작성
218+
219+
### 단위 테스트 패턴
220+
221+
```csharp
222+
[Test]
223+
public void MethodName_Condition_ExpectedResult()
224+
{
225+
// Arrange
226+
var input = "test";
227+
228+
// Act
229+
var result = MethodUnderTest(input);
230+
231+
// Assert
232+
Assert.IsTrue(result.IsSuccess);
233+
Assert.AreEqual(expectedValue, result.Value);
234+
}
235+
```
236+
237+
## 피해야 할 안티패턴
238+
239+
### 1. Result 내부에서 예외 던지기
240+
```csharp
241+
// ❌ 잘못됨
242+
public Result<int> Parse(string s)
243+
{
244+
if (string.IsNullOrEmpty(s))
245+
throw new ArgumentException(); // Result를 사용하는 의미가 없음!
246+
// ...
247+
}
248+
249+
// ✅ 올바름
250+
public Result<int> Parse(string s)
251+
{
252+
if (string.IsNullOrEmpty(s))
253+
return Result<int>.Failure("입력이 비어있습니다");
254+
// ...
255+
}
256+
```
257+
258+
### 2. Result 무시하고 Value 직접 접근
259+
```csharp
260+
// ❌ 잘못됨
261+
var result = Process(input);
262+
var value = result.Value; // 실패 시 예외 발생!
263+
264+
// ✅ 올바름
265+
var result = Process(input);
266+
if (result.IsSuccess)
267+
var value = result.Value;
268+
```
269+
270+
### 3. 불필요한 중첩
271+
```csharp
272+
// ❌ 잘못됨
273+
var result = Pipe.Start(input)
274+
.Bind(x =>
275+
{
276+
var temp = Process1(x);
277+
if (temp.IsSuccess)
278+
return Process2(temp.Value);
279+
return Result<int>.Failure(temp.Error);
280+
});
281+
282+
// ✅ 올바름
283+
var result = Pipe.Start(input)
284+
.Bind(Process1)
285+
.Bind(Process2);
286+
```
287+
288+
## 빌드 및 배포
289+
290+
### Unity Package Manager (UPM)
291+
이 프로젝트는 UPM 패키지로 배포됩니다. 핵심 코드는 `src/UniFP/Assets/Plugins/UniFP`에 있습니다.
292+
293+
### 버전 관리
294+
- `package.json`의 version 필드를 업데이트
295+
- `CHANGELOG.md`에 변경 사항 기록
296+
- Git 태그 생성: `v1.0.0`
297+
298+
## 참고 자료
299+
300+
- **UniTask**: https://github.com/Cysharp/UniTask - 비동기 처리 패턴 참고
301+
- **UniRx**: https://github.com/neuecc/UniRx - Reactive 패턴 참고
302+
- **R3**: https://github.com/Cysharp/R3 - 최신 Reactive Extensions
303+
304+
## 자주 묻는 질문
305+
306+
### Q: 언제 Bind를 사용하고 언제 Map을 사용하나요?
307+
A: Result를 반환하는 함수는 Bind, 일반 값을 반환하는 함수는 Map을 사용합니다.
308+
309+
### Q: 예외 처리는 어떻게 하나요?
310+
A: `Pipe.Try()`를 사용하여 예외를 Result로 변환합니다.
311+
312+
### Q: 여러 Result를 합치려면?
313+
A: 현재는 중첩된 Bind를 사용하거나, 각 Result를 개별적으로 확인한 후 조합합니다.
314+
315+
---
316+
317+
이 가이드를 따라 일관되고 유지보수하기 쉬운 UniFP 코드를 작성하세요!

.gitignore

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# .gitignore for Unity
2+
3+
# Unity generated
4+
[Ll]ibrary/
5+
[Tt]emp/
6+
[Oo]bj/
7+
[Bb]uild/
8+
[Bb]uilds/
9+
[Ll]ogs/
10+
[Uu]ser[Ss]ettings/
11+
12+
# Never ignore Asset meta data
13+
![Aa]ssets/**/*.meta
14+
15+
# Uncomment this line if you wish to ignore the asset store tools plugin
16+
# [Aa]ssets/AssetStoreTools*
17+
18+
# Visual Studio cache directory
19+
.vs/
20+
21+
# Gradle cache directory
22+
.gradle/
23+
24+
# Autogenerated VS/MD/Consulo solution and project files
25+
ExportedObj/
26+
.consulo/
27+
*.csproj
28+
*.unityproj
29+
*.sln
30+
*.suo
31+
*.tmp
32+
*.user
33+
*.userprefs
34+
*.pidb
35+
*.booproj
36+
*.svd
37+
*.pdb
38+
*.mdb
39+
*.opendb
40+
*.VC.db
41+
42+
# Unity3D generated meta files
43+
*.pidb.meta
44+
*.pdb.meta
45+
*.mdb.meta
46+
47+
# Unity3D generated file on crash reports
48+
sysinfo.txt
49+
50+
# Builds
51+
*.apk
52+
*.aab
53+
*.unitypackage
54+
*.app
55+
56+
# Crashlytics generated file
57+
crashlytics-build.properties
58+
59+
# Packed Addressables
60+
[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
61+
62+
# Temporary auto-generated Android Assets
63+
[Aa]ssets/[Ss]treamingAssets/aa.meta
64+
[Aa]ssets/[Ss]treamingAssets/aa/*
65+
66+
# macOS
67+
.DS_Store
68+
69+
# Rider
70+
.idea/
71+
72+
# VSCode
73+
.vscode/

0 commit comments

Comments
 (0)