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

[Django] Static과 Media 파일

· 수정 · 📖 약 1분 · 552자/단어 #django #static #media #deployment
django static files, django media, STATIC_URL, MEDIA_URL, collectstatic, whitenoise

정의

Django는 정적 파일을 두 카테고리로 구분한다.

  • Static: 앱·테마의 일부인 CSS/JS/이미지. 배포 시 collectstatic으로 한 곳에 모음.
  • Media: 사용자가 업로드한 파일 (프로필 사진, 첨부파일).

개발 서버는 자동으로 둘 다 서빙하지만, 프로덕션은 별도 처리 필요 (nginx, S3, whitenoise 등).

settings.py

# 정적 파일 (앱 코드의 일부)
STATIC_URL = "/static/"                          # URL prefix
STATICFILES_DIRS = [BASE_DIR / "static"]         # 프로젝트 공통 정적 파일
STATIC_ROOT = BASE_DIR / "staticfiles"           # collectstatic 결과

# 미디어 (사용자 업로드)
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

각 앱 내부 정적 파일은 자동 발견: myapp/static/myapp/css/....

collectstatic

python manage.py collectstatic --noinput

STATICFILES_FINDERS로 발견된 정적 파일을 모두 STATIC_ROOT로 복사. nginx/CDN이 이 디렉터리를 서빙.

템플릿에서 사용

{% load static %}

<link rel="stylesheet" href="{% static 'css/main.css' %}">
<script src="{% static 'js/app.js' %}" defer></script>
<img src="{% static 'images/logo.png' %}" alt="logo">

{# media (모델의 FileField) #}
<img src="{{ user.avatar.url }}" alt="avatar">

개발 vs 프로덕션 서빙

개발 (DEBUG=True)

# 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)

runserver가 자동으로 정적 파일도 서빙. 프로덕션 X.

프로덕션

옵션 1: nginx

location /static/ {
    alias /app/staticfiles/;
    expires 365d;
    add_header Cache-Control "public, immutable";
}
location /media/ {
    alias /app/media/;
}

옵션 2: whitenoise (Python 직접)

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",    # 두 번째
    ...
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

gzip 자동 + URL hashing (cache busting). Heroku/Render 등 PaaS에서 표준.

옵션 3: S3 + CloudFront

STORAGES = {
    "default": {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"},
    "staticfiles": {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"},
}
AWS_STORAGE_BUCKET_NAME = "..."
AWS_S3_CUSTOM_DOMAIN = "cdn.example.com"

대용량 / 글로벌 트래픽에 적합.

캐시 무효화 (manifest)

STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"

collectstatic 시 각 파일에 hash 추가: main.abc123.css. 변경 시 새 URL → 브라우저 새로 로드. 1년 캐시도 안전.

FileField / ImageField

class Profile(models.Model):
    avatar = models.ImageField(upload_to="avatars/%Y/%m/")    # 동적 경로
    resume = models.FileField(upload_to="resumes/")
profile = Profile.objects.create(avatar=request.FILES["avatar"])
profile.avatar.url            # /media/avatars/2026/06/avatar.jpg
profile.avatar.path           # 절대 파일시스템 경로
profile.avatar.size

ImageField는 Pillow 의존. PIL로 dimensions 자동 검증.

업로드 검증

def validate_avatar(value):
    if value.size > 5 * 1024 * 1024:
        raise ValidationError("5MB 이하")
    if not value.content_type.startswith("image/"):
        raise ValidationError("이미지만")

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

확장자만 의존 X. content_type 검증 + Pillow로 재인코딩 권장.

DataURI / Base64

작은 이미지는 inline:

<img src="data:image/png;base64,iVBORw0...">

요청 1회 절약하나 캐싱 불가. 일반적으론 별 파일 권장.

자주 보는 패턴

사용자별 디렉터리

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

class Profile(models.Model):
    avatar = models.ImageField(upload_to=user_avatar_path)

Random filename

import uuid

def random_filename(instance, filename):
    ext = filename.split(".")[-1]
    return f"uploads/{uuid.uuid4().hex}.{ext}"

class Doc(models.Model):
    file = models.FileField(upload_to=random_filename)

원본 파일명 노출 방지 + 충돌 회피.

Thumbnail 자동 생성

from PIL import Image

class Profile(models.Model):
    avatar = models.ImageField(upload_to="avatars/")

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

또는 easy-thumbnails, sorl-thumbnail 라이브러리.

Private 파일 (인증 필요)

@login_required
def serve_file(request, file_id):
    doc = get_object_or_404(Document, id=file_id)
    if doc.owner != request.user:
        return HttpResponseForbidden()

    response = FileResponse(doc.file.open("rb"))
    response["Content-Disposition"] = f'attachment; filename="{doc.original_name}"'
    return response

MEDIA_URL로 공개 노출하지 말고 view 거치게.

또는 nginx X-Accel-Redirect (Python은 권한만, 파일 전송은 nginx):

response = HttpResponse()
response["X-Accel-Redirect"] = f"/protected/{doc.file.name}"
return response

함정

1. DEBUG=False면 정적 파일 안 서빙

runserver는 DEBUG=False면 static 안 서빙. 프로덕션엔 nginx/whitenoise 필수.

2. STATIC_ROOT를 .gitignore 안 함

collectstatic 결과는 빌드 산출물. git에 커밋 X. .gitignorestaticfiles/.

3. media 디렉터리 권한

웹 서버가 쓸 수 있는 권한 필요. Docker에선 named volume 또는 bind mount.

4. URL 충돌

STATIC_URL = "/static/"
MEDIA_URL = "/static/media/"    # /static/이 먼저 매치되어 404

prefix 겹치지 않게.

5. ImageField + 큰 이미지

업로드된 거대 이미지를 그대로 표시 → 페이지 무거움. thumbnail로 줄여서 표시 + 원본 별도 URL.

💬 댓글

사이트 검색 / 명령어

검색

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