커피숍 WiFi로 2GB 영상을 업로드하는 것은 낙관주의를 시험하는 일입니다. 진행 표시줄이 70%를 향해 느릿느릿 움직이는 것을 지켜보고 있으면, 갑자기 연결이 끊깁니다. 업로드가 실패합니다. 다시 시도하면 서버는 완전히 새로운 파일을 기대합니다. 처음부터 다시 시작해야 합니다. 사용자 생성 영상을 처리하는 애플리케이션을 구축하는 사람에게 이것은 예외적인 상황이 아닙니다. 그것은 일상입니다. tus 프로토콜은 바로 이 문제를 해결하기 위해 만들어졌습니다. 이는 일반적인 HTTP 위에서 작동하며 업로드에 '기억력'을 부여하는 오픈 표준입니다. 네트워크가 복구되면 파일 전송은 중단된 지점에서 정확히 다시 시작됩니다.

Why Standard Uploads Break

표준 multipart 파일 업로드는 전체 페이로드를 단일 트랜잭션으로 취급합니다. 브라우저나 앱은 TCP 연결을 열고 바이트를 스트리밍하며 마지막에 200 OK를 기대합니다. 사용자가 LTE에서 WiFi로 전환하거나 노트북이 절전 모드로 들어가는 등의 이유로 연결이 재설정되면, 서버는 어떤 바이트가 안전하게 도착했는지 알 방법이 없습니다. 대부분의 프레임워크는 부분적인 데이터를 단순히 폐기합니다. 사용자는 실패한 폼과 나빠진 기분만 남게 됩니다. 비디오 파일은 크기가 크고, 불안정한 네트워크 환경의 모바일 기기에서 업로드되는 경우가 많으며, 파일 전체가 온전해지기 전까지는 처리할 수 없는 형식으로 인코딩되는 경우가 많기 때문에 이 고통은 더욱 가중됩니다.

How Tus Changes the Game

tus는 업로드를 일회성 전달이 아닌 상태를 유지하는(stateful) 대화로 재정의합니다. 파일을 한 번에 연속적인 바이트 뭉치로 보내는 대신, 클라이언트는 페이로드를 청크(chunk) 단위로 나눕니다. 더 중요한 점은 서버가 현재까지 수신한 정확한 바이트 수인 오프셋(offset)을 기억한다는 것입니다. 이러한 상태 유지(state persistence) 덕분에 재개가 가능해집니다. 이 프로토콜은 의도적으로 단순하게 설계되었습니다. WebSockets, gRPC 또는 독점적인 SDK를 요구하지 않습니다. 이미 알고 있는 HTTP 메서드를 사용합니다.

The Three Requests That Power Resumption

모든 tus 업로드는 예측 가능한 과정을 따릅니다.

먼저, 클라이언트는 알려진 tus 엔드포인트로 POST 요청을 보냅니다. 헤더에는 파일 정보가 담깁니다. Upload-Length 헤더는 최종 크기를 바이트 단위로 명시하며, 파일 이름이나 콘텐츠 유형과 같은 선택적 메타데이터는 Upload-Metadata 헤더에 base64로 인코딩된 키-값 쌍의 쉼표 구분 목록으로 전달됩니다. 서버는 빈 업로드 리소스를 생성하고 201 Created 상태와 함께 고유한 URL을 가리키는 Location 헤더를 응답합니다. 이 URL은 업로드의 남은 수명 동안 해당 업로드의 주소가 됩니다.

다음은 PATCH 요청입니다. 클라이언트는 해당 고유 URL로 원시 바이너리 데이터 청크를 보냅니다. 페이로드에는 두 개의 중요한 헤더가 동반됩니다. Content-Type은 반드시 application/offset+octet-stream이어야 하며, Upload-Offset은 이 청크가 시작되는 바이트 위치와 일치해야 합니다. 서버는 오프셋이 자체 기록과 일치하는지 확인합니다. 일치하지 않으면 서버는 상태 손상을 방지하기 위해 409 Conflict를 반환합니다. 모든 것이 일치하면 서버는 바이트를 추가하고 내부 오프셋을 업데이트한 뒤, 새로운 Upload-Offset과 함께 204 No Content 응답을 반환합니다. 청크 크기는 설정 가능하지만, 일반적으로 수백 킬로바이트에서 수 메가바이트 사이로 설정합니다. 청크가 작으면 HTTP 오버헤드가 늘어나지만 오류로부터 더 빠르게 복구할 수 있습니다. 청크가 크면 왕복 횟수(round trips)는 줄어들지만, 전송 중에 실패할 경우 더 많은 대역폭을 낭비하게 됩니다.

마지막으로 HEAD 요청입니다. 이것은 안전망 역할을 합니다. 만약 5MB 조각 중 3MB를 보낸 시점에서 청크 중간에 PATCH가 실패하여 연결이 끊겼다고 가정해 봅시다. 클라이언트가 네트워크 접속을 다시 확보하면, 업로드 URL로 HEAD 요청을 보냅니다. 서버는 현재의 Upload-Offset으로 응답합니다. 클라이언트는 비교합니다