33 KiB
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가 비동기로 설계되어 있다.
사용 예시 코드
@inject TusClient TusClient
<input type="file" @ref="_fileElement" />
<button onclick="@Upload">upload</button>
@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<string, string> { { "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<long, long>? |
null |
진행률 콜백 (bytesSent, bytesTotal) |
OnChunkComplete |
Action<long, long, long>? |
null |
청크 완료 콜백 |
OnSuccess |
Action? |
null |
업로드 성공 콜백 |
OnError |
Action<TusError>? |
null |
오류 발생 콜백 |
OnShouldRetry |
Func<TusError, long, bool>? |
null |
재시도 결정 콜백 |
Headers |
Dictionary<string, string> |
new() |
커스텀 HTTP 헤더 |
Metadata |
Dictionary<string, string> |
new() |
업로드 생성 시 메타데이터 |
UploadUrl |
Uri? |
null |
직접 재개용 URL |
RetryDelays |
List<int> |
[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 메서드, URLOriginalHttpResponse(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는 다음과 같은 구조로 잘 정리되어 있다:
- 프로젝트 소개 (tus 프로토콜 설명)
- 사용 동기 ("Why do I use this?")
- 설치 방법 (CDN + NuGet + DI 등록)
- 완전한 예제 코드
- 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 | 파일 메타데이터 |
최소 사용 코드
@inject TusClient TusClient
<input type="file" @ref="_el" />
<button onclick="@Upload">upload</button>
@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 문서 주석(/// <summary>)이 작성되어 있으며, TusUpload.GetUrl() 등 주요 메서드에도 주석이 있다. 단, TusClient, TusJsInterop, FileInputElement에는 주석이 거의 없다.
처음 사용자가 헷갈릴 수 있는 부분
TusOptions의 콜백은[JsonIgnore]이므로,GetOptions()으로 받은 옵션 객체를 그대로Upload()에 재사용하면 콜백이 유실된다.GetOptions()내부에서 수동으로 콜백을 merge하는 이유가 여기에 있다.tus-js-clientCDN 로드를 잊으면 라이브러리가 동작하지 않는다.Upload()을 호출할 때마다 새DotNetObjectReference<TusOptionJsInvoke>가 생성되므로, 이전 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에
<Version>1.0.1</Version>로 직접 명시. - 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이 있고, 정상적으로 설치하여 사용할 수 있는 상태다. 단, 아래 선행 조건이 필요하다:
- 대상 프로젝트가 Blazor WebAssembly여야 함
index.html에tus-js-clientCDN 스크립트 추가 필요- 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를 통해 이루어지는데, 콜백이 설정되지 않은 경우에도 호출이 발생하면 불필요한 오버헤드가 발생한다.
해결 방법
- TusOptionJsInvoke 클래스를 도입하여 모든
[JSInvokable]메서드를 한 객체에 모았다. 이 객체를DotNetObjectReference로 감싸 JavaScript에 전달했다. - 콜백 프로퍼티는
[JsonIgnore]로 직렬화에서 제외하고, JavaScript 옵션 구성용 실제 값만 전달했다. - TusOptionNullCheck를 별도로 생성하여 각 콜백의 null 여부를 JavaScript 측에 알려주고, JS 측에서
if (optNullCheck.isNullOnError) return;과 같이 early return 하도록 했다. 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 객체의 개별 프로퍼티에 새 값을 할당해야 했다.
해결 방법
TusUpload.SetOtions(Action<TusOptions> setOption)API를 설계했다.- 내부적으로
GetOptions()→ callback merge →setOption호출 → 새DotNetObjectReference생성 →SetTusUploadOptionJS 호출의 파이프라인을 구현했다. - 기존
_optionJsInvokeReference를 dispose하고 새로 생성하여 콜백 참조를 갱신했다. - JS 측에서는
upload.options.endpoint = opt.endpoint패턴으로 개별 프로퍼티를 in-place 업데이트했다.
선택 이유
- 액션 기반 API (
Action<TusOptions>)는 사용자가 변경하고 싶은 옵션만 선택적으로 수정할 수 있도록 해준다. TusUpload인스턴스를 폐기하고 새로 생성하는 것보다 효율적이다.
결과
SetOptionE2E 테스트로 검증되어 있다. 업로드 50% 지점에서 ChunkSize를 50000에서 15000으로 변경한 후 정상 재개되었다.
사례 3: 이전 업로드 탐지 및 재개 (FindPreviousUpload / ResumeFromPreviousUpload)
문제 상황
브라우저를 종료했다 다시 열거나, 페이지를 실수로 새로고침한 경우, 진행 중이던 업로드를 이어서 진행할 수 있도록 하는 기능이 필요했다. tus-js-client는 findPreviousUploads() API를 제공하지만, 이 기능을 C#으로 노출해야 했다.
원인 분석
findPreviousUploads()는 브라우저 URL 저장소에 저장된 이전 업로드 목록을 JavaScript 객체 배열로 반환한다. 이 정보를 C# DTO로 변환해야 하고, 사용자가 특정 업로드를 선택해 재개할 수 있도록 해야 했다.
해결 방법
findPreviousUploads()호출 결과를List<TusPreviousUpload>로 역직렬화하는InvokeAsync호출을 구현했다.- 단순히 DTO만 반환하지 않고,
TusPreviousUploadRef래퍼를 도입해Index를 함께 보관하도록 했다. 이 Index가resumeFromPreviousUpload(pres[index])호출에 필요하기 때문이다. TusUpload.FindPreviousUpload()와TusUpload.ResumeFromPreviousUpload(TusPreviousUploadRef)API를 제공하여 사용자가 목록을 조회하고 특정 항목을 골라 재개할 수 있게 했다.
선택 이유
- JavaScript 배열 인덱스는 JSON 역직렬화 과정에서 소실되므로, C# 측에서 명시적으로 Index를 추적하는 래퍼 클래스를 도입했다.
- 복잡한 상태 관리 없이, "목록 조회 → 선택 → 재개" 라는 단순한 흐름으로 구현했다.
결과
ResumeFromPreviousUploadE2E 테스트에서 검증되었다. 중단 후FindPreviousUpload()+ResumeFromPreviousUpload()로 정상 재개된다.- README 예제 코드에도 이 기능이 포함되어 있어 주요 사용 사례로 강조된다.
사례 4: JS ES 모듈의 Lazy 로딩 및 생명주기 관리
문제 상황
tusBlazorClient.js는 Blazor의 JavaScript ES 모듈로 import를 통해 로드된다. 모듈을 사용하지 않을 때도 불필요하게 로드되거나, 싱글톤 TusClient의 생명주기에 맞춰 적절히 해제되어야 했다.
원인 분석
Blazor에서 IJSRuntime.InvokeAsync<IJSObjectReference>("import", ...) 로 로드된 ES 모듈은 IJSObjectReference로 관리되며, DisposeAsync()로 해제할 수 있다. 로드가 비동기이므로 최초 호출 시점까지 지연시킬 수 있다.
해결 방법
TusJsInterop에서_script를 nullable로 선언하고, 모든 public/internal 메서드에서await InitializeAsync()를 먼저 호출하도록 했다.InitializeAsync()는_script != null인 경우 early return 하여 한 번만 모듈을 로드한다.IAsyncDisposable을 구현하여_script.DisposeAsync()로 모듈을 정리한다.TusClient가 Singleton이고,TusClient.DisposeAsync()에서_tusJsInterop.DisposeAsync()를 호출한다.
선택 이유
- Lazy 로딩은 라이브러리를 사용하지 않는 페이지에서는 JS 모듈 로드 비용이 발생하지 않도록 한다.
- null 체크 early return은 복잡한
Lazy<T>패턴 없이도 스레드 안전하다 (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),
TusOptionsXML 문서 주석, GitHub Wiki 링크
문제 해결 사례
위 11번 항목의 4가지 사례 참조:
- Blazor ↔ JavaScript 콜백 브릿지 설계 (델리게이트 직렬화 불가 문제 해결)
- 업로드 중 옵션 동적 변경 (옵션 변경 시 콜백 동기화 문제 해결)
- 이전 업로드 탐지/재개 (JS 배열 인덱스 소실 문제를 래퍼 클래스로 해결)
- JS 모듈 Lazy 로딩 + 생명주기 관리 (불필요한 모듈 로드 방지)
프로젝트 성과
- NuGet 패키지로 배포되어 (
TusBlazorClientv1.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 구축, 단위 테스트 추가.