107 lines
6.2 KiB
Markdown
107 lines
6.2 KiB
Markdown
# TusBlazorClient
|
|
|
|
## 1. 프로젝트 개요
|
|
|
|
Blazor WebAssembly 환경에서 대용량 파일 업로드를 안정적으로 처리하기 위한 **tus 프로토콜 기반 C# 래퍼 라이브러리**입니다.
|
|
|
|
Blazor WASM에서 순수 C# 코드로 대용량 파일을 전송할 경우, 브라우저의 메모리 제약과 느린 I/O 속도로 인해 전송 실패 또는 브라우저 멈춤 현상이 발생하고, 네트워크 중단 시 처음부터 다시 업로드해야 하는 문제가 있었습니다. 저는 이를 해결하기 위해 JavaScript의 `tus-js-client`를 C# API로 감싸, Blazor 개발자가 JavaScript를 직접 다루지 않고도 재개 가능한 대용량 파일 업로드를 사용할 수 있도록 설계하고 구현했습니다.
|
|
|
|
---
|
|
|
|
## 2. 담당 역할
|
|
|
|
- 라이브러리 전체 설계 및 구현 (1인 개발)
|
|
- Public API 설계, JS Interop 브릿지 구현, DI 통합 구성
|
|
|
|
---
|
|
|
|
## 3. 주요 기여
|
|
|
|
### 3.1 C# 네이티브 API로 tus 프로토콜 추상화
|
|
|
|
JavaScript `tus-js-client`를 직접 사용하려면 Blazor에서 `IJSRuntime`을 통한 JS Interop 코드를 반복적으로 작성해야 했습니다. 저는 이 복잡성을 감추고 C# 개발자에게 익숙한 타입 세이프 API를 제공하기 위해 `TusClient` → `TusUpload` → `TusOptions` 구조로 계층을 나눠 설계했습니다.
|
|
|
|
사용자는 아래와 같이 DI 등록 한 줄과 직관적인 C# 코드만으로 업로드를 처리할 수 있습니다.
|
|
|
|
```csharp
|
|
// Program.cs
|
|
builder.Services.AddTusBlazorClient();
|
|
|
|
// Component
|
|
var file = (await TusClient.GetFileInputElement(_fileElement).GetFiles()).First();
|
|
var upload = await TusClient.Upload(file, options);
|
|
await upload.Start();
|
|
```
|
|
|
|
### 3.2 JS → .NET 콜백 브릿지 설계
|
|
|
|
`OnProgress`, `OnError`, `OnSuccess` 등 tus의 이벤트 콜백은 JavaScript에서 발생하지만, 사용자는 이를 C# 델리게이트로 받아야 합니다. 저는 `TusOptionJsInvoke` 클래스에 `[JSInvokable]` 메서드를 정의하고 `DotNetObjectReference`로 JS에 전달하여, JS 이벤트가 발생할 때 .NET 델리게이트가 정확히 호출되도록 중계 구조를 구현했습니다.
|
|
|
|
또한 `TusOptionNullCheck`를 도입하여 사용자가 등록하지 않은 콜백에 대해 JS 측에서 불필요한 interop 호출이 발생하지 않도록 최적화했습니다.
|
|
|
|
### 3.3 TusUpload 생성자를 internal로 제한하여 안전한 인스턴스 생성 강제
|
|
|
|
`TusUpload`가 외부에서 직접 생성될 경우 `DotNetObjectReference` 연결이 누락되어 콜백이 동작하지 않는 문제가 발생할 수 있었습니다. 저는 `TusUpload`의 생성자를 **internal**로 제한하고, 반드시 `TusClient.Upload()`를 통해서만 인스턴스를 얻도록 강제하여 JS 콜백 브릿지가 항상 올바르게 연결되는 것을 보장했습니다.
|
|
|
|
### 3.4 JS 모듈 Lazy 초기화로 불필요한 로드 방지
|
|
|
|
`TusJsInterop`에서 JS ES 모듈을 Lazy 초기화 방식으로 관리하여, 실제로 업로드가 필요한 시점에만 JS 모듈을 로드하도록 처리했습니다. `TusClient`는 Singleton으로 등록되어 모듈을 한 번만 로드하고 이후 모든 업로드가 공유합니다.
|
|
|
|
---
|
|
|
|
## 4. 사용 기술 및 선택 이유
|
|
|
|
| 기술 | 선택 이유 |
|
|
| ----------------------- | ----------------------------------------------------------------- |
|
|
| tus-js-client | tus 프로토콜의 검증된 JS 구현체로, 재개 가능한 업로드 로직을 직접 구현하지 않고 안정적으로 활용하기 위해 선택 |
|
|
| IJSRuntime / JS Interop | Blazor WASM에서 JS 라이브러리를 C#으로 연결하는 공식 메커니즘 |
|
|
|
|
|
|
|
|
---
|
|
|
|
## 5. 구현 사항
|
|
|
|
### 폴더 구조
|
|
|
|
```
|
|
TusBlazorClient/
|
|
├── TusClient.cs # Public API 진입점, 업로드 생성 팩토리
|
|
├── TusUpload.cs # 단일 업로드 생명주기 관리
|
|
├── TusOptions.cs # 설정 모델 (18개 프로퍼티)
|
|
├── TusJsInterop.cs # .NET ↔ JS 브릿지 계층 (Lazy 초기화)
|
|
├── TusOptionJsInvoke.cs # JS → .NET 콜백 수신기 ([JSInvokable])
|
|
├── TusOptionNullCheck.cs # 불필요한 콜백 호출 방지 정보
|
|
├── FileInputElement.cs # 파일 입력 요소 래퍼
|
|
├── TusError.cs # 오류 모델
|
|
├── TusHttpRequest.cs # HTTP 요청 DTO
|
|
├── TusHttpResponse.cs # HTTP 응답 DTO
|
|
└── TusPreviousUpload.cs # 이전 업로드 DTO
|
|
```
|
|
|
|
### 업로드 기본 흐름
|
|
|
|
```
|
|
1. DI 등록: builder.Services.AddTusBlazorClient()
|
|
2. TusClient 주입 후 FileInputElement로 파일 선택
|
|
3. TusOptions 구성 (Endpoint, 콜백 등)
|
|
4. TusClient.Upload(file, options) 으로 TusUpload 생성
|
|
5. (선택) FindPreviousUpload() + ResumeFromPreviousUpload() 로 이어 올리기
|
|
6. TusUpload.Start() 호출
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 기술적 의사결정 및 회고
|
|
|
|
### 인터페이스 미분리에 대한 판단
|
|
|
|
`TusClient`와 `TusJsInterop`에 별도 인터페이스를 추출하지 않았습니다. 라이브러리 규모가 작고, 테스트를 Mocking 기반 단위 테스트 대신 Selenium E2E 테스트로 커버하고 있어 인터페이스 분리의 실질적인 이득이 크지 않다고 판단했습니다. 다만 외부 사용자가 테스트 대역을 구성해야 하는 상황이 생긴다면 인터페이스 추출이 필요한 지점이 될 수 있습니다.
|
|
|
|
### Fluent API가 아닌 명령형 API 채택
|
|
|
|
업로드 설정과 실행을 메서드 체이닝으로 연결하는 Fluent API 대신, 옵션 객체를 구성하고 업로드 인스턴스에 명령을 내리는 **명령형 API**를 채택했습니다. 이는 `FindPreviousUpload()`, `ResumeFromPreviousUpload()`, `Abort()` 등 업로드 생명주기의 중간 단계에 개입해야 하는 시나리오에서 Fluent API보다 직관적으로 코드를 작성할 수 있다고 판단했기 때문입니다.
|
|
|
|
### 콜백의 JSON 직렬화 제외
|
|
|
|
`TusOptions`의 콜백 프로퍼티는 `[JsonIgnore]`로 마킹하여 JSON 직렬화 대상에서 제외했습니다. .NET 델리게이트는 JS로 직접 전달할 수 없으므로, 직렬화 시 오류가 발생하거나 의미 없는 값이 전달되는 것을 방지하기 위한 명시적 설계입니다. |