Skip to main content
Summary

static과 media는 무엇이 다른가

웹 페이지는 HTML만으로 완성되지 않습니다. CSS가 있어야 보기 좋은 화면이 되고, JavaScript가 있어야 브라우저에서 동적인 동작을 만들 수 있습니다. 로고, 아이콘, 기본 배경 이미지도 필요합니다. Django에서는 이런 파일을 정적 파일(static files) 이라고 부릅니다.

반면 사용자가 회원가입 후 올린 프로필 사진, 게시글에 첨부한 이미지, 문서 파일은 성격이 다릅니다. 개발자가 미리 준비해 배포한 파일이 아니라, 서비스가 실행되는 동안 사용자가 올린 파일입니다. Django에서는 이를 미디어 파일(media files) 이라고 부릅니다.

둘 다 브라우저에서는 URL로 접근하는 파일처럼 보입니다. 하지만 관리 방식은 완전히 다릅니다.

  • static: 개발자가 만든 CSS, JavaScript, 이미지. 보통 코드 저장소에 함께 둔다.

  • media: 사용자가 업로드한 파일. 보통 코드 저장소에 넣지 않고 별도로 보관한다.

이 구분이 중요한 이유는 파일의 생명주기가 다르기 때문입니다. static 파일은 개발자가 수정하고 배포할 때 함께 모으면 됩니다. 하지만 media 파일은 배포 후에도 사용자가 계속 추가합니다. 사용자 업로드 파일을 static 폴더에 넣으면 배포 과정에서 덮어쓰거나 누락될 수 있고, 반대로 CSS 파일을 media처럼 관리하면 캐시와 배포 관리가 어려워집니다.1

정적 파일과 미디어 파일 다루기AI 생성 이미지

정적 파일 설정하기

Django 프로젝트를 만들면 보통 django.contrib.staticfiles 앱이 기본으로 포함되어 있습니다. 이 앱은 여러 앱과 프로젝트 폴더에 흩어진 정적 파일을 찾아 주는 역할을 합니다.

먼저 settings.py에 정적 파일 설정을 둡니다.

# config/settings.py

STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / 'staticfiles'

각 설정의 역할은 다릅니다.

STATIC_URL은 브라우저에서 정적 파일에 접근할 때 사용할 URL 접두어입니다. 위처럼 설정하면 static/css/site.css 파일은 브라우저에서 /static/css/site.css 같은 URL로 접근하게 됩니다. 새 Django 프로젝트에서는 STATIC_URL = 'static/'처럼 앞의 슬래시 없이 만들어지는 경우도 있습니다. 이 장에서는 독자가 URL을 눈으로 따라가기 쉽도록 루트 기준 URL인 /static/ 형태로 설명하겠습니다.

STATICFILES_DIRS는 개발자가 프로젝트 공통 정적 파일을 두는 위치입니다. 예를 들어 전체 사이트에서 사용할 CSS와 JavaScript는 다음처럼 둘 수 있습니다.

myproject/
├── config/
├── boards/
├── templates/
├── static/
│   ├── css/
│   │   └── site.css
│   └── js/
│       └── site.js
└── manage.py

앱 전용 정적 파일은 앱 내부에 둘 수도 있습니다.

boards/
├── static/
│   └── boards/
│       └── board.css
├── templates/
├── models.py
└── views.py

앱 내부 경로에 다시 boards/ 폴더를 넣는 이유는 템플릿 폴더를 나눌 때와 비슷합니다. 다른 앱에도 style.css 같은 이름의 파일이 있을 수 있기 때문입니다. boards/board.css처럼 앱 이름을 한 번 더 넣으면 파일 이름 충돌을 줄일 수 있습니다.

STATIC_ROOT는 배포할 때 사용하는 설정입니다. 개발 중에 CSS를 직접 작성하는 폴더가 아니라, collectstatic 명령이 여러 위치의 정적 파일을 한곳에 모아 두는 목적지입니다. 따라서 직접 작업하는 원본 파일을 STATIC_ROOT에 두지 않는 편이 좋습니다.

템플릿에서 CSS와 JavaScript 연결하기

정적 파일은 템플릿에서 직접 /static/css/site.css처럼 쓰기보다 {% static %} 태그를 사용하는 것이 좋습니다. 나중에 STATIC_URL이 바뀌거나 정적 파일 저장 방식이 바뀌어도 템플릿 코드를 덜 고치게 됩니다.

공통 레이아웃인 base.html에 CSS와 JavaScript를 연결해 보겠습니다.

{% load static %}
<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>Django Board</title>
  <link rel="stylesheet" href="{% static 'css/site.css' %}">
</head>
<body>
  <header>
    <h1><a href="{% url 'boards:post_list' %}">게시판</a></h1>
  </header>

  <main>
    {% block content %}{% endblock %}
  </main>

  <script src="{% static 'js/site.js' %}"></script>
</body>
</html>

핵심은 두 줄입니다.

{% load static %}
<link rel="stylesheet" href="{% static 'css/site.css' %}">

{% load static %}은 템플릿에서 static 태그를 사용하겠다는 선언입니다. {% static 'css/site.css' %}는 기본 설정에서는 STATIC_URL과 파일 경로를 조합해 실제 URL을 만들어 줍니다. 정적 파일 저장소를 더 고급으로 설정하면, 이 태그가 해시가 붙은 파일명이나 CDN 주소를 만들어 주는 식으로 동작할 수도 있습니다.

이제 static/css/site.css에 간단한 스타일을 추가할 수 있습니다.

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  background: #f6f7f9;
  color: #222;
}

main {
  max-width: 720px;
  margin: 40px auto;
  padding: 24px;
  background: white;
  border-radius: 12px;
}

a {
  color: #2563eb;
  text-decoration: none;
}

이 설정만으로도 게시판 화면은 훨씬 읽기 좋아집니다. View나 Model 코드는 그대로 두고, 화면의 표현만 CSS로 분리한 것입니다.

개발 환경과 배포 환경의 차이

개발 중에는 python manage.py runserver로 실행한 Django 개발 서버가 정적 파일을 찾아 보여 줍니다. DEBUG=True이고 django.contrib.staticfiles가 활성화되어 있으면, Django는 STATICFILES_DIRS와 각 앱의 static/ 폴더를 검색합니다.

하지만 배포 환경에서는 방식이 달라집니다. 운영 서버에서 Django 애플리케이션은 로그인, 게시글 조회, 폼 처리처럼 동적인 요청을 처리하는 데 집중하는 편이 좋습니다. CSS, JavaScript, 이미지처럼 변하지 않는 파일은 웹 서버, CDN, 또는 정적 파일 전용 미들웨어가 처리하는 것이 더 적합합니다. 파일 요청은 많고 단순하며, 캐시를 적극적으로 사용할 수 있기 때문입니다.

배포 전에 보통 다음 명령을 실행합니다.

python manage.py collectstatic

이 명령은 여러 앱의 static/ 폴더와 STATICFILES_DIRS에 있는 파일을 STATIC_ROOT로 모읍니다. 그다음 Nginx 같은 웹 서버가 STATIC_ROOT 폴더를 /static/ URL로 제공하거나, WhiteNoise 같은 도구가 정적 파일 응답을 맡을 수 있습니다.2

정리하면 다음과 같습니다.

  • 개발 중: Django 개발 서버가 static 파일을 찾아 제공한다.

  • 배포 전: collectstatic으로 static 파일을 STATIC_ROOT에 모은다.

  • 배포 후: 웹 서버, CDN, WhiteNoise 등이 static 파일을 제공한다.

여기서 중요한 점은 collectstatic이 media 파일을 모으는 명령이 아니라는 것입니다. 사용자가 업로드한 파일은 배포 시점에 확정되어 있지 않습니다. 따라서 media 파일은 별도 저장소나 운영 서버의 지속 가능한 디렉터리에 보관해야 합니다.

미디어 파일 설정하기

이제 사용자가 게시글에 이미지를 올릴 수 있게 해 보겠습니다. 먼저 settings.py에 media 설정을 추가합니다.

# config/settings.py

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

MEDIA_URL은 업로드 파일에 접근할 때 사용할 URL 접두어입니다. MEDIA_ROOT는 실제 파일이 저장될 서버의 디렉터리입니다. 예를 들어 이미지가 posts/2026/07/sample.jpg로 저장되면 실제 파일 위치는 대략 다음처럼 됩니다.

media/posts/2026/07/sample.jpg

브라우저에서는 다음 URL로 접근합니다.

/media/posts/2026/07/sample.jpg

개발 환경에서는 media 파일을 직접 제공하도록 urls.py에 설정을 추가해야 합니다.

# config/urls.py

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('boards.urls')),
]

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

이 코드는 개발 중에만 사용해야 합니다. settings.DEBUG가 참일 때만 media URL을 Django가 처리하게 했기 때문입니다. 운영 환경에서는 media 파일도 웹 서버나 별도 파일 저장소가 제공해야 합니다.

ImageField와 FileField 사용하기

Django 모델에서 업로드 파일을 다룰 때는 FileField나 ImageField를 사용합니다. FileField는 일반 파일을 저장할 때 사용하고, ImageField는 이미지 파일을 다룰 때 사용합니다. ImageField를 사용하려면 이미지 처리를 위해 Pillow 패키지가 필요합니다.

pip install Pillow

게시글에 대표 이미지를 추가해 보겠습니다.

# boards/models.py

from django.conf import settings
from django.db import models

class Post(models.Model):
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='posts',
    )
    title = models.CharField(max_length=100)
    content = models.TextField()
    image = models.ImageField(upload_to='posts/%Y/%m/', blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

upload_to='posts/%Y/%m/'는 업로드 파일을 연도와 월 기준 폴더에 나누어 저장하겠다는 뜻입니다. 예를 들어 2026년 7월에 올린 파일은 media/posts/2026/07/ 아래에 저장됩니다.

여기서 데이터베이스에 이미지 파일 자체가 들어가는 것은 아닙니다. 데이터베이스에는 posts/2026/07/photo.jpg 같은 파일 경로가 저장되고, 실제 파일은 MEDIA_ROOT 아래에 저장됩니다.3

모델을 바꿨으므로 마이그레이션을 실행합니다.

python manage.py makemigrations
python manage.py migrate

그리고 ModelForm에 이미지 필드를 포함합니다.

# boards/forms.py

from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content', 'image']

업로드 폼의 템플릿에서는 반드시 enctype="multipart/form-data"를 지정해야 합니다.

<form method="post" enctype="multipart/form-data">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">저장</button>
</form>

이 속성이 없으면 브라우저가 파일 내용을 서버로 보내지 않습니다. 제목과 본문은 전송되는데 이미지만 비어 있다면 가장 먼저 이 부분을 확인해야 합니다.

함수 기반 View를 사용한다면 폼을 만들 때 request.FILES도 함께 넘겨야 합니다.

form = PostForm(request.POST, request.FILES)

이전 장에서 다룬 CreateView, UpdateView 같은 제네릭 View를 사용한다면 일반적인 폼 처리 흐름에서 업로드 파일도 함께 전달됩니다. 그래도 템플릿의 enctype은 직접 작성해야 합니다.

업로드한 이미지 표시하기

상세 화면에서는 업로드된 이미지의 URL을 사용해 표시합니다.

<h2>{{ post.title }}</h2>

{% if post.image %}
  <img src="{{ post.image.url }}" alt="{{ post.title }} 이미지">
{% endif %}

<p>{{ post.content }}</p>

post.image.url은 MEDIA_URL과 저장된 파일 경로를 조합해 URL을 만듭니다. 다만 이미지가 없는 게시글에서 바로 .url을 호출하면 오류가 날 수 있으므로 {% if post.image %}로 먼저 확인하는 습관이 좋습니다.

업로드 파일을 제한하기

파일 업로드 기능은 편리하지만 위험도 함께 생깁니다. 사용자가 어떤 파일을 얼마나 크게 올릴지 알 수 없기 때문입니다. 따라서 최소한 다음 세 가지는 제한해야 합니다.

첫째, 허용할 확장자를 제한합니다. 둘째, 파일 크기를 제한합니다. 셋째, 저장 경로를 예측 가능하고 안전하게 관리합니다.

예를 들어 게시글 이미지는 JPG, PNG, WebP만 허용하고 2MB 이하로 제한할 수 있습니다.

# boards/validators.py

from django.core.exceptions import ValidationError

def validate_image_size(file):
    max_size = 2 * 1024 * 1024
    if file.size > max_size:
        raise ValidationError('이미지는 2MB 이하만 업로드할 수 있습니다.')

확장자는 Django가 제공하는 FileExtensionValidator를 사용할 수 있습니다. 저장 파일명은 원본 이름을 그대로 쓰기보다 UUID를 사용하면 충돌과 정보 노출을 줄일 수 있습니다.

# boards/models.py

import uuid
from pathlib import Path

from django.conf import settings
from django.core.validators import FileExtensionValidator
from django.db import models

from .validators import validate_image_size


def post_image_path(instance, filename):
    ext = Path(filename).suffix.lower()
    return f'posts/{instance.author_id}/{uuid.uuid4()}{ext}'


class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    content = models.TextField()
    image = models.ImageField(
        upload_to=post_image_path,
        blank=True,
        validators=[
            FileExtensionValidator(['jpg', 'jpeg', 'png', 'webp']),
            validate_image_size,
        ],
    )
    created_at = models.DateTimeField(auto_now_add=True)

upload_to에는 문자열뿐 아니라 함수도 넣을 수 있습니다. 위 예제는 작성자 id별 폴더를 만들고, 파일명은 UUID로 바꿉니다. 사용자가 올린 원본 파일명에 개인정보가 들어 있을 수도 있으므로, 그대로 공개 URL에 노출하지 않는 편이 안전합니다.

위와 같은 validator는 ModelForm이나 관리자 페이지처럼 Django의 폼 검증을 거치는 흐름에서 자연스럽게 실행됩니다. 반대로 코드에서 모델 객체를 직접 만들고 save()만 호출하는 경우에는 검증이 자동으로 실행되지 않을 수 있으니, 그런 흐름에서는 별도 검증을 신경 써야 합니다.

다만 확장자 검사는 완전한 보안 장치가 아닙니다. 파일 이름은 사용자가 바꿀 수 있습니다. ImageField는 이미지 파일인지 확인하는 데 도움을 주지만, 운영 서비스에서는 웹 서버의 업로드 용량 제한, 악성 파일 검사, 비공개 파일 접근 제어까지 함께 고려해야 합니다.

또 하나 기억할 점이 있습니다. 모델 객체를 삭제했다고 해서 서버에 저장된 파일까지 항상 원하는 방식으로 정리되는 것은 아닙니다. 실제 서비스에서는 사용하지 않는 업로드 파일을 정기적으로 삭제하거나, 객체 삭제 시 파일도 함께 삭제하는 정리 정책을 따로 마련해야 합니다.

정리

static과 media는 모두 파일이지만 목적이 다릅니다. static은 개발자가 준비한 화면 자산이고, media는 사용자가 서비스 이용 중 올린 파일입니다.

정적 파일은 STATIC_URL, STATICFILES_DIRS, {% static %} 태그로 연결하고, 배포 시에는 collectstatic으로 모아 웹 서버나 CDN이 제공하게 합니다. 업로드 파일은 MEDIA_URL, MEDIA_ROOT, ImageField, FileField로 다루며, 개발 환경에서는 urls.py에 media 제공 설정을 추가합니다.

파일 업로드 기능을 만들 때는 저장되는지만 확인하고 끝내면 안 됩니다. 어떤 파일을 허용할지, 얼마나 큰 파일까지 받을지, 어디에 어떤 이름으로 저장할지까지 정해야 운영 중 문제가 줄어듭니다. 화면의 완성도는 static에서 나오고, 사용자 참여 기능은 media에서 시작됩니다. 두 개념을 구분하면 Django 프로젝트의 파일 관리가 훨씬 명확해집니다.

참고

1How to manage static files - Django documentation

2How to deploy static files - Django documentation

3FileField and FieldFile - Django documentation

책 목록

Django 활용법: 웹 개발 기본기부터 배포와 운영까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기