Skip to content

파일 업로드

15.4.1. FormData 구성

파일 업로드는 FormData 객체를 사용하며, Content-Typemultipart/form-data로 전송합니다.

typescript
async function uploadFile(file: File, metadata: { category: string }) {
  const formData = new FormData()
  formData.append('file', file)
  formData.append('category', metadata.category)

  const response = await axios.post('/api/files', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
  })

  return response.data
}
  • 파일과 메타데이터를 하나의 FormData에 포함하여 단일 요청으로 전송합니다.
  • Content-Type 헤더는 명시적으로 multipart/form-data를 지정합니다.
  • 다중 파일 업로드 시 동일한 키로 append를 반복 호출합니다.

15.4.2. 진행률 표시

대용량 파일 업로드 시 Axios의 onUploadProgress 콜백을 활용하여 진행률을 표시합니다.

vue
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'

const progress = ref(0)
const isUploading = ref(false)

async function upload(file: File) {
  isUploading.value = true
  progress.value = 0

  const formData = new FormData()
  formData.append('file', file)

  try {
    await axios.post('/api/files', formData, {
      headers: { 'Content-Type': 'multipart/form-data' },
      onUploadProgress(event) {
        if (event.total) {
          progress.value = Math.round((event.loaded / event.total) * 100)
        }
      },
    })
  } finally {
    isUploading.value = false
  }
}
</script>

<template>
  <div v-if="isUploading" class="progress-bar">
    <div class="progress-fill" :style="{ width: `${progress}%` }" />
    <span>{{ progress }}%</span>
  </div>
</template>
  • event.totalundefined일 수 있으므로 반드시 존재 여부를 확인합니다.
  • 진행률은 0~100 사이의 정수로 변환하여 표시합니다.

15.4.3. 드래그 앤 드롭

파일 드래그 앤 드롭은 @dragover.prevent@drop.prevent 이벤트로 처리합니다.

vue
<script setup lang="ts">
import { ref } from 'vue'

const isDragOver = ref(false)
const selectedFile = ref<File | null>(null)

function handleDrop(event: DragEvent) {
  isDragOver.value = false
  const files = event.dataTransfer?.files
  if (files && files.length > 0) {
    selectedFile.value = files[0]
  }
}

function handleFileSelect(event: Event) {
  const target = event.target as HTMLInputElement
  if (target.files && target.files.length > 0) {
    selectedFile.value = target.files[0]
  }
}
</script>

<template>
  <div
    class="drop-zone"
    :class="{ 'drag-over': isDragOver }"
    @dragover.prevent="isDragOver = true"
    @dragleave="isDragOver = false"
    @drop.prevent="handleDrop"
  >
    <p>파일을 드래그하거나 클릭하여 선택합니다.</p>
    <input type="file" @change="handleFileSelect" />
  </div>
  <p v-if="selectedFile">선택된 파일: {{ selectedFile.name }}</p>
</template>
  • @dragover.prevent는 브라우저 기본 동작(파일 열기)을 차단하기 위해 필수로 적용합니다.
  • 드래그 상태(isDragOver)를 추적하여 시각적 피드백을 제공합니다.
  • <input type="file">을 병행 배치하여 클릭 방식도 지원합니다.

15.4.4. 클라이언트 검증

파일 업로드 전 클라이언트에서 확장자, 크기, MIME 타입을 검증합니다.

typescript
// src/utils/fileValidation.ts
interface FileValidationResult {
  valid: boolean
  error: string | null
}

const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx']
const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB
const ALLOWED_MIME_TYPES = [
  'image/jpeg',
  'image/png',
  'image/gif',
  'application/pdf',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
]

export function validateFile(file: File): FileValidationResult {
  const extension = file.name.split('.').pop()?.toLowerCase() ?? ''
  if (!ALLOWED_EXTENSIONS.includes(extension)) {
    return { valid: false, error: `허용되지 않는 파일 형식입니다. (${ALLOWED_EXTENSIONS.join(', ')})` }
  }

  if (file.size > MAX_FILE_SIZE) {
    return { valid: false, error: '파일 크기는 최대 50MB까지 허용됩니다.' }
  }

  if (!ALLOWED_MIME_TYPES.includes(file.type)) {
    return { valid: false, error: 'MIME 타입이 허용 목록에 포함되지 않습니다.' }
  }

  return { valid: true, error: null }
}
검증 항목기준비고
확장자허용 목록 기반백엔드 허용 목록과 동기화
파일 크기최대 50MB프로젝트 요구사항에 따라 조정 가능
MIME 타입허용 목록 기반확장자 위조 방지를 위한 이중 검증
  • 허용 확장자 목록은 백엔드 표준과 동일하게 유지해야 합니다.
  • 클라이언트 검증은 사용자 편의를 위한 사전 검증이며, 서버 측 검증을 대체하지 않습니다.

TIENIPIA QUALIFIED STANDARD