파일 업로드
15.4.1. FormData 구성
파일 업로드는 FormData 객체를 사용하며, Content-Type은 multipart/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.total이undefined일 수 있으므로 반드시 존재 여부를 확인합니다.- 진행률은 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 타입 | 허용 목록 기반 | 확장자 위조 방지를 위한 이중 검증 |
- 허용 확장자 목록은 백엔드 표준과 동일하게 유지해야 합니다.
- 클라이언트 검증은 사용자 편의를 위한 사전 검증이며, 서버 측 검증을 대체하지 않습니다.