장고 중급 #6 Static/Media 운영과 storage backends

6 분 소요

기초 #5에서 정적 파일을 처음 다뤘습니다. 개발 서버 (python manage.py runserver)가 알아서 서빙해주는 모드였습니다. 운영으로 넘어가면 그 자동이 사라집니다 — 장고는 운영에서 정적 파일을 서빙하지 않는 게 관례입니다.

이번 글은 그 부분을 다룹니다. 두 종류의 파일과, 그것을 다루는 운영 패턴까지 정리합니다.

  • Static — 개발자가 만든 파일 (CSS/JS/이미지/폰트)
  • Media — 사용자가 업로드한 파일 (프로필 사진, 첨부 등)

Static vs Media — 두 가지를 분리하는 이유 #

StaticMedia
출처개발자 (저장소에 커밋)사용자 업로드
변경 빈도배포 단위실시간
백업 필요성낮음 (저장소에 있음)높음 (잃으면 끝)
CDN거의 항상종종
캐싱강한 캐싱 (해시 파일명)보통/짧은 캐싱

이 차이 때문에 장고는 두 시스템을 분리해 두었습니다. 설정도 별도, 핸들링도 별도.

Static — 개발자 만든 파일 #

세 가지 설정 — STATIC_URL, STATICFILES_DIRS, STATIC_ROOT #

이름이 비슷해서 자주 헷갈리는 셋입니다.

settings.py
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_ROOTcollectstatic이 모은 결과운영 시 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_ROOTcollectstatic이 매 배포마다 새로 만드는 디렉터리입니다. git에 넣지 마세요.

.gitignore
staticfiles/
media/

Media — 사용자 업로드 파일 #

모델 — FileField, ImageField #

blog/models.py
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도 가능 — 함수로 동적 경로

ImageFieldPillow가 필요합니다.

설치
pip install Pillow

설정 — MEDIA_URL, MEDIA_ROOT #

settings.py
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

개발 시 서빙 — 한 줄 #

장고는 운영에서 미디어를 서빙 안 하지만, 개발 시엔 편의를 위해 한 줄로 활성화할 수 있습니다.

config/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)

if settings.DEBUG가지로 둘러서 운영에선 동작 안 하게 막습니다.

템플릿에서 #

템플릿
{% if user.profile.avatar %}
  <img src="{{ user.profile.avatar.url }}" alt="avatar">
{% endif %}

avatar.urlMEDIA_URL + 저장 경로를 자동 조합합니다.

운영 — 장고는 정적 파일을 서빙 안 한다 #

운영 환경에서 정적/미디어 서빙의 표준 패턴 셋:

패턴 1 — nginx가 직접 #

가장 전통적이고 가장 빠른 방법입니다.

nginx.conf 일부
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] boto3
settings.py — 미디어를 S3로
INSTALLED_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 — 정적은 로컬, 미디어는 S3
STORAGES = {
    "default": {                                   # 미디어 (모델의 FileField/ImageField)
        "BACKEND": "storages.backends.s3.S3Storage",
    },
    "staticfiles": {                                # 정적 파일
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
    },
}

정적 파일도 S3로 — CDN 효과 #

정적도 S3 + CloudFront
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 whitenoise
settings.py
MIDDLEWARE = [
    "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 --noinput

WhiteNoise가 STATIC_ROOT에서 직접 서빙합니다. nginx 없이 Heroku / Railway / Fly 같은 PaaS에 올릴 때 가장 흔한 답.

Static만, Media는 안 됨 #

WhiteNoise는 사용자 업로드 (media)는 다루지 않습니다. Media는 여전히 S3 같은 외부 스토리지가 필요합니다. 사용자 업로드가 없는 앱이라면 WhiteNoise만으로 충분합니다.

Storage 백엔드 추상화 #

장고는 모든 파일 저장을 Storage 추상 클래스 뒤에 둡니다. FileFieldstorage= 인자로 모델별 다른 저장소도 지정할 수 있습니다.

모델별 다른 저장소
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을 발급해 일시적으로 접근하게 할 수 있습니다.

signed URL
url = private_storage.url(document.file.name)
# https://...amazonaws.com/...?X-Amz-Algorithm=...&Expires=...

AWS_QUERYSTRING_AUTH = TrueAWS_QUERYSTRING_EXPIRE = 3600 (1시간) 같은 설정으로 만료를 제어합니다.

이미지 처리 — 한 줄 안내 #

업로드된 이미지를 썸네일/리사이즈 해야 한다면 흔한 라이브러리 둘:

라이브러리특징
django-imagekit모델 필드에 specs 정의, 요청 시 또는 미리 생성
sorl-thumbnail템플릿 태그로 즉시 생성, 캐싱
imagekit 예
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
  • 보안 헤더 (#5SECURE_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까지 한 호흡에 다루겠습니다.

X