클라이언트쪽 파일 업로드 구조

백엔드&인프라 추종자·2025년 11월 5일

아주 정확하게 이해하셨습니다, 정우님. 👌
지금 말씀하신 구조는 “서버가 인증·검증만 담당하고, 실제 업로드는 브라우저 → MinIO 직통” 방식입니다.
이건 대규모 파일 업로드 서비스나 SaaS API에서 거의 표준 패턴입니다.

정리해드리면, 정우님이 구상하신 구조는 다음과 같습니다 👇


🧭 전체 아키텍처 개요

[브라우저]  ⇄  [Go API 서버]  ⇄  [MinIO]
단계설명
1️⃣브라우저가 업로드를 요청 (POST /api/presign)
2️⃣Go API 서버가 용량·파일명 검증 후 MinIO SDK로 presigned URL 생성
3️⃣presigned URL과 form 필드를 브라우저에 반환
4️⃣브라우저가 해당 URL로 직접 업로드 (fetch or axios 등 사용)
5️⃣업로드 성공 시 브라우저에서 API 서버로 “업로드 완료 콜백” 전달

🧩 Go 서버 예시 (MinIO SDK 기반)

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "time"

    "github.com/minio/minio-go/v7"
    "github.com/minio/minio-go/v7/pkg/credentials"
)

var minioClient *minio.Client

func main() {
    var err error
    minioClient, err = minio.New("localhost:9000", &minio.Options{
        Creds:  credentials.NewStaticV4("minioadmin", "minioadmin", ""),
        Secure: false,
    })
    if err != nil {
        log.Fatal(err)
    }

    http.HandleFunc("/api/presign", handlePresign)
    log.Println("Server started at :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handlePresign(w http.ResponseWriter, r *http.Request) {
    ctx := context.Background()

    // 예: 클라이언트가 업로드할 파일명, 예상 크기를 전달
    filename := r.URL.Query().Get("filename")
    sizeStr := r.URL.Query().Get("size") // bytes 단위
    if filename == "" || sizeStr == "" {
        http.Error(w, "missing filename or size", http.StatusBadRequest)
        return
    }

    // 용량 제한 (예: 50MB)
    // 실제 파일 크기는 브라우저가 미리 File.size 로 전달하도록 함
    // 너무 크면 URL 생성 자체를 막음
    // (MinIO 정책으로도 content-length-range 를 중첩 제한 가능)
    // -----------------------------
    // NOTE: 여기선 브라우저측에서도 JS에서 File.size 체크하도록 병행 권장
    // -----------------------------
    const maxSize = 50 * 1024 * 1024
    // ... size parsing 생략 (단순화)

    // presigned POST 정책 생성
    policy := minio.NewPostPolicy()
    policy.SetBucket("uploads")
    policy.SetKey("user-uploads/" + filename)
    policy.SetExpires(time.Now().UTC().Add(5 * time.Minute))
    policy.SetContentLengthRange(0, maxSize)

    // presigned URL 및 form 필드 생성
    presignedURL, formData, err := minioClient.PresignedPostPolicy(ctx, policy)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // 브라우저로 전달
    json.NewEncoder(w).Encode(map[string]interface{}{
        "url":  presignedURL.String(),
        "form": formData,
    })
}

🧠 클라이언트 (HTML + JavaScript 예시)

<input type="file" id="fileInput" />
<button onclick="uploadFile()">Upload</button>

<script>
async function uploadFile() {
  const file = document.getElementById('fileInput').files[0];
  if (!file) return alert("파일을 선택하세요");

  // 1️⃣ presigned URL 요청
  const res = await fetch(`/api/presign?filename=${file.name}&size=${file.size}`);
  const { url, form } = await res.json();

  // 2️⃣ FormData 구성
  const formData = new FormData();
  for (const [k, v] of Object.entries(form)) formData.append(k, v);
  formData.append("file", file);

  // 3️⃣ MinIO 로 직접 업로드
  const uploadRes = await fetch(url, {
    method: "POST",
    body: formData,
  });

  if (uploadRes.ok) {
    alert("업로드 성공!");
  } else {
    alert("업로드 실패!");
  }
}
</script>

🔐 이 구조의 장점

항목설명
🚫 Go 서버는 파일 데이터를 전혀 받지 않음트래픽과 메모리 부하가 크게 줄어듭니다.
MinIO가 직접 처리업로드 속도 빠르고 서버 리소스 효율적입니다.
🧩 사전 검증 가능파일 크기, 확장자 등 제한을 presigned 발급 전에 확인 가능
🕒 짧은 유효시간예: 5분 → 이후 자동 만료
🧱 보안적 분리업로드 전용 권한만 노출되어, 다른 파일 접근 불가

✅ 요약

항목설명
presigned URL 유효시간보통 3~10분
파일 크기 제한서버 발급 시 SetContentLengthRange() + JS File.size 병행
서버 역할presigned URL 생성 + 검증 (파일 직접 수신 없음)
클라이언트 역할presigned URL로 직접 MinIO에 업로드
1회용 여부필요시 DB로 관리, 기본은 여러 번 업로드 가능

이 방식은 CloudFront + S3 기반 SaaS 업로드 구조와 거의 동일하며,
정우님이 직접 구축하는 MinIO 환경에서도 완벽히 같은 아키텍처로 작동합니다.


원하신다면 다음 단계로,
👉 위 구조를 Gin 프레임워크 기반으로 완성한 실행 가능한 예제 프로젝트 구조 (디렉토리 + 코드 전체)로 정리해드릴까요?

profile
AI 답변 글을 주로 올립니다.

0개의 댓글