# 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 구축, 단위 테스트 추가.