본문 바로가기
Tech Notes

파일 업로드를 쉽게 알아보자

by miracle-tech 2025. 10. 9.
728x90
반응형
파일 업로드 완벽 가이드: Frontend에서 Backend까지

📁 파일 업로드 완벽 가이드

Frontend에서 Backend까지 모든 것을 다루는 종합 가이드

📊 파일 업로드 프로세스 Overview

👤
1. 사용자 선택
파일 선택 또는 드래그 앤 드롭
2. 클라이언트 검증
크기, 형식, 미리보기
📤
3. 업로드
FormData / Base64 전송
🔒
4. 서버 검증
보안 검사, 파일 처리
💾
5. 저장
로컬/클라우드 저장
6. 응답
URL 및 메타데이터 반환

상세 데이터 플로우

Frontend (Browser)
multipart/form-data
HTTP POST Request
Backend Server
(Node.js, Python, etc.)
파일 처리
(검증, 리사이징, 최적화)
Storage Layer
(Local / S3 / CDN)

🎯 파일 업로드 기본 개념

1.1 HTTP와 파일 전송

파일 업로드는 HTTP의 multipart/form-data 인코딩 타입을 사용합니다. 이는 바이너리 데이터를 안전하게 전송하기 위해 설계된 표준 방식입니다.

<!-- 기본 HTML 파일 업로드 폼 -->
<form action="/upload" method="POST" enctype="multipart/form-data">
  <input type="file" name="file" accept="image/*" multiple />
  <button type="submit">업로드</button>
</form>
💡 중요: enctype="multipart/form-data"를 반드시 설정해야 파일이 올바르게 전송됩니다.

1.2 파일 업로드 방식 비교

방식 장점 단점 사용 사례
FormData 표준 방식, 대용량 지원 IE9 이하 미지원 일반적인 파일 업로드
Base64 텍스트로 전송, 간단함 33% 용량 증가, 메모리 사용 작은 이미지, 썸네일
Chunked Upload 대용량 파일, 재개 가능 구현 복잡 동영상, 대용량 파일
Direct Upload 서버 부하 감소 클라우드 종속 S3, CDN 직접 업로드

💻 Frontend 구현

2.1 모던 파일 업로드 UI

📱 드래그 앤 드롭
const dropZone = document.getElementById('drop-zone');

dropZone.addEventListener('dragover', (e) => {
  e.preventDefault();
  dropZone.classList.add('dragging');
});

dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  const files = Array.from(e.dataTransfer.files);
  handleFiles(files);
});
🖼️ 이미지 미리보기
function previewImage(file) {
  const reader = new FileReader();
  
  reader.onload = (e) => {
    const img = new Image();
    img.src = e.target.result;
    document.getElementById('preview')
      .appendChild(img);
  };
  
  reader.readAsDataURL(file);
}

2.2 파일 업로드 진행률

function uploadWithProgress(file) {
  const formData = new FormData();
  formData.append('file', file);
  
  const xhr = new XMLHttpRequest();
  
  // 진행률 이벤트 리스너
  xhr.upload.addEventListener('progress', (e) => {
    if (e.lengthComputable) {
      const percentComplete = (e.loaded / e.total) * 100;
      updateProgressBar(percentComplete);
    }
  });
  
  xhr.open('POST', '/api/upload');
  xhr.send(formData);
}

2.3 React 컴포넌트 예제

function FileUploader() {
  const [files, setFiles] = useState([]);
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);
  
  const handleUpload = async () => {
    setUploading(true);
    const formData = new FormData();
    
    files.forEach(file => {
      formData.append('files', file);
    });
    
    try {
      const response = await axios.post('/api/upload', formData, {
        onUploadProgress: (progressEvent) => {
          const percent = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          setProgress(percent);
        }
      });
      
      console.log('Upload successful:', response.data);
    } catch (error) {
      console.error('Upload failed:', error);
    } finally {
      setUploading(false);
      setProgress(0);
    }
  };
  
  return (
    <div className="uploader">
      <input 
        type="file" 
        multiple 
        onChange={(e) => setFiles(Array.from(e.target.files))}
      />
      
      {uploading && (
        <div className="progress-bar">
          <div style={{width: `${progress}%`}}>
            {progress}%
          </div>
        </div>
      )}
      
      <button onClick={handleUpload} disabled={uploading}>
        {uploading ? '업로드 중...' : '업로드'}
      </button>
    </div>
  );
}

🔧 Backend 구현

3.1 Node.js/Express with Multer

const express = require('express');
const multer = require('multer');
const path = require('path');

// Multer 설정
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/');
  },
  filename: (req, file, cb) => {
    const uniqueName = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, uniqueName + path.extname(file.originalname));
  }
});

const upload = multer({
  storage,
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB
  },
  fileFilter: (req, file, cb) => {
    const allowedTypes = /jpeg|jpg|png|gif|pdf/;
    const extname = allowedTypes.test(
      path.extname(file.originalname).toLowerCase()
    );
    const mimetype = allowedTypes.test(file.mimetype);
    
    if (extname && mimetype) {
      return cb(null, true);
    } else {
      cb(new Error('Invalid file type'));
    }
  }
});

// 단일 파일 업로드
app.post('/upload/single', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file uploaded' });
  }
  
  res.json({
    filename: req.file.filename,
    originalName: req.file.originalname,
    size: req.file.size,
    path: req.file.path
  });
});

// 다중 파일 업로드
app.post('/upload/multiple', upload.array('files', 10), (req, res) => {
  const files = req.files.map(file => ({
    filename: file.filename,
    originalName: file.originalname,
    size: file.size
  }));
  
  res.json({ files });
});

3.2 이미지 처리 및 최적화

const sharp = require('sharp');

async function processImage(inputPath, outputDir, filename) {
  const name = path.parse(filename).name;
  
  // 썸네일 생성 (200x200)
  await sharp(inputPath)
    .resize(200, 200, {
      fit: 'cover',
      position: 'center'
    })
    .jpeg({ quality: 80 })
    .toFile(`${outputDir}/${name}_thumb.jpg`);
  
  // 중간 사이즈 (800x800)
  await sharp(inputPath)
    .resize(800, 800, {
      fit: 'inside',
      withoutEnlargement: true
    })
    .jpeg({ quality: 85 })
    .toFile(`${outputDir}/${name}_medium.jpg`);
  
  // 원본 최적화
  await sharp(inputPath)
    .jpeg({ quality: 90, progressive: true })
    .toFile(`${outputDir}/${name}_optimized.jpg`);
}

// 업로드 후 이미지 처리
app.post('/upload/image', upload.single('image'), async (req, res) => {
  try {
    await processImage(
      req.file.path,
      'uploads/processed',
      req.file.filename
    );
    
    res.json({
      success: true,
      message: 'Image processed successfully'
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

📚 프레임워크별 파일 업로드 라이브러리

Frontend 라이브러리

⚛️ React
  • react-dropzone - 드래그 앤 드롭 업로드
  • react-filepond - 풍부한 기능의 업로더
  • antd Upload - Ant Design 컴포넌트
  • react-uploady - 모던 업로드 솔루션
  • uppy - 모듈식 파일 업로더
🟢 Vue.js
  • vue-filepond - FilePond Vue 버전
  • vue-dropzone - Dropzone.js 래퍼
  • vue-upload-component - Vue 전용
  • element-ui Upload - Element UI
  • vuetify v-file-input - Vuetify
🔺 Angular
  • ng2-file-upload - Angular 업로드
  • ngx-dropzone - 드롭존 컴포넌트
  • ngx-filepond - FilePond Angular
  • primeng FileUpload - PrimeNG
  • @angular/material - Material File

Backend 라이브러리

프레임워크 라이브러리 특징
Node.js/Express multer 가장 인기 있는 multipart 처리 미들웨어
formidable 폼 데이터 파싱 라이브러리
busboy 스트리밍 기반 파서
express-fileupload 간단한 Express 미들웨어
multiparty multipart/form-data 파싱
Python/Django Django FileField 내장 파일 처리
django-storages 클라우드 스토리지 지원
django-filer 고급 파일 관리
django-imagekit 이미지 처리
Python/FastAPI python-multipart FastAPI 기본 지원
aiofiles 비동기 파일 처리
pillow 이미지 처리
Ruby on Rails Active Storage Rails 5.2+ 내장
CarrierWave 유연한 업로드 솔루션
Shrine 모듈식 파일 툴킷

☁️ 저장소 전략

로컬 vs 클라우드 스토리지 비교

💾 로컬 스토리지

장점:

  • 빠른 접근 속도
  • 완전한 제어 가능
  • 추가 비용 없음

단점:

  • 확장성 제한
  • 백업 관리 필요
  • CDN 구성 복잡
☁️ 클라우드 스토리지

장점:

  • 무한 확장 가능
  • 자동 백업
  • CDN 통합
  • 고가용성

단점:

  • 비용 발생
  • 네트워크 의존
  • 벤더 종속

AWS S3 통합 예제

const AWS = require('aws-sdk');
const multerS3 = require('multer-s3');

// S3 설정
const s3 = new AWS.S3({
  accessKeyId: process.env.AWS_ACCESS_KEY,
  secretAccessKey: process.env.AWS_SECRET_KEY,
  region: 'ap-northeast-2'
});

// Multer-S3 설정
const uploadS3 = multer({
  storage: multerS3({
    s3: s3,
    bucket: 'my-bucket',
    acl: 'public-read',
    metadata: (req, file, cb) => {
      cb(null, {fieldName: file.fieldname});
    },
    key: (req, file, cb) => {
      const folder = 'uploads';
      const filename = Date.now().toString() + path.extname(file.originalname);
      cb(null, `${folder}/${filename}`);
    }
  }),
  limits: { fileSize: 10 * 1024 * 1024 } // 10MB
});

// S3 업로드 엔드포인트
app.post('/upload-s3', uploadS3.single('file'), (req, res) => {
  res.json({
    success: true,
    fileUrl: req.file.location,
    key: req.file.key
  });
});

🔒 보안 고려사항

⚠️ 주의: 파일 업로드는 주요 보안 취약점이 될 수 있습니다. 항상 서버 측 검증을 수행하세요.

필수 보안 체크리스트

  • 파일 타입 검증 - MIME 타입과 확장자 모두 확인
  • 파일 크기 제한 - DoS 공격 방지
  • 파일명 살균 - 특수문자 제거 및 길이 제한
  • 바이러스 스캔 - ClamAV 등 활용
  • 저장 경로 제한 - 디렉토리 탐색 방지
  • 실행 권한 제거 - 업로드된 파일 실행 방지

파일 타입 검증 강화

const fileType = require('file-type');
const fs = require('fs').promises;

async function verifyFileType(filePath, allowedTypes) {
  // 파일 내용 기반 타입 확인
  const buffer = await fs.readFile(filePath);
  const type = await fileType.fromBuffer(buffer);
  
  if (!type || !allowedTypes.includes(type.mime)) {
    // 잘못된 파일 즉시 삭제
    await fs.unlink(filePath);
    throw new Error(`Invalid file type: ${type ? type.mime : 'unknown'}`);
  }
  
  return type;
}

// 사용 예제
app.post('/secure-upload', upload.single('file'), async (req, res) => {
  try {
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    await verifyFileType(req.file.path, allowedTypes);
    
    res.json({ success: true });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

⚡ 성능 최적화

클라이언트 측 최적화

🗜️ 이미지 압축
function compressImage(file, maxWidth) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    
    reader.onload = (e) => {
      const img = new Image();
      img.src = e.target.result;
      
      img.onload = () => {
        const canvas = document.createElement('canvas');
        const ratio = maxWidth / img.width;
        canvas.width = maxWidth;
        canvas.height = img.height * ratio;
        
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        
        canvas.toBlob(resolve, 'image/jpeg', 0.8);
      };
    };
  });
}
🚀 병렬 업로드
async function uploadBatch(files, limit = 3) {
  const results = [];
  
  for (let i = 0; i < files.length; i += limit) {
    const batch = files.slice(i, i + limit);
    const promises = batch.map(file => uploadFile(file));
    const batchResults = await Promise.all(promises);
    results.push(...batchResults);
  }
  
  return results;
}

서버 측 최적화

비동기 처리 with Job Queue

const Queue = require('bull');
const imageQueue = new Queue('image processing');

// 업로드 엔드포인트 - 즉시 응답
app.post('/upload', upload.single('file'), async (req, res) => {
  // 즉시 응답 반환
  res.json({ 
    jobId: req.file.filename,
    status: 'queued'
  });
  
  // 백그라운드 처리 큐에 추가
  await imageQueue.add('process-image', {
    filepath: req.file.path,
    filename: req.file.filename
  });
});

// 백그라운드 워커
imageQueue.process('process-image', async (job) => {
  const { filepath, filename } = job.data;
  
  // 썸네일 생성
  await generateThumbnail(filepath);
  
  // 이미지 최적화
  await optimizeImage(filepath);
  
  // S3 업로드
  await uploadToS3(filepath);
  
  // 완료 알림
  notifyClient(filename, 'complete');
});

대용량 파일 처리 - Chunked Upload

// 클라이언트: 파일을 청크로 분할
function uploadInChunks(file, chunkSize = 5 * 1024 * 1024) {
  const chunks = Math.ceil(file.size / chunkSize);
  const uploadId = generateUniqueId();
  
  for (let i = 0; i < chunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    
    uploadChunk(chunk, i, chunks, uploadId);
  }
}

async function uploadChunk(chunk, index, total, uploadId) {
  const formData = new FormData();
  formData.append('chunk', chunk);
  formData.append('index', index);
  formData.append('total', total);
  formData.append('uploadId', uploadId);
  
  await fetch('/upload-chunk', {
    method: 'POST',
    body: formData
  });
}

✨ 베스트 프랙티스

💡 핵심 체크리스트:
  • 클라이언트와 서버 모두에서 파일 검증 수행
  • 적절한 파일 크기 제한 설정 (일반적으로 5-10MB)
  • 파일명 살균 및 유니크한 이름 생성
  • 이미지는 업로드 후 리사이징/최적화
  • 대용량 파일은 청크 업로드 또는 직접 업로드 사용
  • 민감한 파일은 접근 권한 검증 후 제공
  • CDN을 활용하여 정적 파일 서빙 최적화
  • 업로드 진행률과 에러 처리로 UX 개선
  • 정기적인 백업 및 불필요한 파일 정리
  • 로깅 및 모니터링으로 이상 징후 감지

🎯 마무리

파일 업로드는 웹 애플리케이션의 필수 기능이지만, 제대로 구현하려면 많은 고려사항이 있습니다. 이 가이드에서 다룬 내용을 참고하여 안전하고 효율적인 파일 업로드 시스템을 구축하시기 바랍니다.

🚀 Quick Start
  1. 프레임워크에 맞는 라이브러리 선택
  2. 기본 업로드 기능 구현
  3. 파일 검증 로직 추가
  4. 이미지 처리 및 최적화
  5. 저장소 전략 결정
  6. 보안 검증 강화
📚 추가 학습 자료
  • MDN - File API
  • OWASP File Upload Security
  • AWS S3 Best Practices
  • Image Optimization Guide
  • Streaming Large Files
  • WebRTC P2P File Transfer
💬 피드백: 이 가이드가 도움이 되셨다면, 실제 프로젝트에 적용해보시고 경험을 공유해주세요!
728x90