본문으로 건너뛰기
김신건의 로그

[Django] File Uploads + Storage: FileField, ImageField, S3

· 수정 · 📖 약 1분 · 313자/단어 #django #files #upload #storage #media
django file upload, FileField, ImageField, FILE_UPLOAD_HANDLERS, MEDIA_ROOT, django-storages, S3 backend

정의

Django 의 파일 업로드 + 저장 시스템. FileField/ImageField + Storage backend 로 로컬 / S3 / GCS 등 자유롭게.

Model 필드

from django.db import models

def user_avatar_path(instance, filename):
    return f'avatars/{instance.user.id}/{filename}'

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    avatar = models.ImageField(
        upload_to=user_avatar_path,
        blank=True,
        null=True,
        max_length=200,
    )
    resume = models.FileField(
        upload_to='resumes/%Y/%m/',
        blank=True,
    )

upload_to 는 문자열 또는 함수. 함수는 (instance, filename) → path 반환.

Form + View

# forms.py
class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ['avatar', 'resume']

# views.py
def upload_profile(request):
    if request.method == 'POST':
        form = ProfileForm(request.POST, request.FILES, instance=request.user.profile)
        # request.FILES 필수!
        if form.is_valid():
            form.save()
            return redirect('profile')
    else:
        form = ProfileForm(instance=request.user.profile)
    return render(request, 'upload.html', {'form': form})

Template

<!-- multipart/form-data 필수! -->
<form method="post" enctype="multipart/form-data">
  {% csrf_token %}
  {{ form.as_p }}
  <button>업로드</button>
</form>

CAUTION

enctype="multipart/form-data" 없으면 파일 안 올라감. 잊기 쉬움.

Settings

# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# 업로드 크기 한도
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440   # 2.5MB (메모리)
DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440    # form data 전체
FILE_UPLOAD_HANDLERS = [
    'django.core.files.uploadhandler.MemoryFileUploadHandler',
    'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]

개발 시 media 파일 serve:

# urls.py
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    ...
]
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

IMPORTANT

프로덕션은 nginx/CloudFront 로 media serve. Django 로 serve = 매우 느림.

Storage Backend

flowchart LR
    Model[FileField] --> Storage[Storage backend]
    Storage --> Local["FileSystemStorage (기본)"]
    Storage --> S3[S3Boto3Storage]
    Storage --> GCS[GoogleCloudStorage]
    Storage --> Azure[AzureStorage]
    Storage --> Custom[Custom storage]

Django 6.0 의 STORAGES 설정

# settings.py (Django 6.0+)
STORAGES = {
    'default': {
        'BACKEND': 'storages.backends.s3.S3Storage',
        'OPTIONS': {
            'bucket_name': 'my-media',
            'region_name': 'us-east-1',
            'location': 'media',
            'default_acl': 'private',
            'file_overwrite': False,
        },
    },
    'staticfiles': {
        'BACKEND': 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage',
    },
}

TIP

STORAGES 는 Django 4.2+ 부터. DEFAULT_FILE_STORAGE 는 deprecated.

S3 예시 (django-storages)

pip install django-storages[boto3]
STORAGES = {
    'default': {
        'BACKEND': 'storages.backends.s3.S3Storage',
        'OPTIONS': {
            'bucket_name': env('AWS_S3_BUCKET'),
            'region_name': env('AWS_REGION'),
            'location': 'media',
            'file_overwrite': False,
            'querystring_expire': 3600,   # signed URL expiration
        },
    },
}

S3 자세한 건 aws-s3.

Custom Storage

from django.core.files.storage import Storage

class MyStorage(Storage):
    def _save(self, name, content):
        # 저장 로직
        return saved_name

    def _open(self, name, mode='rb'):
        # 열기 로직
        return file

    def exists(self, name): ...
    def url(self, name): ...
    def size(self, name): ...

Direct Upload (프리사인 URL)

브라우저 → S3 직접 업로드 (서버 우회):

import boto3

def get_presigned_url(request):
    filename = request.GET['filename']
    key = f'uploads/{request.user.id}/{filename}'

    s3 = boto3.client('s3')
    url = s3.generate_presigned_post(
        Bucket='my-bucket',
        Key=key,
        Conditions=[
            ['content-length-range', 0, 10 * 1024 * 1024],
        ],
        ExpiresIn=3600,
    )
    return JsonResponse(url)
// 클라이언트
const {url, fields} = await fetch('/presigned-url/?filename=x.jpg').then(r => r.json());
const formData = new FormData();
Object.entries(fields).forEach(([k, v]) => formData.append(k, v));
formData.append('file', file);
await fetch(url, { method: 'POST', body: formData });

이미지 처리

from PIL import Image

def save(self, *args, **kwargs):
    super().save(*args, **kwargs)
    if self.avatar:
        img = Image.open(self.avatar.path)
        img.thumbnail((300, 300))
        img.save(self.avatar.path)

또는 django-imagekit / easy-thumbnails.

Security

# 파일 형식 검증
def validate_image(value):
    valid_extensions = ['.jpg', '.png', '.webp']
    ext = os.path.splitext(value.name)[1].lower()
    if ext not in valid_extensions:
        raise ValidationError('허용 안 된 형식')

    # MIME type 확인
    import magic
    mime = magic.from_buffer(value.read(2048), mime=True)
    value.seek(0)
    if not mime.startswith('image/'):
        raise ValidationError('이미지가 아닙니다')

class Profile(models.Model):
    avatar = models.ImageField(upload_to='avatars/', validators=[validate_image])

CAUTION

확장자만 검증 = 위험. evil.exe.jpg 통과. python-magic 으로 MIME sniff.

흔한 함정

WARNING

  1. enctype 잊음 = 파일 안 올라감 (silent).
  2. 로컬 저장 → 프로덕션 배포 후 사라짐 = 컨테이너 재시작. S3 등 외부 저장.
  3. 크기 한도 안 정함 = 100MB 업로드 = OOM. FILE_UPLOAD_MAX_MEMORY_SIZE.
  4. 경로 traversal = ../../etc/passwd 같은 filename. Django 는 자동 방어하지만 커스텀 upload_to 는 주의.

다른 프레임워크

Framework파일 업로드
DjangoFileField + request.FILES
RailsActive Storage (7+)
SpringMultipartFile
FastAPIUploadFile
Expressmulter

관련 위키

이 글의 용어 (4개)
[AWS] S3: object storage, storage classes, lifecyclecloud
정의 S3 = AWS 의 object storage. bucket + key + object. 11 9's durability, 무한 확장. 2026 시점 인터넷의 핵심 스토리지…
[Django] 보안: CSRF, XSS, SQL injection, headersdjango
정의 Django는 OWASP Top 10 대부분을 기본 방어한다: CSRF 토큰, ORM의 SQL 인젝션 방지, 템플릿 자동 escape, 비밀번호 해싱, 세션 쿠키 보안 등.…
[Django] Form과 ModelFormdjango
정의 Django Form은 사용자 입력 정의·검증·렌더링·바인딩을 통합. 일반 은 임의 필드, 은 모델 자동 매핑. 검증과 보안(CSRF, SQL 인젝션 방지) 내장. Form…
[Django] Static과 Media 파일django
정의 Django는 정적 파일을 두 카테고리로 구분한다. - Static: 앱·테마의 일부인 CSS/JS/이미지. 배포 시 으로 한 곳에 모음. - Media: 사용자가 업로드한…

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기