TIL - 20260606

juni·2026년 6월 6일

TIL

목록 보기
371/468

0606 풀스택 실무 기초 (9/N): 파일 업로드와 정적 파일 관리


✅ 1. 파일 업로드란 무엇인가?

  • 파일 업로드(File Upload)란 사용자가 이미지, 문서, 영상, 첨부파일 등을 서버로 전송하고, 서버가 해당 파일을 저장하거나 외부 스토리지에 업로드하는 기능입니다.
  • 웹서비스에서는 프로필 이미지, 상품 이미지, 이벤트 배너, 첨부파일, 엑셀 파일 업로드 등 다양한 곳에서 파일 업로드가 사용됩니다.
  • 풀스택 개발자는 프론트엔드에서 파일을 선택하고, 백엔드에서 파일을 검증하고, 저장소에 안전하게 보관한 뒤, 다시 사용자에게 보여주는 전체 흐름을 이해해야 합니다.

➕ 1-1. 실무에서 파일 업로드가 중요한 이유

  • 상품/이벤트 운영에 필수

    • 쇼핑몰이나 휴대폰 판매 사이트에서는 상품 이미지, 배너 이미지, 사전예약 이벤트 이미지가 자주 바뀝니다.
  • 관리자 페이지와 연결

    • 운영자가 관리자 페이지에서 직접 이미지를 올리고 수정할 수 있어야 합니다.
  • 보안 위험이 큼

    • 사용자가 올리는 파일은 신뢰할 수 없습니다.
    • 악성 스크립트, 실행 파일, 과도하게 큰 파일, 위장 확장자 등을 막아야 합니다.
  • 서버 비용과 성능에 영향

    • 이미지나 파일이 많아지면 서버 용량, 트래픽, CDN 비용에 영향을 줍니다.
    • 그래서 저장 위치와 캐싱 전략이 중요합니다.

✅ 2. 파일 업로드 기본 흐름

  • 일반적인 파일 업로드 흐름은 다음과 같습니다.
사용자
  ↓
프론트엔드에서 파일 선택
  ↓
FormData로 파일 전송
  ↓
백엔드에서 파일 수신
  ↓
파일 검증
  ↓
로컬 서버 또는 S3에 저장
  ↓
DB에 파일 URL/메타데이터 저장
  ↓
프론트엔드에서 업로드 결과 표시

➕ 2-1. 예시 흐름

1. 관리자가 이벤트 배너 이미지 선택
2. React에서 FormData 생성
3. POST /api/admin/banners/upload 요청
4. NestJS에서 파일 크기와 확장자 검증
5. AWS S3에 이미지 업로드
6. DB에 이미지 URL 저장
7. 관리자 페이지에서 업로드된 배너 미리보기 표시

✅ 3. multipart/form-data

  • 파일 업로드에는 일반 JSON 요청이 아니라 보통 multipart/form-data 형식을 사용합니다.
  • JSON은 텍스트 데이터 전달에 적합하지만, 파일 같은 바이너리 데이터를 보내기에는 적합하지 않습니다.

➕ 3-1. 일반 JSON 요청

POST /api/consults
Content-Type: application/json

{
  "name": "홍길동",
  "phone": "01012345678"
}

➕ 3-2. 파일 업로드 요청

POST /api/upload
Content-Type: multipart/form-data

file: image.png
type: banner
  • 파일과 텍스트 필드를 함께 보낼 수 있습니다.
  • 프론트엔드에서는 FormData 객체를 사용합니다.

✅ 4. 프론트엔드 파일 업로드

➕ 4-1. input file

  • HTML에서는 input type="file"을 사용해 파일을 선택할 수 있습니다.
function FileInput() {
  return <input type="file" />;
}
  • 이미지 파일만 선택하게 제한할 수도 있습니다.
<input type="file" accept="image/*" />
  • 여러 개의 파일을 선택하려면 multiple 속성을 사용합니다.
<input type="file" multiple />

➕ 4-2. React에서 파일 선택 처리

import { useState } from "react";

function BannerUpload() {
  const [file, setFile] = useState<File | null>(null);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFile = event.target.files?.[0];

    if (!selectedFile) return;

    setFile(selectedFile);
  };

  return (
    <input
      type="file"
      accept="image/*"
      onChange={handleChange}
    />
  );
}
  • event.target.files는 사용자가 선택한 파일 목록입니다.
  • 단일 업로드에서는 보통 첫 번째 파일만 사용합니다.

✅ 5. FormData로 파일 전송하기

  • 파일은 FormData에 담아서 서버로 전송합니다.

➕ 5-1. fetch 업로드 예시

async function uploadBanner(file: File) {
  const formData = new FormData();

  formData.append("file", file);
  formData.append("type", "banner");

  const response = await fetch("/api/admin/uploads/banner", {
    method: "POST",
    body: formData,
  });

  if (!response.ok) {
    throw new Error("파일 업로드에 실패했습니다.");
  }

  return response.json();
}
  • FormData를 사용할 때는 보통 Content-Type을 직접 지정하지 않습니다.
  • 브라우저가 boundary 값을 포함한 multipart/form-data 헤더를 자동으로 설정합니다.
// 피하는 것이 좋은 방식
headers: {
  "Content-Type": "multipart/form-data"
}
  • 직접 지정하면 boundary가 빠져서 서버가 파일을 제대로 파싱하지 못할 수 있습니다.

➕ 5-2. axios 업로드 예시

import axios from "axios";

async function uploadBanner(file: File) {
  const formData = new FormData();

  formData.append("file", file);

  const response = await axios.post(
    "/api/admin/uploads/banner",
    formData
  );

  return response.data;
}

✅ 6. 프론트엔드 파일 검증

  • 파일 검증은 백엔드에서 반드시 해야 하지만, 프론트엔드에서도 1차 검증을 하면 사용자 경험이 좋아집니다.

➕ 6-1. 파일 크기 검증

const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB

if (file.size > MAX_FILE_SIZE) {
  alert("5MB 이하의 파일만 업로드할 수 있습니다.");
  return;
}

➕ 6-2. 파일 타입 검증

const allowedTypes = ["image/jpeg", "image/png", "image/webp"];

if (!allowedTypes.includes(file.type)) {
  alert("JPG, PNG, WebP 파일만 업로드할 수 있습니다.");
  return;
}
  • 프론트엔드 검증은 사용자가 조작할 수 있으므로 보안 수단으로 믿으면 안 됩니다.
  • 실제 보안 검증은 반드시 백엔드에서 다시 해야 합니다.

✅ 7. 이미지 미리보기

  • 업로드 전 이미지 미리보기를 제공하면 운영자가 파일을 잘못 선택하는 실수를 줄일 수 있습니다.
import { useState } from "react";

function ImagePreviewUpload() {
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFile = event.target.files?.[0];

    if (!selectedFile) return;

    const objectUrl = URL.createObjectURL(selectedFile);
    setPreviewUrl(objectUrl);
  };

  return (
    <div>
      <input type="file" accept="image/*" onChange={handleChange} />

      {previewUrl && (
        <img
          src={previewUrl}
          alt="업로드 미리보기"
          style={{ width: 300 }}
        />
      )}
    </div>
  );
}
  • URL.createObjectURL로 로컬 파일의 임시 미리보기 URL을 만들 수 있습니다.
  • 실제 서비스에서는 컴포넌트가 사라질 때 URL.revokeObjectURL로 메모리를 정리하는 것이 좋습니다.

✅ 8. NestJS에서 파일 업로드 처리

  • NestJS에서는 FileInterceptor와 Multer를 사용해 파일 업로드를 처리할 수 있습니다.

➕ 8-1. 단일 파일 업로드 예시

import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('admin/uploads')
export class UploadController {
  @Post('banner')
  @UseInterceptors(FileInterceptor('file'))
  uploadBanner(@UploadedFile() file: Express.Multer.File) {
    return {
      success: true,
      message: '파일 업로드에 성공했습니다.',
      data: {
        originalName: file.originalname,
        mimeType: file.mimetype,
        size: file.size,
      },
    };
  }
}
  • FileInterceptor('file')의 'file'은 프론트엔드 formData.append("file", file)의 필드명과 일치해야 합니다.

➕ 8-2. 여러 파일 업로드 예시

import {
  Controller,
  Post,
  UploadedFiles,
  UseInterceptors,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';

@Controller('admin/uploads')
export class UploadController {
  @Post('images')
  @UseInterceptors(FilesInterceptor('files', 5))
  uploadImages(@UploadedFiles() files: Express.Multer.File[]) {
    return {
      success: true,
      message: '여러 파일 업로드에 성공했습니다.',
      data: files.map((file) => ({
        originalName: file.originalname,
        mimeType: file.mimetype,
        size: file.size,
      })),
    };
  }
}
  • FilesInterceptor('files', 5)는 최대 5개의 파일을 받을 수 있다는 의미입니다.

✅ 9. 백엔드 파일 검증

  • 파일 검증은 반드시 백엔드에서 해야 합니다.
  • 사용자는 프론트엔드 검증을 우회해서 직접 API를 호출할 수 있기 때문입니다.

➕ 9-1. 검증해야 할 항목

  1. 파일 존재 여부
  2. 파일 크기
  3. MIME 타입
  4. 확장자
  5. 파일명
  6. 업로드 권한
  7. 저장 경로
  8. 이미지 해상도
  9. 악성 파일 여부

➕ 9-2. NestJS 파일 크기 제한 예시

import { BadRequestException } from '@nestjs/common';

const MAX_FILE_SIZE = 5 * 1024 * 1024;

if (file.size > MAX_FILE_SIZE) {
  throw new BadRequestException('5MB 이하의 파일만 업로드할 수 있습니다.');
}

➕ 9-3. MIME 타입 검증 예시

const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];

if (!allowedMimeTypes.includes(file.mimetype)) {
  throw new BadRequestException('JPG, PNG, WebP 파일만 업로드할 수 있습니다.');
}
  • MIME 타입만 완전히 믿는 것도 위험합니다.
  • 중요한 서비스에서는 실제 파일 시그니처 검사나 이미지 처리 라이브러리를 통한 검증도 고려해야 합니다.

✅ 10. 파일명 관리

  • 사용자가 올린 원본 파일명을 그대로 저장하면 문제가 생길 수 있습니다.

➕ 10-1. 원본 파일명을 그대로 쓰면 위험한 이유

  • 같은 이름의 파일이 덮어쓰기 될 수 있습니다.
  • 한글, 공백, 특수문자로 경로 문제가 생길 수 있습니다.
  • 파일명에 의도치 않은 문자가 들어갈 수 있습니다.
  • 사용자의 개인정보가 파일명에 포함될 수 있습니다.
홍길동_신분증.png
../../../server.js
banner final 최종 진짜최종.png

➕ 10-2. 안전한 파일명 예시

import { randomUUID } from 'crypto';
import * as path from 'path';

function createSafeFileName(originalName: string) {
  const ext = path.extname(originalName).toLowerCase();
  const fileName = `${randomUUID()}${ext}`;

  return fileName;
}
  • 저장 파일명은 UUID처럼 충돌 가능성이 낮은 값으로 만들고, 원본 파일명은 필요한 경우 DB에 별도로 저장합니다.

✅ 11. 로컬 서버 저장 방식

  • 가장 간단한 방식은 서버 디렉토리에 파일을 저장하는 것입니다.

➕ 11-1. 로컬 저장 장점

  • 구현이 간단합니다.
  • 별도 클라우드 설정이 필요 없습니다.
  • 작은 프로젝트나 테스트 환경에서 빠르게 사용할 수 있습니다.

➕ 11-2. 로컬 저장 단점

  • 서버가 교체되면 파일 이전이 필요합니다.
  • 서버 디스크가 가득 찰 수 있습니다.
  • 서버가 여러 대면 파일 동기화 문제가 생깁니다.
  • 백업을 직접 관리해야 합니다.
  • 정적 파일 트래픽이 서버 부하로 이어질 수 있습니다.

➕ 11-3. 로컬 저장이 적합한 경우

  • 개발 환경
  • 임시 테스트
  • 내부 관리자 전용 소규모 서비스
  • 파일 수가 적고 서버가 1대인 구조

✅ 12. AWS S3 저장 방식

  • 운영 환경에서는 보통 파일을 서버에 직접 저장하지 않고 AWS S3 같은 객체 스토리지에 저장합니다.
  • S3는 이미지, 첨부파일, 백업 파일 같은 정적 파일을 저장하기에 적합합니다.

➕ 12-1. S3를 사용하는 이유

  • 서버 디스크 용량 부담을 줄일 수 있습니다.
  • 서버를 교체해도 파일이 유지됩니다.
  • CloudFront와 연결해 빠르게 파일을 제공할 수 있습니다.
  • 접근 권한과 수명 주기 정책을 관리할 수 있습니다.
  • 대량 파일 저장에 적합합니다.

➕ 12-2. S3 업로드 흐름

프론트엔드
  ↓ 파일 업로드 요청
백엔드
  ↓ 파일 검증
AWS S3
  ↓ 업로드 완료
백엔드
  ↓ 파일 URL 또는 key 저장
DB

✅ 13. S3에 저장할 때 DB에 저장할 정보

  • S3에 파일을 올린 뒤에는 DB에 파일 자체를 저장하는 것이 아니라 파일을 찾을 수 있는 정보를 저장합니다.
id
originalName
storedName
mimeType
size
bucket
key
url
createdAt

➕ 13-1. 파일 메타데이터 예시

{
  "id": 1,
  "originalName": "banner.png",
  "storedName": "0f4b7f8e-8b7a-4a1c-9f2a.png",
  "mimeType": "image/png",
  "size": 345123,
  "bucket": "my-service-bucket",
  "key": "banners/0f4b7f8e-8b7a-4a1c-9f2a.png",
  "url": "https://cdn.example.com/banners/0f4b7f8e-8b7a-4a1c-9f2a.png"
}
  • 실무에서는 URL보다 S3 key를 기준으로 관리하는 것이 더 유연한 경우가 많습니다.
  • CDN 도메인이 바뀌어도 key만 있으면 URL을 다시 만들 수 있기 때문입니다.

✅ 14. 정적 파일 제공

  • 정적 파일(Static File)은 서버에서 특별한 비즈니스 로직 없이 그대로 제공하는 파일입니다.
  • 이미지, CSS, JavaScript, 폰트, PDF 등이 대표적인 정적 파일입니다.

➕ 14-1. 정적 파일 예시

/images/banner.webp
/uploads/products/galaxy-s25.png
/assets/main.js
/fonts/Pretendard.woff2
  • React 빌드 결과물도 정적 파일입니다.
  • 브라우저는 HTML, CSS, JS, 이미지 파일을 다운로드해서 화면을 렌더링합니다.

✅ 15. CDN과 CloudFront

  • CDN(Content Delivery Network)은 정적 파일을 사용자와 가까운 서버에서 제공하는 네트워크입니다.
  • AWS에서는 CloudFront를 사용해 S3 파일을 빠르게 배포할 수 있습니다.

➕ 15-1. CDN을 사용하는 이유

  • 이미지 로딩 속도 개선
  • 서버 트래픽 감소
  • 글로벌 사용자 대응
  • 캐싱을 통한 비용 절감
  • 대량 접속 상황에서 안정성 향상
사용자
  ↓
CloudFront CDN
  ↓
S3 원본 파일
  • 처음 요청은 S3에서 가져오고, 이후 요청은 CloudFront 캐시에서 빠르게 응답할 수 있습니다.

✅ 16. 캐시 정책

  • 정적 파일은 캐싱을 잘 활용하면 성능이 크게 좋아집니다.
  • 하지만 파일이 바뀌었는데 사용자가 오래된 파일을 계속 보는 문제가 생길 수도 있습니다.

➕ 16-1. 장기 캐싱이 적합한 파일

main.a1b2c3.js
style.x9y8z7.css
banner.0f4b7f8e.webp
  • 파일명에 해시나 UUID가 들어가면 내용이 바뀔 때 파일명도 바뀝니다.
  • 이런 파일은 길게 캐싱해도 안전합니다.
Cache-Control: public, max-age=31536000, immutable

➕ 16-2. 짧은 캐싱이 적합한 파일

index.html
robots.txt
sitemap.xml
  • HTML이나 설정 파일은 변경사항이 빠르게 반영되어야 할 수 있습니다.
  • 너무 길게 캐싱하면 배포 후에도 이전 화면이 보일 수 있습니다.
Cache-Control: no-cache

✅ 17. 이미지 최적화

  • 업로드된 이미지를 그대로 사용하면 용량이 너무 커질 수 있습니다.
  • 이미지 최적화는 성능, SEO, 사용자 경험에 직접적인 영향을 줍니다.

➕ 17-1. 이미지 최적화 방법

  1. WebP 또는 AVIF 변환
  2. 이미지 리사이징
  3. 썸네일 생성
  4. 불필요한 메타데이터 제거
  5. 압축 품질 조정
  6. 모바일/PC용 이미지 분리
  7. lazy loading 적용

➕ 17-2. 실무 예시

원본 이미지:
product-original/galaxy-s25.png

최적화 이미지:
product/galaxy-s25_320.webp
product/galaxy-s25_768.webp
product/galaxy-s25_1200.webp
  • 화면 크기에 맞는 이미지를 내려주면 불필요한 트래픽을 줄일 수 있습니다.

✅ 18. 파일 업로드 보안 주의점

➕ 18-1. 업로드 허용 파일 제한

  • 모든 확장자를 허용하면 위험합니다.
  • 이미지 업로드 기능이라면 이미지 확장자와 MIME 타입만 허용해야 합니다.
허용:
jpg, jpeg, png, webp

차단:
exe, sh, js, html, php, zip

➕ 18-2. 실행 가능한 경로에 저장하지 않기

  • 사용자가 업로드한 파일이 서버에서 실행되면 매우 위험합니다.
  • 업로드 파일은 실행 권한이 없는 위치에 저장해야 합니다.
  • S3에 저장하더라도 HTML, JS 업로드를 허용하면 XSS 위험이 생길 수 있습니다.

➕ 18-3. 파일 크기 제한

  • 파일 크기 제한이 없으면 대용량 파일 업로드로 서버 메모리나 디스크가 고갈될 수 있습니다.
프로필 이미지: 2MB 이하
배너 이미지: 5MB 이하
첨부파일: 10MB 이하

➕ 18-4. 권한 검사

  • 파일 업로드 API는 반드시 인증과 권한 검사를 해야 합니다.
  • 특히 관리자 배너, 상품 이미지, 이벤트 이미지는 관리자만 업로드할 수 있어야 합니다.
프론트엔드에서 업로드 버튼 숨김
  ↓
백엔드에서 ADMIN 권한 검사
  ↓
권한 없으면 403 Forbidden

✅ 19. 파일 삭제 처리

  • 파일 삭제는 DB 데이터와 실제 저장소 파일을 함께 고려해야 합니다.

➕ 19-1. 삭제 시 고려할 것

  1. DB 레코드를 먼저 삭제할 것인가?
  2. S3 파일을 먼저 삭제할 것인가?
  3. 삭제 실패 시 재시도할 것인가?
  4. 이미 다른 데이터에서 같은 파일을 참조하고 있지는 않은가?
  5. 삭제 로그를 남길 것인가?
  6. 실수 삭제를 복구할 수 있는가?

➕ 19-2. 실무 전략

  • 중요한 파일은 즉시 물리 삭제하지 않고 deletedAt을 기록한 뒤 일정 기간 후 삭제할 수 있습니다.
  • S3 Lifecycle 정책을 활용해 특정 경로의 오래된 파일을 자동 삭제할 수도 있습니다.
  • DB에는 있는데 S3에는 없는 파일, S3에는 있는데 DB에는 없는 파일을 정리하는 배치 작업이 필요할 수 있습니다.

✅ 20. 실무 체크리스트

➕ 20-1. 프론트엔드 체크리스트

  1. 파일 선택 input이 있는가?
  2. 파일 크기 1차 검증이 있는가?
  3. 파일 타입 1차 검증이 있는가?
  4. 이미지 미리보기가 필요한가?
  5. 업로드 중 로딩 상태를 보여주는가?
  6. 업로드 실패 메시지를 보여주는가?
  7. 중복 업로드 클릭을 방지하는가?
  8. 업로드 완료 후 URL 또는 이미지가 화면에 반영되는가?

➕ 20-2. 백엔드 체크리스트

  1. 업로드 API에 인증/권한 검사가 있는가?
  2. 파일 크기 제한이 있는가?
  3. MIME 타입과 확장자 검증이 있는가?
  4. 안전한 파일명으로 저장하는가?
  5. 업로드 실패 시 에러 로그를 남기는가?
  6. DB에 파일 메타데이터를 저장하는가?
  7. S3 업로드 실패와 DB 저장 실패의 순서를 고려했는가?
  8. 삭제 시 DB와 실제 파일의 정합성을 고려했는가?

➕ 20-3. 운영 체크리스트

  1. S3 버킷이 공개되어 있지 않은가?
  2. 필요한 파일만 공개 접근 가능한가?
  3. CloudFront 캐시 정책이 적절한가?
  4. 이미지 용량이 과도하지 않은가?
  5. 사용하지 않는 파일이 계속 쌓이고 있지 않은가?
  6. 파일 업로드 실패 로그를 확인할 수 있는가?
  7. 업로드 파일 백업 또는 복구 전략이 있는가?
  8. 개인정보가 포함된 파일의 접근 권한이 제한되어 있는가?

✅ 21. AI를 활용해 파일 업로드 기능을 만들 때 질문법

  • 파일 업로드는 프론트엔드, 백엔드, 저장소, DB, 보안이 모두 연결됩니다.
  • AI에게 질문할 때는 저장 위치와 검증 조건을 명확히 알려줘야 합니다.

➕ 21-1. 좋은 질문 예시

React + NestJS + AWS S3 환경에서 관리자 배너 이미지 업로드 기능을 만들고 싶어.

조건:
1. 프론트엔드는 React + TypeScript
2. 백엔드는 NestJS
3. 저장소는 AWS S3
4. 이미지는 jpg, png, webp만 허용
5. 최대 파일 크기는 5MB
6. 관리자 권한이 있는 사용자만 업로드 가능
7. S3 key는 banners/{uuid}.{ext} 형식
8. DB에는 originalName, key, url, size, mimeType 저장
9. 업로드 실패 시 사용자 메시지와 서버 로그를 분리

프론트엔드 코드, NestJS Controller/Service 구조, DB 저장 구조, 주의점을 나눠서 설명해줘.

➕ 21-2. AI 답변 검증 기준

  1. 프론트엔드 검증만 믿고 있지 않은가?
  2. 백엔드에서 파일 크기와 타입을 검증하는가?
  3. 원본 파일명을 그대로 저장 경로에 쓰지 않는가?
  4. 관리자 권한 검사를 포함하는가?
  5. S3 Secret Key를 프론트엔드에 넣지 않는가?
  6. DB 저장 실패와 S3 업로드 실패를 고려하는가?
  7. 공개 파일과 비공개 파일의 접근 권한을 구분하는가?
  8. 캐싱과 이미지 최적화까지 고려하는가?

📌 요약

  • 파일 업로드는 사용자가 이미지나 문서를 서버로 전송하고, 서버가 이를 검증한 뒤 로컬 저장소나 S3 같은 외부 저장소에 저장하는 기능입니다.
  • 파일 업로드에는 보통 multipart/form-data와 FormData를 사용합니다.
  • 프론트엔드에서는 파일 선택, 1차 검증, 미리보기, 업로드 상태 표시를 담당합니다.
  • 백엔드에서는 파일 크기, MIME 타입, 확장자, 권한, 파일명을 반드시 검증해야 합니다.
  • 운영 환경에서는 로컬 서버 저장보다 AWS S3 같은 객체 스토리지를 사용하는 것이 관리와 확장에 유리합니다.
  • S3에 파일을 저장할 때는 DB에 파일 자체가 아니라 key, url, mimeType, size, originalName 같은 메타데이터를 저장합니다.
  • 정적 파일은 CDN과 캐싱 전략을 함께 적용하면 성능과 비용을 개선할 수 있습니다.
  • 업로드 파일은 보안 위험이 크므로 허용 파일 제한, 파일 크기 제한, 권한 검사, 안전한 파일명 생성, 실행 권한 차단을 반드시 고려해야 합니다.

0개의 댓글