[Django] Static과 Media 파일
정의
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. .gitignore에 staticfiles/.
3. media 디렉터리 권한
웹 서버가 쓸 수 있는 권한 필요. Docker에선 named volume 또는 bind mount.
4. URL 충돌
STATIC_URL = "/static/"
MEDIA_URL = "/static/media/" # /static/이 먼저 매치되어 404
prefix 겹치지 않게.
5. ImageField + 큰 이미지
업로드된 거대 이미지를 그대로 표시 → 페이지 무거움. thumbnail로 줄여서 표시 + 원본 별도 URL.
💬 댓글