장고 중급 #6 Static/Media 운영과 storage backends
기초 #5에서 정적 파일을 처음 다뤘습니다. 개발 서버 (python manage.py runserver)가 알아서 서빙해주는 모드였습니다. 운영으로 넘어가면 그 자동이 사라집니다 — 장고는 운영에서 정적 파일을 서빙하지 않는 게 관례입니다.
이번 글은 그 부분을 다룹니다. 두 종류의 파일과, 그것을 다루는 운영 패턴까지 정리합니다.
- Static — 개발자가 만든 파일 (CSS/JS/이미지/폰트)
- Media — 사용자가 업로드한 파일 (프로필 사진, 첨부 등)
Static vs Media — 두 가지를 분리하는 이유 #
| Static | Media | |
|---|---|---|
| 출처 | 개발자 (저장소에 커밋) | 사용자 업로드 |
| 변경 빈도 | 배포 단위 | 실시간 |
| 백업 필요성 | 낮음 (저장소에 있음) | 높음 (잃으면 끝) |
| CDN | 거의 항상 | 종종 |
| 캐싱 | 강한 캐싱 (해시 파일명) | 보통/짧은 캐싱 |
이 차이 때문에 장고는 두 시스템을 분리해 두었습니다. 설정도 별도, 핸들링도 별도.
Static — 개발자 만든 파일 #
세 가지 설정 — STATIC_URL, STATICFILES_DIRS, STATIC_ROOT
#
이름이 비슷해서 자주 헷갈리는 셋입니다.
STATIC_URL = "/static/"
STATICFILES_DIRS = [
BASE_DIR / "static", # 프로젝트 전역 정적 파일
]
STATIC_ROOT = BASE_DIR / "staticfiles" # collectstatic의 출력지| 설정 | 의미 | 쓰임 |
|---|---|---|
STATIC_URL | 브라우저가 보는 URL prefix | /static/css/style.css |
STATICFILES_DIRS | 개발 시 추가로 검색할 디렉터리 | 프로젝트 전역 정적 파일 |
STATIC_ROOT | collectstatic이 모은 결과 | 운영 시 nginx가 서빙 |
추가로, 앱별 static/ 디렉터리는 AppDirectoriesFinder가 자동으로 찾아줍니다. blog/static/blog/style.css 같은 형태.
템플릿에서 #
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<img src="{% static 'images/logo.png' %}" alt="logo">
<script src="{% static 'js/app.js' %}"></script>{% static %} 태그가 STATIC_URL + 파일 경로를 조합해줍니다.
collectstatic — 운영 배포의 정수
#
개발 시엔 여러 곳에 흩어진 정적 파일을:
- 앱별
static/들 STATICFILES_DIRS의 디렉터리들- 외부 라이브러리 (
django.contrib.admin등)의 정적 파일
collectstatic이 **한 곳 (STATIC_ROOT)**에 모아줍니다.
python manage.py collectstatic --noinput--noinput은 “기존 파일을 덮어쓸까요?” 같은 프롬프트를 자동으로 yes로 처리합니다. CI에서는 필수입니다.
배포 흐름:
1. 코드 pull / 이미지 빌드
2. python manage.py migrate ← DB 스키마
3. python manage.py collectstatic --noinput ← 정적 파일 모음
4. 새 프로세스 시작 (gunicorn 재시작 등)STATIC_ROOT는 git ignore
#
STATIC_ROOT는 collectstatic이 매 배포마다 새로 만드는 디렉터리입니다. git에 넣지 마세요.
staticfiles/
media/Media — 사용자 업로드 파일 #
모델 — FileField, ImageField
#
from django.db import models
class Profile(models.Model):
user = models.OneToOneField("auth.User", on_delete=models.CASCADE)
avatar = models.ImageField(upload_to="avatars/%Y/%m/", blank=True)
class Post(models.Model):
title = models.CharField(max_length=200)
cover = models.ImageField(upload_to="covers/", blank=True)
attachment = models.FileField(upload_to="attachments/%Y/%m/", blank=True)upload_to의 동작:
"avatars/"—MEDIA_ROOT/avatars/아래 저장"covers/%Y/%m/"— 연/월별 폴더 자동 생성 (covers/2026/05/)- callable도 가능 — 함수로 동적 경로
ImageField는 Pillow가 필요합니다.
pip install Pillow설정 — MEDIA_URL, MEDIA_ROOT
#
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"개발 시 서빙 — 한 줄 #
장고는 운영에서 미디어를 서빙 안 하지만, 개발 시엔 편의를 위해 한 줄로 활성화할 수 있습니다.
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)if settings.DEBUG가지로 둘러서 운영에선 동작 안 하게 막습니다.
템플릿에서 #
{% if user.profile.avatar %}
<img src="{{ user.profile.avatar.url }}" alt="avatar">
{% endif %}avatar.url이 MEDIA_URL + 저장 경로를 자동 조합합니다.
운영 — 장고는 정적 파일을 서빙 안 한다 #
운영 환경에서 정적/미디어 서빙의 표준 패턴 셋:
패턴 1 — nginx가 직접 #
가장 전통적이고 가장 빠른 방법입니다.
server {
listen 80;
server_name myblog.com;
location /static/ {
alias /srv/myblog/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /srv/myblog/media/;
expires 30d;
}
location / {
proxy_pass http://127.0.0.1:8000; # gunicorn
}
}/static/과 /media/는 nginx가 직접 디스크에서 서빙하고, 그 외 요청만 gunicorn (장고)으로 넘깁니다.
패턴 2 — 클라우드 스토리지 (S3 등) #
스케일이 커지거나 다중 서버가 되면 디스크 공유가 어려워집니다. S3, GCS, Azure Blob 같은 객체 스토리지로 옮깁니다.
패턴 3 — WhiteNoise (작은 앱용) #
간단한 앱에선 nginx 없이 gunicorn 앞단에서 정적 파일을 서빙할 수 있습니다.
django-storages + S3 — 본격 패턴
#
pip install django-storages[s3] boto3INSTALLED_APPS = [
...
"storages",
]
# AWS 자격 (환경 변수에서 읽기 권장)
AWS_ACCESS_KEY_ID = os.environ["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = os.environ["AWS_SECRET_ACCESS_KEY"]
AWS_STORAGE_BUCKET_NAME = "my-blog-media"
AWS_S3_REGION_NAME = "ap-northeast-2"
AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com"
AWS_DEFAULT_ACL = None
AWS_QUERYSTRING_AUTH = False # 공개 버킷이면
# Django 4.2+ 의 STORAGES 설정
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}이렇게 두면 Post.cover.save(...) 같은 호출이 자동으로 S3에 업로드됩니다. 모델 코드는 한 줄도 안 바뀝니다. cover.url도 자동으로 S3 URL.
Django 4.2+ 의 STORAGES 설정
#
장고 4.2부터 DEFAULT_FILE_STORAGE / STATICFILES_STORAGE가 deprecated. 통합된 STORAGES dict를 씁니다.
STORAGES = {
"default": { # 미디어 (모델의 FileField/ImageField)
"BACKEND": "storages.backends.s3.S3Storage",
},
"staticfiles": { # 정적 파일
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}정적 파일도 S3로 — CDN 효과 #
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {"location": "media"},
},
"staticfiles": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {"location": "static"},
},
}collectstatic이 자동으로 S3에 업로드됩니다. CloudFront 같은 CDN을 앞단에 두면 글로벌하게 빠른 정적 서빙.
보안 한 줄 — 자격증명 #
AWS 키를 코드/저장소에 넣지 마세요. 환경 변수, 시크릿 매니저, IAM Role 어디에 두든 코드 밖.
AWS_ACCESS_KEY_ID = os.environ["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = os.environ["AWS_SECRET_ACCESS_KEY"]EC2/ECS/EKS 라면 IAM Role을 붙여 키 자체를 코드에서 다루지 않는 게 가장 안전한 답입니다.
WhiteNoise — 작은 앱의 정답 #
별도 nginx도, S3도 부담스러운 작은 앱에선 WhiteNoise가 답입니다. gunicorn 앞단에서 정적 파일을 직접 서빙합니다.
pip install whitenoiseMIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # SecurityMiddleware 바로 다음
...
]
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}CompressedManifestStaticFilesStorage가 무엇:
- manifest — 파일 내용 해시를 파일명에 포함 (
style.abc123.css). 변경 시 새 URL이 되어 캐시 무효화 자동 - compressed — gzip / brotli 압축 버전을 미리 만들어둠
python manage.py collectstatic --noinputWhiteNoise가 STATIC_ROOT에서 직접 서빙합니다. nginx 없이 Heroku / Railway / Fly 같은 PaaS에 올릴 때 가장 흔한 답.
Static만, Media는 안 됨 #
WhiteNoise는 사용자 업로드 (media)는 다루지 않습니다. Media는 여전히 S3 같은 외부 스토리지가 필요합니다. 사용자 업로드가 없는 앱이라면 WhiteNoise만으로 충분합니다.
Storage 백엔드 추상화
#
장고는 모든 파일 저장을 Storage 추상 클래스 뒤에 둡니다. FileField의 storage= 인자로 모델별 다른 저장소도 지정할 수 있습니다.
from storages.backends.s3 import S3Storage
private_storage = S3Storage(bucket_name="my-private-bucket", default_acl="private")
class Document(models.Model):
file = models.FileField(upload_to="docs/", storage=private_storage)비공개 문서는 별도 비공개 버킷, 공개 이미지는 기본 버킷, 같은 분리가 가능합니다.
Pre-signed URL — 비공개 파일 공유 #
S3 버킷이 비공개여도 시간 제한 URL을 발급해 일시적으로 접근하게 할 수 있습니다.
url = private_storage.url(document.file.name)
# https://...amazonaws.com/...?X-Amz-Algorithm=...&Expires=...AWS_QUERYSTRING_AUTH = True와 AWS_QUERYSTRING_EXPIRE = 3600 (1시간) 같은 설정으로 만료를 제어합니다.
이미지 처리 — 한 줄 안내 #
업로드된 이미지를 썸네일/리사이즈 해야 한다면 흔한 라이브러리 둘:
| 라이브러리 | 특징 |
|---|---|
| django-imagekit | 모델 필드에 specs 정의, 요청 시 또는 미리 생성 |
| sorl-thumbnail | 템플릿 태그로 즉시 생성, 캐싱 |
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill
class Profile(models.Model):
avatar = models.ImageField(upload_to="avatars/")
avatar_thumb = ImageSpecField(
source="avatar",
processors=[ResizeToFill(150, 150)],
format="JPEG",
options={"quality": 85},
)크기별 변환을 모델에 선언하면 라이브러리가 알아서 생성합니다. 이미지가 무겁다면 백그라운드 작업 (실전 #4 Celery)으로 넘기는 패턴도 흔합니다.
실전 체크리스트 — 운영 배포 전 #
-
DEBUG = False -
ALLOWED_HOSTS명시 -
STATIC_ROOT설정 +.gitignore등록 -
python manage.py collectstatic --noinput이 배포 스크립트에 포함 - nginx / WhiteNoise / S3 중 어떤 방식으로 정적 파일 서빙할지 결정
- 미디어 파일 백업 정책 (S3 라면 버전 관리 / 라이프사이클)
- AWS 자격증명은 환경 변수 / IAM Role
- 보안 헤더 (#5 —
SECURE_SSL_REDIRECT,HSTS등)
정리 #
이번 글에서 잡은 것:
- Static vs Media — 개발자/사용자, 변경 빈도, 백업 필요성이 다른 두 종류
STATIC_URL(브라우저 prefix),STATICFILES_DIRS(개발 검색 경로),STATIC_ROOT(collectstatic 출력)collectstatic— 배포 시 흩어진 정적 파일을 한 곳에 모음MEDIA_URL,MEDIA_ROOT, 모델의FileField/ImageField,upload_to- 개발 시에만
static(settings.MEDIA_URL, ...)로 서빙 - 운영에선 장고가 정적 파일을 서빙하지 않음 — nginx / S3 / WhiteNoise
django-storages[s3]+STORAGES(4.2+)로 S3 전환- WhiteNoise — 작은 앱의 답, 미들웨어 한 줄 +
CompressedManifestStaticFilesStorage Storage추상화로 모델별 다른 저장소- Pre-signed URL로 비공개 파일 시간 제한 공유
- 이미지 처리:
django-imagekit,sorl-thumbnail
다음 글(#7 테스트)에서는 중급의 마지막 — 테스트 입니다. django.test.TestCase, fixtures, factory_boy, pytest-django까지 한 호흡에 다루겠습니다.