# TusBlazorClient
## 1. 프로젝트 목적
### 해결하려는 문제
.NET Blazor WebAssembly 환경에서 **대용량 파일 업로드**를 안정적으로 처리하는 것. Blazor WASM의 순수 C# 파일 I/O는 속도가 느리고 전송 가능한 파일 크기에 제한이 있어, 대용량 파일 전송이 실질적으로 어렵다.
### 기존 방식의 불편함
- Blazor WASM에서 순수 C# 코드로 대용량 파일을 전송할 경우, 브라우저의 메모리 제약과 느린 I/O 속도로 인해 전송이 실패하거나 브라우저가 멈추는 현상이 발생한다.
- 네트워크 중단 시 처음부터 다시 업로드해야 하므로, 대용량 파일일수록 실패 확률이 높아진다.
- 기존 `tus-js-client`는 JavaScript 라이브러리이므로, Blazor C# 환경에서 직접 사용하기 어렵다.
### 라이브러리 사용 시 단순해지는 부분
- JavaScript 라이브러리를 직접 다루지 않고, **C# 네이티브 API**로 tus 프로토콜 업로드를 사용할 수 있다.
- DI 컨테이너에 `AddTusBlazorClient()` 한 줄로 등록 후 `TusClient`를 주입받아 즉시 사용 가능하다.
- 파일 선택, 업로드 생성, 진행률 추적, 재개, 중단, 옵션 변경까지 모두 C#에서 타입 세이프하게 처리된다.
### 주요 사용자
.NET Blazor WebAssembly 개발자. 특히 대용량 파일 업로드(동영상, 이미지, 문서 등)가 필요한 웹 애플리케이션을 구축하는 개발자.
### 한 줄 설명
"Blazor WebAssembly에서 tus 프로토콜 기반의 재개 가능한 대용량 파일 업로드를 C# API로 제공하는 래퍼 라이브러리"
---
## 2. Public API 설계
### 주요 클래스/인터페이스/메서드
| 클래스 | 역할 | 주요 멤버 |
|--------|------|----------|
| `TusClient` | 진입점 (Singleton) | `Upload()`, `IsSupported()`, `CanStoreUrls()`, `GetFileInputElement()` |
| `TusUpload` | 단일 업로드 작업 | `Start()`, `Abort()`, `Terminate()`, `GetUrl()`, `GetFileInfo()`, `GetOptions()`, `SetOtions()`, `FindPreviousUpload()`, `ResumeFromPreviousUpload()` |
| `TusOptions` | 설정 클래스 | `Endpoint`, `ChunkSize`, `OnProgress`, `OnError`, `OnSuccess`, `OnShouldRetry`, `Headers`, `Metadata` 등 18개 프로퍼티 |
| `FileInputElement` | HTML input[type=file] 래퍼 | `GetFiles()`, `Length()` |
| `JsFile` | 선택된 단일 파일 | `ToJsObjectReference()`, `GetFileInfo()` |
| `JsFileInfo` | 파일 메타데이터 | `Name`, `Size`, `LastModified` |
| `SetupExtension` | DI 등록 | `AddTusBlazorClient()` |
### 기본 사용 흐름
```
1. DI 등록: builder.Services.AddTusBlazorClient()
2. Razor 컴포넌트에서 TusClient 주입
3. FileInputElement.GetFiles() 로 파일 선택
4. TusOptions 구성 (Endpoint, 콜백 등)
5. TusClient.Upload(file, options) 으로 TusUpload 생성
6. (선택) FindPreviousUpload() + ResumeFromPreviousUpload() 로 이어 올리기
7. TusUpload.Start() 호출
```
### Options/Config 구조
`TusOptions`는 **클래스**로 설계되어, 프로퍼티에 기본값이 지정되어 있다. 콜백 프로퍼티는 `[JsonIgnore]`로 마킹되어 JSON 직렬화 대상에서 제외된다 (JS로 전달 불가능한 .NET 델리게이트이기 때문).
### 비동기 API 설계
모든 I/O 관련 메서드는 `Task` 또는 `ValueTask`를 반환한다. JavaScript interop 호출이 비동기이기 때문에 전체 API가 비동기로 설계되어 있다.
### 사용 예시 코드
```csharp
@inject TusClient TusClient
@code {
private ElementReference _fileElement;
private TusUpload? _tusUpload;
private async Task Upload()
{
var file = (await TusClient.GetFileInputElement(_fileElement).GetFiles()).First();
var fileInfo = await file.GetFileInfo();
var opt = new TusOptions
{
Endpoint = new Uri("http://localhost:1080/files"),
Metadata = new Dictionary { { "filename", fileInfo.Name } },
OnError = (err) => Console.WriteLine($"Failed: {err.ErrorMessage}"),
OnProgress = (uploaded, total) => Console.WriteLine($"{(double)uploaded / total:P}"),
OnSuccess = async () => {
var url = await _tusUpload!.GetUrl();
Console.WriteLine($"Uploaded to {url}");
},
};
_tusUpload = await TusClient.Upload(file, opt);
var previousUploads = await _tusUpload.FindPreviousUpload();
if (previousUploads.Count > 0)
await _tusUpload.ResumeFromPreviousUpload(previousUploads.First());
await _tusUpload.Start();
}
}
```
### API 설계 의도
- **TusClient를 Singleton**으로 등록해 JS 모듈을 한 번만 로드하고 모든 업로드가 공유한다.
- **TusUpload는 내부 생성자**로 제한하여, 반드시 `TusClient.Upload()`를 통해서만 생성하도록 강제한다. 이로 인해 JS 콜백 브릿지(`DotNetObjectReference`)가 올바르게 연결되는 것을 보장한다.
- **Fluent API가 아닌 명령형 API**를 채택했다. 옵션 객체를 구성하고, 업로드 생성 후 명령을 내리는 직관적인 구조다.
---
## 3. 내부 구조와 책임 분리
### 주요 폴더 구조
```
TusBlazorClient/
├── TusClient.cs # Public API 진입점
├── TusOptions.cs # 설정 모델
├── TusUpload.cs # 단일 업로드 작업
├── TusError.cs # 오류 모델
├── TusHttpRequest.cs # HTTP 요청 DTO
├── TusHttpResponse.cs # HTTP 응답 DTO
├── TusPreviousUpload.cs # 이전 업로드 DTO
├── TusJsInterop.cs # .NET ↔ JS 브릿지 계층
├── TusOptionJsInvoke.cs # JS → .NET 콜백 수신기
├── TusOptionNullCheck.cs # Null 콜백 최적화 정보
├── FileInputElement.cs # File input 래퍼 + JsFile, JsFileInfo
├── SetupExtension.cs # DI 등록 확장 메서드
└── wwwroot/
└── tusBlazorClient.js # JS ↔ tus-js-client 브릿지
```
### 주요 클래스별 책임
| 클래스 | 책임 |
|--------|------|
| `TusClient` | 외부 API 진입점, 업로드 생성 팩토리 |
| `TusJsInterop` | `IJSRuntime`을 통한 JavaScript ES 모듈 호출 관리, Lazy 초기화 |
| `tusBlazorClient.js` | `tus-js-client`의 옵션 변환, 콜백 null 체크, HTTP 헤더 파싱 |
| `TusUpload` | 단일 업로드의 생명주기 관리, 옵션 동적 변경 |
| `TusOptionJsInvoke` | `[JSInvokable]` 메서드로 JS에서 .NET 델리게이트 호출 중계 |
| `TusOptionNullCheck` | JS 측에서 불필요한 콜백 호출을 방지하는 정보 제공 |
### 인터페이스와 구현체 분리
**인터페이스가 별도로 추출되어 있지 않다.** `TusClient`와 `TusJsInterop` 모두 구체 클래스만 존재한다. 이는 라이브러리 규모가 작은 점을 고려한 선택으로 보인다. 테스트는 E2E 테스트(Selenium)로 커버하고 있어 Mocking이 필요하지 않다.
### 내부 구현과 외부 API 분리
- `TusUpload` 생성자는 **internal**로 제한되어 있다. 외부 사용자는 반드시 `TusClient.Upload()`를 통해서만 인스턴스를 얻을 수 있다.
- `TusJsInterop`는 **internal 멤버**를 통해 `TusUpload`, `FileInputElement` 등 내부 클래스만 접근 가능한 메서드를 노출한다.
- `TusOptionJsInvoke`, `TusOptionNullCheck`는 외부에 노출되지 않고 `TusClient` 내부에서만 생성된다.
### 확장 가능한 구조
`TusOptions`에 새로운 프로퍼티를 추가하는 방식으로 기능 확장이 이루어지며, `tusBlazorClient.js`의 `GetUploadOption`에서도 동일한 옵션을 매핑해야 한다. 현재 확장 포인트가 인터페이스로 정형화되어 있지 않다.
### 디자인 패턴 사용
| 패턴 | 적용 위치 |
|------|----------|
| **Singleton** | `TusClient`를 DI Singleton으로 등록 |
| **Adapter (래퍼)** | `tus-js-client`를 C#에 맞게 감싼 전체 구조가 Adapter 패턴 |
| **Builder-like** | `TusOptions` 객체를 구성한 후 업로드 생성 → Builder 패턴의 변형 |
| **IDisposable/AsyncDisposable** | `TusClient`, `TusUpload`, `TusJsInterop` 모두 구현 |
| **Extension Method** | `SetupExtension.AddTusBlazorClient()` |
---
## 4. 설정과 확장성
### 설정 가능한 옵션 목록 (TusOptions)
| 옵션 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| `Endpoint` | `Uri` | **(필수)** | 업로드 생성 URL |
| `ChunkSize` | `long?` | `null` (Infinity) | PATCH 요청 본문 최대 크기 (byte) |
| `OnProgress` | `Action?` | `null` | 진행률 콜백 (bytesSent, bytesTotal) |
| `OnChunkComplete` | `Action?` | `null` | 청크 완료 콜백 |
| `OnSuccess` | `Action?` | `null` | 업로드 성공 콜백 |
| `OnError` | `Action?` | `null` | 오류 발생 콜백 |
| `OnShouldRetry` | `Func?` | `null` | 재시도 결정 콜백 |
| `Headers` | `Dictionary` | `new()` | 커스텀 HTTP 헤더 |
| `Metadata` | `Dictionary` | `new()` | 업로드 생성 시 메타데이터 |
| `UploadUrl` | `Uri?` | `null` | 직접 재개용 URL |
| `RetryDelays` | `List` | `[0, 1000, 3000, 5000]` | 재시도 대기 시간 (ms) |
| `StoreFingerprintForResuming` | `bool` | `true` | URL 저장소에 fingerprint 저장 |
| `RemoveFingerprintOnSuccess` | `bool` | `false` | 업로드 완료 시 fingerprint 제거 |
| `UploadLengthDeferred` | `bool` | `false` | Upload-Defer-Length 헤더 사용 |
| `UploadDataDuringCreation` | `bool` | `false` | 생성-with-upload 확장 사용 |
| `AddRequestId` | `bool` | `false` | 랜덤 Request ID 추가 |
| `ParallelUploads` | `int` | `1` | 병렬 업로드 수 |
| `ParallelUploadBoundaries` | `List<(int, int)>?` | `null` | 병렬 업로드 파트 경계 |
### 기본값 제공 방식
`TusOptions` 클래스에서 프로퍼티 초기화로 기본값을 제공한다. `new TusOptions()`만 생성해도 `RetryDelays`, `Headers`, `Metadata`, `StoreFingerprintForResuming`, `ParallelUploads` 등이 유효한 기본값을 가진다.
### 설정 검증 방식
명시적인 설정 검증 로직은 발견되지 않는다. JavaScript 레벨에서 `tus-js-client`가 자체적으로 검증할 것으로 추정된다. **(확인 필요)**
### 확장 지점
- 콜백: `OnBeforeRequest`, `OnAfterResponse`를 통해 모든 HTTP 요청/응답을 가로채고 분석할 수 있다.
- `OnShouldRetry`로 재시도 로직을 사용자가 완전히 제어할 수 있다.
- `Headers`로 인증 토큰 등 커스텀 헤더 주입이 가능하다.
- 인터페이스 기반 확장 포인트는 존재하지 않으며, 상속을 통한 교체도 지원되지 않는다.
### API 호환성 설계
`TusOptions`는 클래스이므로 새 프로퍼티 추가 시 기존 코드가 깨지지 않는다. `TusUpload.SetOtions()` API가 있어 업로드 실행 중에도 옵션 변경이 가능하다.
---
## 5. 오류 처리
### 예외 처리 방식
- `TusClient`에서 `IsSupported()`, `CanStoreUrls()` 등 간단한 조회 메서드는 `try-catch`로 감싸 실패 시 `false`를 반환한다.
- 그 외 직접적인 예외 throw/catch 패턴보다는 **콜백 기반 오류 전달**을 주로 사용한다.
### Result/Error 타입 사용
별도의 Result 타입은 없지만, `TusError`라는 전용 오류 클래스가 존재한다. `TusError`는 오류 메시지와 함께 실패한 요청/응답의 상세 정보를 포함한다.
### 사용자 입력 오류와 시스템 오류 구분
코드에서 명시적으로 구분하고 있지 않다. 모든 오류는 `OnError` 콜백을 통해 `TusError`로 전달된다. 네트워크 오류, HTTP 4xx/5xx 오류 모두 동일 콜백으로 수신된다.
### 커스텀 예외/에러 코드
별도 정의되어 있지 않다. `tus-js-client`의 오류 문자열을 그대로 `ErrorMessage`에 담아 전달한다.
### 실패 원인 전달 방식
`TusError`는 다음 정보를 외부 사용자에게 전달한다:
- `ErrorMessage` (string): 오류 설명
- `OriginalHttpRequest` (TusHttpRequest?): 실패한 요청의 HTTP 메서드, URL
- `OriginalHttpResponse` (TusHttpResponse?): 응답의 StatusCode, Body, Headers
이 구조를 통해 사용자는 네트워크 레벨에서 무슨 일이 일어났는지 상세히 파악할 수 있다.
### 재시도 가능/불가능 오류 구분
`OnShouldRetry` 콜백에서 `TusError`와 `retryAttempt`(long)을 받아 사용자가 직접 재시도 여부를 결정한다. 콜백을 지정하지 않으면 tus-js-client의 기본 재시도 로직(409, 423, 4XX 이외 상태코드 재시도)이 적용된다.
---
## 6. 상태 관리
### 상태 enum/객체
별도의 상태 enum은 존재하지 않는다. 업로드 상태는 tus-js-client 내부에서 관리되며, 콜백(`OnProgress`, `OnSuccess`, `OnError`)을 통해 상태 변화가 외부에 통지된다.
### 상태 전이 흐름
```
[생성: TusClient.Upload()]
│
├── FindPreviousUpload() → ResumeFromPreviousUpload() (선택적)
│
├── Start()
│ │
│ ├── OnProgress → OnChunkComplete → ... (진행 중)
│ │ │
│ │ ├── Abort(true/false) → 중단
│ │ │ └── Start() → 재개
│ │ │
│ │ ├── OnError → (OnShouldRetry) → 자동 재시도 or 중단
│ │ │
│ │ └── OnSuccess → 완료
│ │
│ └── Terminate(url) → 서버에서 완전히 제거
│
└── DisposeAsync() → 리소스 정리
```
### 완료/실패/취소 처리
- **완료**: `OnSuccess` 콜백 → 업로드 URL을 `GetUrl()`로 획득 가능
- **실패**: `OnError` 콜백 → `TusError`로 상세 정보 확인
- **취소**: `Abort(bool shouldTerminate)` → `shouldTerminate=true`면 서버에서도 삭제
### 중복 실행 방지
`TusClient.Upload()`는 상태를 확인하지 않고 항상 새 `TusUpload`를 생성한다. `TusUpload.Start()`를 중복 호출하는 것에 대한 방어 로직은 확인되지 않는다.
### 동시성 / Thread-Safe
Blazor WASM은 단일 스레드에서 실행되므로, 별도의 동시성 제어가 필요하지 않다. `lock`, `SemaphoreSlim` 등은 사용되지 않는다.
---
## 7. 사용성
### README 구조
README.md는 다음과 같은 구조로 잘 정리되어 있다:
1. 프로젝트 소개 (tus 프로토콜 설명)
2. 사용 동기 ("Why do I use this?")
3. 설치 방법 (CDN + NuGet + DI 등록)
4. 완전한 예제 코드
5. Wiki 링크 (상세 API 문서)
### Quick Start
README에 포함된 예제 코드가 Quick Start 역할을 동시에 수행한다. 별도의 튜토리얼 페이지는 없다.
### 샘플 프로젝트
`TusBlazorClient.Demo` 프로젝트가 존재하며, 13개의 페이지로 주요 기능을 모두 시연한다:
| 페이지 | 시연 기능 |
|--------|----------|
| Index | `IsSupported()`, `CanStoreUrls()` |
| Upload | 기본 업로드 |
| UploadByJsObjectReference | IJSObjectReference 직접 사용 |
| UploadResume | 업로드 중단 후 재개 |
| ResumeFromPreviousUpload | 이전 업로드 찾아 재개 |
| ShouldRetry | 무한 재시도 |
| ShouldNoRetry | 재시도 없음 |
| OnRequest | OnBeforeRequest/OnAfterResponse |
| SetOption | 업로드 중 옵션 변경 |
| GetOption | 옵션 조회 |
| GetFileInfo | 파일 메타데이터 |
### 최소 사용 코드
```csharp
@inject TusClient TusClient
@code {
private ElementReference _el;
private async Task Upload() {
var file = (await TusClient.GetFileInputElement(_el).GetFiles()).First();
var upload = await TusClient.Upload(file,
new TusOptions { Endpoint = new Uri("https://example.com/files") });
await upload.Start();
}
}
```
### XML 주석/문서화 수준
`TusOptions`의 모든 프로퍼티에 XML 문서 주석(`/// `)이 작성되어 있으며, `TusUpload.GetUrl()` 등 주요 메서드에도 주석이 있다. 단, `TusClient`, `TusJsInterop`, `FileInputElement`에는 주석이 거의 없다.
### 처음 사용자가 헷갈릴 수 있는 부분
- `TusOptions`의 콜백은 `[JsonIgnore]`이므로, `GetOptions()`으로 받은 옵션 객체를 그대로 `Upload()`에 재사용하면 콜백이 유실된다. `GetOptions()` 내부에서 수동으로 콜백을 merge하는 이유가 여기에 있다.
- `tus-js-client` CDN 로드를 잊으면 라이브러리가 동작하지 않는다.
- `Upload()`을 호출할 때마다 새 `DotNetObjectReference`가 생성되므로, 이전 upload 객체를 `Dispose`하지 않으면 리소스 누수가 발생할 수 있다.
---
## 8. 테스트와 검증
### 테스트 프로젝트
`TusBlazorClient.Test` 프로젝트가 존재한다. NUnit 3.13.3 + Selenium WebDriver 4.12.4 (Firefox) 조합으로 E2E 테스트를 수행한다.
### 단위 테스트
**존재하지 않음.** 별도의 단위 테스트는 없다.
### 통합 테스트 (E2E)
8개의 Selenium E2E 테스트 케이스:
| 테스트 | 검증 내용 |
|--------|----------|
| `Upload()` | 기본 업로드 성공, 진행률 콜백, 청크 완료 콜백, 파일 이름 일치, 유효한 업로드 URL |
| `UploadByJsObj()` | `ToJsObjectReference()` 경로 업로드 성공 |
| `UploadResume()` | 중단 후 `Start()` 재개, 재개 시 첫 progress가 0이 아님 |
| `ResumeFromPreviousUpload()` | `FindPreviousUpload()` + `ResumeFromPreviousUpload()` 성공 |
| `ShouldRetry()` | `OnShouldRetry`가 true 반환 시 5회 이상 재시도 발생 |
| `ShouldNotRetry()` | `OnShouldRetry`가 false 반환 시 1회 이하 재시도 |
| `OnRequest()` | `OnBeforeRequest`, `OnAfterResponse` 발생, 응답에 `tus-resumable` 헤더 존재 |
| `SetOption()` | 업로드 중 `SetOtions()`으로 ChunkSize 변경, 변경된 청크 크기로 전송됨 |
| `GetOption()` | 업로드 성공 후 `GetOptions()`로 옵션 조회 가능 |
### 실패 케이스 테스트
`ShouldRetry()`와 `ShouldNotRetry()` 두 테스트가 오류 시나리오를 커버한다. 의도적으로 틀린 URL로 업로드를 시도하여 오류 콜백과 재시도 동작을 검증한다.
### Mock/Fake
사용되지 않음. 모든 테스트는 실제 tus 서버(Docker로 구동된 `tusd`, `172.17.0.3:8080`)와 실제 Firefox 브라우저를 사용한다.
### 테스트가 부족한 부분
- **단위 테스트 부재**: `TusOptionJsInvoke`, `TusOptions`, `TusOptionNullCheck` 등 순수 C# 로직에 대한 단위 테스트가 없다.
- **병렬 업로드 테스트 부재**: `ParallelUploads` > 1 시나리오가 없음.
- **다양한 오류 시나리오 부족**: 네트워크 중단, 타임아웃, 서버 오류 등 세분화된 오류 케이스가 부재.
- **Chrome/Edge 브라우저 테스트 부재**: Firefox만 사용.
---
## 9. 패키징과 배포
### 패키지 배포 구조
- NuGet.org에 `TusBlazorClient` 패키지로 배포되어 있다.
- `GeneratePackageOnBuild=true`로 빌드 시 자동 패키징된다.
- `.sln` 파일과 세 개의 프로젝트로 구성된 표준 .NET 솔루션 구조다.
### 버전 관리
- Semantic Versioning 사용. 현재 버전 `1.0.1`.
- csproj에 `1.0.1`로 직접 명시.
- Git 히스토리 기준 약 20개의 커밋으로 진화됨.
### 패키지 메타데이터
```
Title: TusBlazorClient
Description: tus-blazor-client is a wrapper library project for tus-js-client
that can be used in .NET Blazor.
Copyright: MIT
PackageProjectUrl: https://github.com/thsdmfwns/tus-blazor-client
PackageLicenseUrl: (LICENSE 파일)
PackageTags: tus, blazor, wrapper, js, browser
PackageReleaseNotes: 1.0.0
Version: 1.0.1
```
### CI/CD
코드 저장소에서 GitHub Actions나 다른 CI/CD workflow 파일은 **존재하지 않는다**. **(확인 필요)**
### 빌드/테스트/패키징 명령어
- 빌드: `dotnet build`
- 테스트: `dotnet test` (단, Selenium E2E 테스트는 Firefox + Docker tusd 서버 필요)
- 패키징: 빌드 시 `GeneratePackageOnBuild=true`로 자동 생성
### 외부 프로젝트 사용 가능 상태
NuGet.org에 배포된 버전 1.0.1이 있고, 정상적으로 설치하여 사용할 수 있는 상태다. 단, 아래 선행 조건이 필요하다:
1. 대상 프로젝트가 Blazor WebAssembly여야 함
2. `index.html`에 `tus-js-client` CDN 스크립트 추가 필요
3. DI에 `AddTusBlazorClient()` 등록 필요
---
## 10. 성능과 리소스 고려
### async/await 사용 방식
- 모든 JS interop 메서드는 `async`로 선언되어 있으며, `ValueTask`를 반환하여 불필요한 Task 할당을 방지한다.
- `IAsyncDisposable`을 구현하여 JS 리소스를 비동기적으로 정리한다.
### Stream/Buffer 처리
.NET 측에서는 Stream이나 buffer를 직접 다루지 않는다. 실제 파일 전송은 브라우저의 `tus-js-client`가 처리하며, .NET 측은 설정값을 전달하고 콜백을 수신하는 역할만 한다.
### 메모리 사용량을 줄이기 위한 구조
- **Lazy 모듈 로딩**: `TusJsInterop`는 JavaScript ES module을 최초 사용 시점까지 지연 로딩한다 (`InitializeAsync()`). 사용하지 않으면 모듈이 로드되지 않는다.
- **TusOptionNullCheck**: 콜백이 null인 경우 JavaScript에서 `invokeMethodAsync` 호출을 아예 건너뛰도록 하여, .NET↔JS 간 불필요한 마샬링을 제거한다.
- **onBeforeRequest / onAfterResponse 동기화**: 이 두 콜백은 내부 상태를 변경하지 않으므로 `invokeMethodAsync` 대신 `invokeMethod`(동기 호출)를 사용한다.
### 반복 객체 생성 최소화
- `TusClient`는 Singleton으로 등록되어 JS 모듈을 재사용한다.
- `TusOptionJsInvoke`, `TusOptionNullCheck`는 upload 생성 시마다 새로 생성된다 (각 업로드가 다른 콜백을 가질 수 있으므로 불가피).
### 성능 측정/벤치마크
**존재하지 않는다.** 코드 내 성능 측정이나 벤치마크 코드는 발견되지 않았다.
---
## 11. 포트폴리오용 문제 해결 사례 후보
---
### 사례 1: Blazor ↔ JavaScript 콜백 브릿지 설계
#### 문제 상황
`tus-js-client`는 업로드 진행 상황, 오류, 성공 등을 JavaScript 콜백으로 통지한다. 이 콜백을 C# Blazor에서 받아 개발자가 C# 델리게이트로 처리할 수 있도록 해야 했다.
#### 원인 분석
Blazor와 JavaScript 간 데이터 전달은 JSON 직렬화 기반으로 이루어지지만, C# 델리게이트(`Action`, `Func`)는 직렬화할 수 없다. 또한 JavaScript→.NET 호출은 `invokeMethodAsync`를 통해 이루어지는데, 콜백이 설정되지 않은 경우에도 호출이 발생하면 불필요한 오버헤드가 발생한다.
#### 해결 방법
1. **TusOptionJsInvoke** 클래스를 도입하여 모든 `[JSInvokable]` 메서드를 한 객체에 모았다. 이 객체를 `DotNetObjectReference`로 감싸 JavaScript에 전달했다.
2. 콜백 프로퍼티는 `[JsonIgnore]`로 직렬화에서 제외하고, JavaScript 옵션 구성용 실제 값만 전달했다.
3. **TusOptionNullCheck**를 별도로 생성하여 각 콜백의 null 여부를 JavaScript 측에 알려주고, JS 측에서 `if (optNullCheck.isNullOnError) return;` 과 같이 early return 하도록 했다.
4. `OnBeforeRequest`, `OnAfterResponse`처럼 내부 상태를 변경하지 않는 콜백은 `invokeMethod`(동기)로 호출하여 오버헤드를 더 줄였다.
#### 선택 이유
- `DotNetObjectReference`는 Blazor가 공식 제공하는 JS→.NET 호출 메커니즘으로, 가장 안정적인 방법이다.
- 별도의 인터페이스를 정의하지 않고 하나의 `TusOptionJsInvoke` 클래스에 모든 콜백을 통합한 이유는 JS 모듈에 전달할 .NET 참조를 단일 객체로 유지하기 위해서다.
#### 결과
- C# 개발자는 JavaScript를 한 줄도 작성하지 않고 순수 C# 델리게이트로 모든 이벤트를 처리할 수 있게 되었다.
- null 체크 최적화로 불필요한 JS→.NET 호출이 제거되어, 콜백을 일부만 사용하는 시나리오에서 성능이 개선되었다.
---
### 사례 2: 업로드 중 옵션 동적 변경 (SetOtions)
#### 문제 상황
업로드 진행 중에 ChunkSize를 변경하거나 새로운 콜백을 등록해야 하는 사용 사례가 있었다. tus-js-client는 생성 시점에 옵션을 설정하는 구조이지만, 업로드 옵션 객체를 직접 수정하면 반영될 수 있었다.
#### 원인 분석
`TusUpload` 생성 시 전달된 `DotNetObjectReference`와 `TusOptionJsInvoke`는 옵션 변경 후에도 기존 콜백을 참조하고 있어, 옵션을 그냥 변경하면 콜백이 동기화되지 않는 문제가 있었다. 또한 JavaScript 측의 `tus.Upload.options` 객체의 개별 프로퍼티에 새 값을 할당해야 했다.
#### 해결 방법
1. `TusUpload.SetOtions(Action setOption)` API를 설계했다.
2. 내부적으로 `GetOptions()` → callback merge → `setOption` 호출 → 새 `DotNetObjectReference` 생성 → `SetTusUploadOption` JS 호출의 파이프라인을 구현했다.
3. 기존 `_optionJsInvokeReference`를 dispose하고 새로 생성하여 콜백 참조를 갱신했다.
4. JS 측에서는 `upload.options.endpoint = opt.endpoint` 패턴으로 개별 프로퍼티를 in-place 업데이트했다.
#### 선택 이유
- 액션 기반 API (`Action`)는 사용자가 변경하고 싶은 옵션만 선택적으로 수정할 수 있도록 해준다.
- `TusUpload` 인스턴스를 폐기하고 새로 생성하는 것보다 효율적이다.
#### 결과
- `SetOption` E2E 테스트로 검증되어 있다. 업로드 50% 지점에서 ChunkSize를 50000에서 15000으로 변경한 후 정상 재개되었다.
---
### 사례 3: 이전 업로드 탐지 및 재개 (FindPreviousUpload / ResumeFromPreviousUpload)
#### 문제 상황
브라우저를 종료했다 다시 열거나, 페이지를 실수로 새로고침한 경우, 진행 중이던 업로드를 이어서 진행할 수 있도록 하는 기능이 필요했다. tus-js-client는 `findPreviousUploads()` API를 제공하지만, 이 기능을 C#으로 노출해야 했다.
#### 원인 분석
`findPreviousUploads()`는 브라우저 URL 저장소에 저장된 이전 업로드 목록을 JavaScript 객체 배열로 반환한다. 이 정보를 C# DTO로 변환해야 하고, 사용자가 특정 업로드를 선택해 재개할 수 있도록 해야 했다.
#### 해결 방법
1. `findPreviousUploads()` 호출 결과를 `List`로 역직렬화하는 `InvokeAsync` 호출을 구현했다.
2. 단순히 DTO만 반환하지 않고, `TusPreviousUploadRef` 래퍼를 도입해 `Index`를 함께 보관하도록 했다. 이 Index가 `resumeFromPreviousUpload(pres[index])` 호출에 필요하기 때문이다.
3. `TusUpload.FindPreviousUpload()`와 `TusUpload.ResumeFromPreviousUpload(TusPreviousUploadRef)` API를 제공하여 사용자가 목록을 조회하고 특정 항목을 골라 재개할 수 있게 했다.
#### 선택 이유
- JavaScript 배열 인덱스는 JSON 역직렬화 과정에서 소실되므로, C# 측에서 명시적으로 Index를 추적하는 래퍼 클래스를 도입했다.
- 복잡한 상태 관리 없이, "목록 조회 → 선택 → 재개" 라는 단순한 흐름으로 구현했다.
#### 결과
- `ResumeFromPreviousUpload` E2E 테스트에서 검증되었다. 중단 후 `FindPreviousUpload()` + `ResumeFromPreviousUpload()`로 정상 재개된다.
- README 예제 코드에도 이 기능이 포함되어 있어 주요 사용 사례로 강조된다.
---
### 사례 4: JS ES 모듈의 Lazy 로딩 및 생명주기 관리
#### 문제 상황
`tusBlazorClient.js`는 Blazor의 JavaScript ES 모듈로 `import`를 통해 로드된다. 모듈을 사용하지 않을 때도 불필요하게 로드되거나, 싱글톤 `TusClient`의 생명주기에 맞춰 적절히 해제되어야 했다.
#### 원인 분석
Blazor에서 `IJSRuntime.InvokeAsync("import", ...)` 로 로드된 ES 모듈은 `IJSObjectReference`로 관리되며, `DisposeAsync()`로 해제할 수 있다. 로드가 비동기이므로 최초 호출 시점까지 지연시킬 수 있다.
#### 해결 방법
1. `TusJsInterop`에서 `_script`를 nullable로 선언하고, 모든 public/internal 메서드에서 `await InitializeAsync()`를 먼저 호출하도록 했다.
2. `InitializeAsync()`는 `_script != null`인 경우 early return 하여 **한 번만 모듈을 로드**한다.
3. `IAsyncDisposable`을 구현하여 `_script.DisposeAsync()`로 모듈을 정리한다.
4. `TusClient`가 Singleton이고, `TusClient.DisposeAsync()`에서 `_tusJsInterop.DisposeAsync()`를 호출한다.
#### 선택 이유
- Lazy 로딩은 라이브러리를 사용하지 않는 페이지에서는 JS 모듈 로드 비용이 발생하지 않도록 한다.
- null 체크 early return은 복잡한 `Lazy` 패턴 없이도 스레드 안전하다 (Blazor WASM은 싱글 스레드).
#### 결과
- `TusClient`를 DI로 주입만 받고 실제 업로드를 사용하지 않는 경우에도 불필요한 JS 모듈 로딩이 발생하지 않는다.
- `DisposeAsync()` 체인이 명확하게 구성되어 있다 (`TusClient` → `TusJsInterop` → JS module).
---
## 12. 최종 포트폴리오 문장 초안
### 프로젝트 개요
Blazor WebAssembly 환경에서 tus 프로토콜 기반의 재개 가능한(resumable) 대용량 파일 업로드를 지원하는 .NET 라이브러리. 오픈소스 JavaScript 라이브러리인 `tus-js-client`를 C# API로 래핑하여, Blazor 개발자가 JavaScript 코드 없이도 대용량 파일의 청크 업로드, 중단 후 재개, 병렬 업로드를 구현할 수 있도록 했다.
### 담당 역할
- **단독 개발** (코드 저장소 전체의 설계, 구현, 테스트, 문서화, NuGet 배포까지 단독 수행)
### 주요 기여
- Blazor ↔ `tus-js-client` 간의 완전한 콜백 브릿지 설계 및 구현 (7종 콜백 지원)
- `TusOptions` 기반 설정 모델로 tus-js-client의 모든 옵션(18개)을 C#으로 노출
- 업로드 중 동적 옵션 변경(`SetOtions`), 이전 업로드 탐지/재개(`FindPreviousUpload`/`ResumeFromPreviousUpload`) API 설계
- `IAsyncDisposable` 기반의 JS 리소스 생명주기 관리
- Blazor WASM Demo 앱 (13개 페이지) 및 Selenium E2E 테스트 (9개 테스트 케이스) 구현
### 사용 기술 및 선택 이유
| 기술 | 선택 이유 |
|------|----------|
| .NET 7 Blazor (Razor SDK) | 라이브러리가 `tusBlazorClient.js`를 번들로 포함해야 하므로 |
| `DotNetObjectReference` + `[JSInvokable]` | Blazor 프레임워크가 공식 제공하는 JS→.NET 호출 방식이므로 |
| `[JsonIgnore]` 콜백 분리 | C# 델리게이트는 직렬화 불가 → JS 전달 데이터와 콜백을 분리 |
| `ValueTask` 반환 | 불필요한 Task 할당을 방지하여 성능 최적화 |
| Singleton DI 등록 + Lazy JS 모듈 로딩 | 모듈 로딩 비용 최소화 및 리소스 재사용 |
| Selenium WebDriver (Firefox) | Blazor WASM은 브라우저 환경에서만 동작 → E2E 테스트로 검증 |
| tus-js-client (CDN) | 이미 검증된 오픈소스 라이브러리를 재사용하여 안정성 확보 |
### 구현 사항
- **Public API**: `TusClient` (진입점), `TusUpload` (업로드 작업), `TusOptions` (설정), `FileInputElement`/`JsFile`/`JsFileInfo` (파일 처리)
- **내부 구조**: `TusJsInterop` (JS 브릿지), `TusOptionJsInvoke` (콜백 수신), `TusOptionNullCheck` (null 콜백 최적화), `tusBlazorClient.js` (246라인, tus-js-client 어댑터)
- **DI**: `SetupExtension.AddTusBlazorClient()` 확장 메서드
- **리소스 관리**: 모든 주요 클래스가 `IAsyncDisposable` 구현, JS 모듈의 Lazy 로딩 및 생명주기 관리
- **오류 모델**: `TusError` + `TusHttpRequest`/`TusHttpResponse`로 네트워크 레벨 상세 정보 전달
- **테스트**: 9개의 Selenium E2E 테스트 (성공, 실패, 재개, 옵션 변경, 재시도 등)
- **데모 앱**: 13개 Razor 페이지로 모든 기능 시연
- **문서화**: README (설치/Quick Start), `TusOptions` XML 문서 주석, GitHub Wiki 링크
### 문제 해결 사례
위 11번 항목의 4가지 사례 참조:
1. Blazor ↔ JavaScript 콜백 브릿지 설계 (델리게이트 직렬화 불가 문제 해결)
2. 업로드 중 옵션 동적 변경 (옵션 변경 시 콜백 동기화 문제 해결)
3. 이전 업로드 탐지/재개 (JS 배열 인덱스 소실 문제를 래퍼 클래스로 해결)
4. JS 모듈 Lazy 로딩 + 생명주기 관리 (불필요한 모듈 로드 방지)
### 프로젝트 성과
- **NuGet 패키지**로 배포되어 (`TusBlazorClient` v1.0.1) 외부 프로젝트에서 `dotnet add package`로 즉시 설치 가능
- README 예제 코드 복사 → 붙여넣기 수준의 간결한 Quick Start 제공
- E2E 테스트로 주요 시나리오 검증 완료 (업로드, 재개, 오류 복구, 옵션 변경)
- tus-js-client의 모든 옵션(18개)과 콜백(7종)을 C#에 완전히 매핑
### 회고
- **잘한 점**: 싱글톤 + Lazy 로딩 구조는 라이브러리형 프로젝트에 적합한 선택이었다. `TusOptionNullCheck` 최적화처럼 작지만 실용적인 설계 선택이 실제 성능에 도움이 되었다. Blazor WASM 환경의 한계(느린 C# I/O)를 인지하고 검증된 JS 라이브러리를 래핑하는 전략도 실용적이었다.
- **아쉬운 점**: 단위 테스트가 없어 리팩토링 시 C# 로직의 회귀를 잡기 어렵다. 상속/인터페이스 기반 확장 포인트가 없어 외부 사용자가 내부 동작을 교체할 수 없다. CI/CD 파이프라인이 없어 테스트 자동화와 NuGet 자동 배포가 되어 있지 않다.
- **다음 개선 방향**: `CancellationToken` 지원 추가, IUploadStrategy 인터페이스 도입으로 확장성 확보, GitHub Actions CI/CD 구축, 단위 테스트 추가.