7. 사진 게시물 제출하여 게시하기

그동안 우리는 사진 게시물을 Django Admin에서 게시했습니다. 이번엔 사진 게시물을 게시하는 기능을 구현하겠습니다. Django Form을 이용할 것인데, 이번 편에서는 왜 Django Form을 쓰면 좋고, 어떻게 동작하는 지 흐름을 이해하는 내용을 다루겠습니다.

1. Django Form

Django Form은 Django의 주요 매력 요소 중 하나라 생각합니다. Django는 MTV 패턴을 따른다고 하는데, Django Model과 Form을 활용하면 반복되는 처리를 Django가 대신 하고 이용자는 데이터(model)와 표현물(template)에 집중하게 됩니다.

Form은 이름에서 드러나듯이 입력 양식(form)을 다루는 기능입니다. “입력 양식”이란 Django가 웹 프레임워크이니 웹 입력 양식을 뜻합니다. Django Form은 HTML로 만든 웹 화면의 form 태그에서 서버로 전달된 항목이 유효한 지 검증(validation)할 뿐만 아니라 웹 입력 항목에 필요한 HTML 태그를 생성해 출력합니다. 유효하지 않은 항목이 있으면 어떻게 유효하지 않은 지 안내말을 출력하기도 합니다.

유효성은 꼼꼼하게 검사(validation)해야 합니다. 보안 측면에서 클라이언트(서비스 이용자)가 서버로 보내오는 데이터는 그다지 신뢰해서는 안 됩니다. 우리 서비스를 위태롭게 할 코드가 숨겨져 있을지도 모릅니다. 운영 측면에서도 이용자가 system이나 admin과 같이 운영자를 사칭하는 계정 이름을 짓거나 화면을 망가뜨리거나 다른 이용자의 권한을 가로채는 서비스 앞단(front-end)용 코드를 심을지도 모릅니다.

보안성을 높이려면 마냥 뚫고 들어오지 못 하게 폐쇄하기만 할 게 아니라 개방할 필요도 있습니다. 개발자 또는 개발팀이 아무리 뛰어난 능력을 가졌어도 갈수록 증가하는 소프트웨어 복잡성에서 발생하는 수많은 경우와 상황에 대응하는 건 불가능합니다. 또한 서비스에 구현된 모든 기능을 완전히 직접 구현하여 제공하는 것이 아닌 이상 우리가 만드는 소프트웨어는 다른 소프트웨어나 도구와 연결되는데, 우리가 만든 소프트웨어의 바깥 환경이 변하면서 우리가 만든 소프트웨어도 녹슬어 끊임없이 새로운 문제에 부딪히게 됩니다. 이 문제는 폐쇄하여 감출 게 아니라 오히려 개방하여 더 드러내서 많은 사람이 문제를 발견하는 게 낫습니다. Django는 오픈소스 프로젝트이며, 많은 개발자가 참여하고 기여하고 사용합니다. Django처럼 애용되고 활성화 된 오픈소스 프로젝트는 사람이 유발하는 보안 구멍을 주시하는 눈이 많아서 우리가 직접 구현하는 것보다 더 신뢰할 만하다고 생각합니다. 우리의 능력이 뛰어나든 그렇지 않든 말이지요.

운영 측면에서 대응해야 할 대응은 반복되는 처리가 많습니다. 예를 들어, 숫자만 입력받을 항목에 숫자 외 다른 글자가 입력되었는지 검사하고 들어 있으면 예외 처리하거나 첨부한 이미지 파일이 제대로 된 파일인지 검사하는 처리는 항목 개수만큼이나 반복되는 과정입니다. 이를 일일이 코드로 검사한다면 실수할 가능성이 큽니다. 일관성과 관리 차원에서 그러한 역할을 하는 검사기(validator)를 만들어 처리하는 게 좋습니다.

이렇게 클라이언트로부터 전송받은 데이터가 유효한지 검사하고 걸러내는 역할을 Django Form가 합니다. Django Form을 사용하면 상당히 다양한 입력 형식에 대해 수 년에 걸쳐 쌓인 경험으로 유효성을 검사합니다. 가령, Django Form의 EmailField 폼 필드를 사용하면, 다국어나 .wiki.google과 같은 새로운 최상위 도메인(Top-level domain), 심지어 IPv4나 IPv6와 같이 IP주소로 구성된 전자우편 주소에 대응 가능합니다.

물론 어디까지나 유효성을 검사하는 것이므로 제가 앞서 언급한 보안성에 대해 무결하지는 않습니다. 예를 들어, Django Form의 ImageField 폼 필드는 클라이언트가 제출한 파일이 이미지 파일로 유효한지 확인하는 방법을 Image Library인 PIL이나 Pillow의 verify()에 의존합니다. verify() 메서드는 파일의 헤더 영역을 읽어 들여서 유효한 파일인지 검사할 뿐입니다. 그 마저도 일부 파일에 대해서만 제공하여, GIF 파일을 처리하는 모듈엔 verify()가 아예 없습니다. GIF, PNG, Jpeg과 같은 이미지 파일은 일반 문자열을 담는 Metadata 영역(chunk)을 지원하는데, 이 요소를 악용하여 보안을 위협하는 코드를 삽입하여 서버나 클라이언트(방문자)에게 해를 끼칠 가능성이 있습니다1.

하지만, 이는 Django Form이 보안에 초점을 맞춘 기능은 아니니 보안 대응용으로 Django Form에 의존하지 않아야 한다는 의미이며, 입력 항목이 유효한 지에 대한 필수 검사 요소는 갖추고 있으므로 Django Form을 가장 기본으로 사용하고 보안에 필요한 조치를 추가하는 것이 나을 것입니다.

Form과 ModelForm

Django Form은 django.forms 모듈에서 FormModelForm 클래스로 제공됩니다. Form은 앞서 설명한 내용을 그대로 담고 있는 클래스입니다. ModelForm은 Django Model과 연계한 Form 클래스입니다. Django Model을 사용한다면 ModelForm을 이용하여 입력 양식과 입력 항목 검증, 그리고 검증된 입력 데이터를 데이터베이스에 저장하는 과정을 편하게 처리합니다.

자세한 건 코드로 구현하면서 다루겠습니다.

2. 사진 게시물을 Form을 이용하여 게시하기

Form 만들기

photos 디렉터리에 forms.py 파일을 만듭니다. 앞으로 photos 앱에서 사용하는 Form은 이 모듈에 만듭니다. 이제 사진 게시물을 편집하는(생성하거나 수정) 폼을 PhotoForm이라는 이름으로 만듭니다.

from __future__ import unicode_literals

from django import forms

from .models import Photo


class PhotoForm(forms.ModelForm):
    class Meta:
        model = Photo
        fields = ('image', 'content', )

아주 간결한 코드입니다. forms 모듈에 있는 ModelForm 클래스를 상속받는 PhotoForm 클래스를 만들면 이 클래스는 Form 클래스입니다. ModelForm이므로 클래스 안에 Meta 클래스를 또 만들고, 그 안에 model = Photo라는 코드로 이 Model form에 연계하는 Model을 Photo로 지정한 것입니다. 이 Photo 모델 클래스는 photos 앱 디렉터리의 models.py 모듈에 있으니 from .models import Photo로 읽어 들인 것입니다. Meta 클래스의 fields 속성은 폼에 사용할 모델의 모델 필드를 지정하는 데 사용합니다. fields = ('image', 'content', ) 이 코드는 Photo 모델의 imagecontent 모델 필드를 폼 필드로 만드는 설정인 셈입니다.

ModelForm은 Form에 연결한 Model의 모델 필드를 기반으로 폼 필드를 만듭니다.

class Photo(models.Model):
    image = models.ImageField(upload_to='%Y/%m/%d/orig')
    filtered_image = models.ImageField(upload_to='%Y/%m/%d/filtered')
    content = models.TextField(max_length=500, null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

Photo 모델이 이와 같은 모델 필드로 구성되어 있으니 PhotoForm을 일반 Form 클래스를 상속받아 만든다면 다음과 같이 만드는 셈입니다.

class PhotoForm(forms.Form):
    image = forms.ImageField()
    filtered_image = forms.ImageField()
    content = forms.CharField(
        max_length=500,
        required=False,
        widget=forms.Textarea
    )
    created_at = forms.DateTimeField(required=False)

Model과 비슷하게 생겼습니다. 웹페이지에 사용할 HTML도 거의 비슷합니다.

Model은 데이터베이스와 연관되어 있어서 모델 필드형(type)이 데이터베이스의 컬럼(column)형(type)에 맞추어져 있고, Form은 웹 입력 양식인 form 관련 태그의 종류에 맞추어져 소소한 차이가 있지만, 결국 웹에서 넘겨받은 데이터를 데이터베이스에 넣는 것이라 서로 비슷한 인터페이스를 갖습니다. ModelForm을 쓰면 모델 필드와 폼 필드 간 차이 마저도 별로 의식하지 않습니다. 그래서 Model을 잘 만들고 ModelForm을 이용하여 Model form을 만들면 우리는 데이터 유효성을 검사하고 이를 데이터베이스에 넣거나 찾아 쓰는 데이터 관리와 처리를 날로 먹게 됩니다.

사진 게시물 작성 화면 만들기

사진 게시물을 게시하려면 사진 파일을 선택하고, 사진을 설명하는 본문 등 사진 게시물에 필요한 사항을 입력해야 합니다. 본 강좌 중 Pystagram 기획편에서 사진 게시물을 작성하고 게시하는 URL을 /photos/upload/로 하기로 했으니 urls.py에 이 주소 패턴을 등록합니다. 시작 패키지(settings.py 파일이 있는 디렉터리)에 있는 urls.py 파일을 열고 다음 URL 패턴을 추가합니다.

# 생략 
from photos.views import hello
from photos.views import detail
from photos.views import create  # 이 줄 추가


urlpatterns = [
    url(r'^hello/$', hello),
    url(r'^photos/(?P<pk>[0-9]+)/$', detail, name='detail'),
    url(r'^photos/upload/$', create, name='create'),  # 이 줄 추가
    url(r'^admin/', admin.site.urls),
]
# 생략

/photos/upload/ URL에 photos 앱 디렉터리에 있는 views.py 모듈의 create 뷰 함수를 연결(mapping)한 것입니다.

이번엔 views.py 파일에 create 뷰 함수를 만듭니다.

# 생략
from .forms import PhotoForm


def create(request):
    form = PhotoForm()
    ctx = {
        'form': form,
    }
    return render(request, 'edit.html', ctx)

edit.html 템플릿 파일에 템플릿 맥락 요소(Context)로 앞서 만든 PhotoForm 클래스 객체를 전달하는데, 폼 클래스 자체가 아니라 폼 클래스를 인스턴스 객체로 생성하여 form에 할당하고, 이 form을 전달합니다.

render() 함수는 5. url에 view 함수 연결해서 사진 출력하기 편에서 역할을 설명했고, 이번 편에서 처음 사용합니다. 이 함수는 대개 세 가지 인자를 필요로 합니다.

  • request
  • 템플릿 파일 이름
  • 사전형(dict) 객체로 전달되는 템플릿 맥락 요소(context)

request 객체는 뷰 함수에 첫 번째 인자로 전달되는 객체입니다. HTTP Request를 뜻합니다. 뷰 함수는 언제나 첫 번째 인자로 request 객체를 전달 받는데, 이 객체를 render() 함수의 첫 번째 인자로 전달합니다. 템플릿에서 템플릿 맥락 요소로 request 객체를 지정하는(mapping) 데 사용됩니다. 두 번째 인자는 템플릿 파일 경로를 문자열로 지정하며, 이 인자 역시 필수 인자입니다. 마지막으로, 세 번째 인자는 템플릿 파일 안에서 사용할 템플릿 맥락 요소를 사전형(dict) 객체로 전달합니다. {'form': form}에서 Key'form'은 템플릿 파일 안에서 form이라는 이름으로 사용하는 템플릿 변수가 되고, Valueform(PhotoForm()의 인스턴스 객체)가 이 템플릿 변수에 연결된(mapped) 객체인 셈이지요.

이번엔 템플릿 파일인 edit.html을 만듭니다. photo 디렉터리에 templates 디렉터리를 만들고, 그 안에 edit.html 파일을 만들어 다음 내용을 담습니다.

{% extends 'layout.html' %}

{% block content %}
<form
    method="POST"
    action=""
    enctype="multipart/form-data"
>
    {% csrf_token %}
    {{ form.as_p }}

    <p>
        <button type="submit">저장</button>
    </p>
</form>

{% endblock %}

Django Template은 추후 연재에서 자세히 다루겠습니다. 이 edit.html는 뷰 함수에서 지정한 템플릿 파일이니 이후엔 뷰 템플릿 파일이라 부르겠습니다. 템플릿 내용 중 눈여겨 볼 점은 {{ form.as_p }} 코드입니다. formcreate 뷰 함수가 form 폼 객체를 form이라는 템플릿 변수로 지정해 전달한 것입니다. 이 객체의 인스턴스 메서드인 as_p()를 호출하면 각 폼 필드를 HTML 태그인 <p></p>(paragraph, 문단 태그)로 감싸서 출력합니다. 실제로 출력되는 HTML 코드는 다음과 같습니다.

<p><label for="id_image">Image file:</label> <input id="id_image" name="image" type="file" /></p>

<p><label for="id_content">Content:</label> <textarea cols="40" id="id_content" maxlength="500" name="content" rows="10">
</textarea></p>

Photo 모델에 있는 모델 필드 중 두 개가 HTML form 입력항목 태그로 표현 되었습니다. created_at은 없는데, 날짜나 시간 관련 모델 필드(DateTimeField, DateField, TimeField)에 auto_now_addauto_now 필드 옵션 중 하나라도 True로 지정되면 Model form으로 폼 필드를 만들 때 기본 입력 항목으로 지정되지 않고, 그래서 HTML 태그로도 만들어 내지 않습니다.

{% csrf_token %}CSRF(Cross Site Request Forgery) 토큰을 만드는 템플릿 태그입니다. Django로 만든 웹 페이지에 접속하면 각 세션을 기반으로 CSRF 토큰을 만들며, 이 토큰이 조작되거나 존재하지 않으면 Form 데이터를 Django로 동작하는 웹 애플리케이션 서버에 보내지 못합니다2. CSRF 토큰 검사를 하지 않도록 하면 되지만, 보안 상 좋지 않으니 HTML 폼 영역에 CSRF 토큰을 생성하도록 {% csrf_token %}을 습관처럼 넣길 권합니다. 빠뜨리면 CSRF 검증을 실패하였다는 오류가 발생합니다.

뷰 템플릿 파일인 edit.html는 레이아웃 구조를 잡는 역할을 하는 layout.html 템플릿 파일로 확장하므로({% extends 'layout.html' %}) 이 layout.html 파일도 만들어야 합니다.

{% load staticfiles %}

<!DOCTYPE html>
<html lang="ko">

<head>
    <title>{% block page_title %}Pystagram{% endblock %}</title>
    <meta charset="utf-8">
    <script type="text/javascript" src="{% static 'js/jquery-2.1.3.min.js' %}"></script>
</head>

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

</html>

지난 6회 연재 글에서 다룬 정적(static) 파일 내용이 얼핏 보이네요. 이것도 Django Template을 다루는 연재 글에서 자세히 다루겠습니다.

이제 Django의 개발용 내장 웹 서버를 구동하고(python manage.py runserver) /photo/upload/ URL로 접속하면 사진 게시물을 작성하는 편집 화면이 나옵니다. filtered_image은 이미지 필터를 적용하여 가공된 이미지 파일을 담는 모델 필드입니다. 다시 말하면 사진 게시물을 편집하는 단계에서 이용자가 접근해서는 안 되는 필드입니다. 그래서 화면에 나타나지 않게 감추었습니다. 앞서 모델폼을 만들 때 fields 속성에서 누락한 탓입니다.

fields는 폼에서 사용할 모델 필드를 지정하는 데 사용하며, 모델 필드 이름을 문자열로 리스트(list)나 튜플(tuple) 객체에 나열해 담으면 됩니다. 그런데 폼 필드로 사용하지 않을 모델 필드는 filtered_image 하나이고, 사용할 모델 필드는 221개쯤 있다고 가정하겠습니다. 고작 하나를 사용하지 않으려고 221개 모델 필드 이름을 나열하면 무척 고통스럽습니다. 이런 경우는 사용하지 않을 모델 필드만 지정해야 편한데, exclude에 지정하면 됩니다.

class PhotoForm(forms.ModelForm):
    class Meta:
        model = Photo
        exclude = ('filtered_image', )

이 코드에서 유의할 점은 exclude 역시 리스트나 튜플 객체를 할당해야 하므로 'filtered_image' 뒤에 쉼표 하나 더 찍어줘야 합니다3.

photos 앱 디렉터리에 templates 디렉터리를 만들고 그곳에 템플릿 파일을 담으면 Django가 알아서 앱 디렉터리에 있는 템플릿 파일을 가져옵니다. 이 동작은 settings.py에 따로 설정되어 있어서 그렇습니다.

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates'), ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

settings.pyTEMPLATES 항목을 보면 'APP_DIRS': True, 코드가 있는데, 이 부분이 바로 Django 앱 별로 템플릿 파일을 다루도록 할 것인지 여부를 지정한 것이며 앱 안에 위치하는 템플릿 디렉터리 이름은 templates로 고정되어 있습니다. False로 바꾸면 앱 디렉터리에 있는 템플릿 파일을 다루지 않습니다.

사진 게시물 게시하기

우리는 사진 게시물 내용을 작성하는 URL과 사진 게시물을 제출하여 게시하는 URL을 같이 쓰겠습니다. 즉, /photo/upload/에 HTTP Get 방식으로 접근하면 사진 게시물을 작성하는 화면이 나오고, POST 방식으로 접근하면 게시물을 제출합니다.

from django.shortcuts import redirect


def create(request):
    if request.method == "GET":
        form = PhotoForm()
    elif request.method == "POST":
        form = PhotoForm(request.POST, request.FILES)

        if form.is_valid():
            obj = form.save()
            return redirect(obj)

    ctx = {
        'form': form,
    }

    return render(request, 'edit.html', ctx)

request.methodGET인 경우는 기존 코드를 그대로 사용하면 됩니다. POST 방식, 즉, 게시물 내용과 파일을 제출 받는 부분을 추가했습니다.

PhotoForm(request.POST, request.FILES)

PhotoForm 폼에 첫 번째 인자로 request.POST를, 두 번째 인자로 request.FILES를 전달합니다. 첫 번째 인자는 폼에서 다룰 데이터를 뜻하며, 사전형(dict) 객체나 사전형 객체처럼 동작하는(비슷한 인터페이스를 제공하는) 객체4여야 합니다. 파일을 제외한 HTML Form에서 POST 방식으로 전송해온 모든 formdata 데이터가 request.POST에 있습니다. 파일은 request.FILES에 있습니다. 그래서, 이 둘을 분리하여 첫 번째 인자, 두 번째 인자로 전달한 것입니다.

여기까지는 폼에서 처리할 데이터를 인자로 전달하여 설정한 것일 뿐이므로, form = PhotoForm()와 다를 바 없습니다. 그렇다고 해서 다음과 같이 코드를 작성해서는 안 됩니다.

    form = PhotoForm()
    if request.method == "POST":
        form.data = request.POST
        form.files = request.FILES

        if form.is_valid():
            obj = form.save()

Django Form은 첫 번째 인자로 넘어온 데이터는 data 멤버에, 파일은 files 멤버에 할당하는 걸 이용한 것인데, Form 클래스로 인스턴스 객체를 생성해 할당하는 과정에서(__init__()) 인자로 전달된 데이터나 파일이 있으면 is_bound라는 멤버에 True가 할당되고, 이 is_boundTrue여야만 is_valid()를 비롯한 폼 검사를 수행하기 때문입니다. 물론,

    if request.method == "POST":
        form.is_bound = True
        form.data = request.POST
        form.files = request.FILES

이렇게 is_bound를 직접 True로 할당하면 되지만, form = PhotoForm(request.POST, request.FILES)라고 코드를 짜면 그만인 것을 굳이 저렇게 짤 필요는 없습니다.

폼에 검사할 데이터를 전달하여 초기화하여 인스턴스 객체(form)에는 전달된 데이터를 검사하는 몇 가지 인스턴스 메서드를 제공합니다. full_clean()clean() 메서드가 폼 데이터를 검사하는 데 사용하는 메서드인데, 실제로는 is_valid() 메서드를 사용하면 됩니다. is_valid() 메서드는 폼에 전달된 데이터를 폼 필드를 기준으로 검사하여 모든 데이터가 유효하면 True를, 하나라도 유효하지 않은 항목이 있으면 False를 반환합니다. 동작은 다음과 같습니다.

  1. is_valid() : 폼 검사와 관련된 오류(error)가 있는 지 검사.
  2. full_clean() : _clean_fields(), _clean_form(), _post_clean() 메서드를 차례대로 수행하여 폼 데이터 유효성을 검사.
  3. 최종 : is_valid()는 오류(errors)가 없으면 True를 반환하고, 있으면 데이터가 유효하지 않아 False를 반환하며, 어떤 항목에 문제가 유효하지 않은 지 여부는 폼 인스턴스 객체의 errors 멤버(프로퍼티)에 사전형 객체처럼 생긴 ErrorDict의 인스턴스 객체로 할당.

데이터가 모두 유효하면 PhotoForm 폼의 인스턴스 객체인 formsave() 메서드를 실행하고, 이 메서드는 연결된 모델을 이용하여 데이터를 저장합니다. save() 메서드는 ModelForm 클래스에 있는 메서드인데, 모델 폼에 연결한 모델을 이용하여 데이터를 저장하고 저장한 모델의 인스턴스 객체를 반환합니다. PhotoFormPhoto 모델을 연결하였으므로 Photo 모델로 생성한 인스턴스 객체를 반환하는 셈이지요.

return redirect(obj) 에서 redirect() 함수는 HTTP Response를 반환하는 Django의 HttpResponseRedirect 클래스를 이용하여 클라이언트를 지정한 URL로 이동(redirect)시킵니다. render() 함수처럼 몇 가지 절차를 간편하게 줄여준 함수이며, django.shortcuts 모듈에 있습니다.

redirect(obj)에서 눈 여겨 볼 부분은 바로 obj입니다. obj 변수엔 Photo 모델의 인스턴스 객체가 연결되어 있습니다. redirect 함수는 전달된 객체가 모델의 인스턴스 객체인 경우 그 객체의 get_absolute_url() 메서드를 호출합니다. 음, 우리는 Photo 모델에 get_absolute_url() 인스턴스 메서드를 만든 적이 없습니다. 먼저 만들고 설명하겠습니다. photo 앱 디렉터리 안에 있는 models.py에서 Photo 모델 클래스에 다음 코드를 추가합니다.

from django.core.urlresolvers import reverse_lazy


class Photo(models.Model):
    # 중략

    def get_absolute_url(self):
        url = reverse_lazy('detail', kwargs={'pk': self.pk})
        return url

Django Model의 get_absolute_url() 메서드는 모델의 개별 데이터에 접근하는 URL을 문자열로 반환합니다. 우리는 개별 사진을 보는 URL을 /photos/사진ID/ 패턴으로 제공하므로, 2번 사진은 /photos/2/, 1023번 사진은 /photos/1023/ URL로 접근합니다. 각 사진의 데이터는 Photo 모델에 존재하며, 사진 데이터란 모델 클래스의 인스턴스 객체이므로 모델 클래스에 인스턴스 메서드로 get_absolute_url()를 만드는 것입니다.

get_absolute_url라는 이름을 반드시 따를 필요는 없으며 없어도 무방합니다. permalink()라는 이름으로 메서드를 만들어도 무방합니다. 다만, get_absolute_url는 Django가 개별 모델 데이터의 URL을 제공하는 메서드라고 전제해 놓은 관례(conventional) 이름이어서 Django가 알아서 처리하는 감춰진 동작5에 사용됩니다. 이런 관례(convention)를 따르면 일일이 지정하고 설정하지 않아도 되어 코드가 간결해집니다.

reverse_lazy()는 나중에 좀 더 자세히 다루기로 하고, 이번 편에서는 urls.py'create' 이름으로 등록한 URL 패턴에 키워드 인자인 pk의 값으로 self.pk를 할당하여 URL 문자열을 가져오는 데 사용했다고 이해하고 넘어가겠습니다.

사진 게시물을 게시하는 기능을 구현했습니다. 실제로 올려보세요. 잘 게시됩니다.

유효하지 않은 폼 항목 오류 출력하기

혹시 사진으로 이미지 파일을 첨부하지 않거나 이미지 파일이 아닌 파일을 첨부하여 게시물을 첨부해 보셨나요? 강좌 소스 코드대로 잘 따라 오셨다면, 오류 내용이 안내됩니다. 우리는 템플릿 파일 어디에도 폼 오류 안내말을 출력하지 않았는데, 이게 어떻게 된 일일까요? 우리가 템플릿 파일에 폼 관련 내용을 담은 건 고작 한 줄 뿐입니다.

    {{ form.as_p }}

as_p로 폼 내용을 HTML로 출력하려 하면, 폼 항목에 오류가 있는 지, 즉, 폼 인스턴스 객체의 errors 속성에 내용이 있는 지 확인하고, 있다면 오류 내용을 출력합니다. {{ form.as_p }}를 풀어쓰면 다음과 같습니다.

    {% for field in form %}
    <p>
        {% if field.errors %}
        {{ field.errors }}
        {% endif %}

        {{ field }}
    </p>
    {% endfor %}

form 템플릿 변수(views.py에서는 form 객체)는 for문으로 순환 가능합니다. 순환하면 폼에 등록된 폼 필드 순서대로 하나씩 폼 필드 객체를 꺼냅니다. 이 필드 객체를 출력하려 하면 이 필드가 생성하는 HTML 내용을 반환하는데, 이 필드 객체에 오류가 있는 경우, 오류 내용이 필드 객체의 errors에 할당됩니다. 한 폼 필드에 오류 내용은 한 개 이상인 경우도 생기므로 순서열 객체(list)에 오류가 하나씩 할당됩니다. {{ field.errors }} 마저도 더 풀어쓰면 다음과 같습니다.

        {% if field.errors %}
        <ul>
            {% for error in field.errors %}
            <li>{{error}}</li>
            {% endfor %}
        </ul>
        {% endif %}

폼 필드를 직접 명시하여 오류를 확인하는 방법도 있습니다. 예를 들어, 이미지 파일 필드인 image에 오류가 있는 지 확인하는 방법은 다음과 같습니다.

{% if form.errors.image %}
    {{form.errors.image}}
{% endif %}

또는

{% if form.image.errors %}
    {{form.image.errors}}
{% endif %}

대개는 Django Form이 자동으로 만들어주는 폼 항목 구성을 그대로 사용하진 않습니다. 각 폼 항목에 CSS나 HTML 속성을 다르게 부여하는데, Django 애플리케이션 개발자가 고치지 않고 Front-end 개발자가 수정하는 경우도 있습니다. 그래서 폼 필드를 구성하는 요소(레이블, 오류, 폼 필드 자체)를 분리해서 위와 같이 다루는 경우가 흔하고, 오히려 {{ form.as_p }}와 같이 Django에서 만들어내는 HTML 그대로를 사용하는 경우가 드뭅니다.


강좌 7편을 마칩니다. 이번 편에서 다룬 Django Form이 동작하는 큰 흐름을 이해하면 앞으로 다룰 Form 세부 요소를 이해하기 쉽습니다.


  1. Encoding Web Shells in PNG IDAT chunks 글이나 Malware Hidden Inside JPG EXIF Headers 글 참조. 

  2. Django와 Rails에서 CSRF Token의 동작 방식이라는 글을 참조하세요. 

  3. 쉼표를 빼서 ('filtered_image')로 표기하면 그냥 문자열 객체가 됩니다. 리스트 객체를 만드는 데 대괄호를([]) 사용하고 튜플 객체를 만드는 데 소괄호(())를 활용해서 헷갈리기 일쑤인데, 튜플을 만드는 데에 필요한 건 괄호가 아니라 쉼표(,)입니다. 왜냐하면 쉼표로 항목을 구분하여 나열하며, 괄호는 명시적으로 생략 가능하기 때문입니다. 단, 예외로 아무 항목이 없는 빈 튜플을 만드는 경우엔 그냥 소괄호로 짝지으면 됩니다. 자세한 내용은 공식 문서의 Tuples를 참조하세요. 

  4. 사전형 객체처럼 생긴 이런 객체를 인스턴스로 만드는 데 사용하는 클래스(type)도 dict를 상속받아서 만들어서 dict형이 제공하는 인터페이스를 포함합니다. 

  5. “magic”이라는 표현을 씁니다. 뭔가 알아서 수행되는데, 이용자(개발자)가 굳이 알 필요가 없는 내부에 감춰진 동작을 뜻하지요. 


6. Django 정적 파일 기능 이해하기

지난 5회에서 다룬 정적 파일을 Django에서 어떻게 다루는지 자세히 알아 보겠습니다.

1. Django와 정적 파일

웹 서버와 웹 애플리케이션, 그리고 정적 파일

웹 게시판이나 블로그, 또는 우리가 만들 Pystagram은 웹 프로그램 또는 웹 애플리케이션입니다. 이런 웹 애플리케이션이 필요한 이유는 뭘까요?

웹 서버는 웹 클라이언트가 특정 위치에(URL) 있는 서버 저장소(storage)에 있는 자원(resource)을 요청(HTTP request) 받아서 제공(serving)하는 응답(HTTP response) 처리가 기본 동작입니다. 이러한 기본 동작은 자원과 접근 가능한 주소가 정적으로 연결된 관계입니다. PC 스토리지의 /Users/hannal/Pictures/private_photo.png 경로에 사진 파일이 있다고 예를 들면 파일 경로는 웹 주소이고 사진 파일은 자원입니다. 사진 파일을 읽어 들여 보거나 수정하거나 지우는 행위는 HTTP method(GET, POST, PUT, DELETE 등)로 표현합니다. 정리하면 웹 서버는 요청받은 URL과 방식으로 서버에 존재하는 자원을 제공하며, 이 동작을 정적 자원(static resource)을 제공하는 것입니다.

그런데 사진 파일 자체를 제공하는 데 그치지 않고, 사진에 설명도 달고 댓글도 단다면 자원(사진, 본문, 댓글 등)을 정적으로 제공하는 건 그다지 효율성이 좋지 않습니다. 본문을 수정하거나 댓글을 단다는 건 내용물이 고정되어 있지 않고 언제든지 변하는 상황인데, 언제든지 가변하는 내용물을 고정된 자원으로 제공하려면 내용물이 바뀔 때마다 고정된 자원도 매번 바꿔서 정적인 상태로 만들어야 합니다1.

가변하는 자원을 운용하려면 동적으로 자원을 처리하는 기능을 구현해야 하는데, 웹 서버에 이러한 기능을 추가하는 건 그리 좋은 생각은 아닙니다. 웹 서버는 대부분 C나 C++ 언어로 작성되어 있고, 동적인 웹 자원을 다루는 처리는 대부분 문자열을 가공하는 과정입니다. C나 C++ 언어로 문자열을 다루는 건 불편할 뿐더러 웹 서버에 동적 자원을 다루는 기능을 직접 탑재하는 것도 까다롭습니다. 문자열 가공을 더 쉽게 다루는 다른 언어(Python, Perl 등)로 동적 자원을 처리하는 별도 웹 서버 애플리케이션을 웹 서버와 분리해서 만들고 관리하는 게 낫습니다. 그리고 웹 애플리케이션과 웹 서버가 통신하는 인터페이스를 중간에 두어 서로를 연결합니다.

Django는 정적 파일을 제공하는 실 서비스용 기능을 제공하지 않는다

Django는 실 서비스 환경에서 사용할 정적 파일을 제공하는 기능을 제공하지 않습니다. 서버에 저장된 정적 파일을 읽어들여서 그대로 웹 클라이언트에 보내기만 하면 그만인 단순한 기능인데도 Django는 그런 기능을 제공하지 않습니다. 왜냐하면 그럴 필요가 없기 때문인데, 앞서 설명한 바와 같이 정적 파일을 제공하는 건 웹 서버 전문 영역이기 때문입니다.

게다가 웹 애플리케이션은 웹 서버와 연결하는 중간 인터페이스를 거치므로 효율이 더 떨어집니다.

하지만 개발 상황인 경우는 효율보다는 기능(역할)이 중요한 경우가 많습니다. 정적 파일이 제대로 제공되는지 확인하려고 항상 웹 서버를 구동할 필요는 없습니다. Django는 개발 단계에서 쓸 정적 파일 제공 기능을 제공합니다. 성능은 웹 서버가 직접 정적 파일을 제공하는 것 보다 떨어지지만 정적 파일 제공에 필요한 기능은 대부분 지원합니다.

Static file과 Media file

Django은 정적 파일을 크게 두 종류로 구분합니다.

Static file은 Javascript, CSS, Image 파일처럼 웹 서비스에서 사용하려고 미리 준비해 놓은 정적 파일입니다. 파일 자체가 고정되어 있고, 서비스 중에도 수시로 추가되거나 변경되지 않고 고정되어 있습니다.

Media file은 이용자가 웹에서 올리는(upload) 파일입니다. 파일 자체는 고정되어 이지만, 언제 어떤 파일이 정적 파일로 제공되고 준비되는지 예측할 수 없습니다.

Static file과 Media file은 정적 파일이라는 점에서는 같지만, 정적 파일을 제공하는 상황을 예측할 수 있는지 여부는 다릅니다. Static file은 서비스에 필요한 정적 파일을 미리 준비해놓기 때문에 manage.py 도구에 findstaticcollectstatic이라는 기능으로 정적 파일을 모으고 찾는 관리 기능을 제공합니다. manage.py은 Django 프로젝트를 관리하는 일에 필요한 기능을 명령줄 쉘(shell)에서 수행하는 도구입니다. 그에 반해 Media file은 이용자가 웹에서 올리는 파일이므로 미리 예측해서 준비할 수 없습니다. 그래서 Static file 관련된 관리 기능인 findstaticcollectstatic 기능을 사용하지 못합니다.

2. Static file

Static file은 웹 서비스에 사용할 정적 파일을 미리 준비하여 제공하는 데 사용합니다. Django로 운영되는 프로젝트의 설정을 관리하는 settings.py에 Static file와 관련된 항목이 다섯 가지 존재하며, 보통은 다음 세 가지를 사용합니다.

  • STATICFILES_DIRS
  • STATIC_URL
  • STATIC_ROOT

STATICFILES_DIRS

STATICFILES_DIRS은 개발 단계에서 사용하는 정적 파일이 위치한 경로들을 지정하는 설정 항목입니다. 특정 Django App2에만 사용하는 정적 파일이 있거나 혹은 정적 파일을 관리하기 용이하게 하기 위해 여러 경로(path)에 정적 파일을 배치하였다면, 이 경로들을 Python의 listtuple로 담으면 됩니다.

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

대개는 static이라는 디렉터리에 정적 파일을 담습니다. 주의할 점은 정적 디렉터리 경로가 하나이더라도 반드시 listtuple로 담아야 한다는 점입니다. 흔히 하는 실수는 다음과 같이 항목 뒤에 쉼표를 빠뜨리는 것입니다.

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static')
)

이런 경우 Django는 ImproperlyConfigured: Your STATICFILES_DIRS setting is not a tuple or list; perhaps you forgot a trailing comma?라는 경고를 출력하며 정적 파일을 제대로 제공(serving)하지 못합니다.

manage.py에서 제공하는 명령어 중 findstaticSTATICFILES_DIRS에 설정한 경로에서 지정한 정적 파일을 찾습니다. 실습해보지요. jQuery download에서 “Download the compressed, production jQuery x.x.x”로 된 링크를 찾은 뒤 그 링크에 걸려있는 jQuery 파일을 내려 받습니다. 이 강좌를 쓰는 시점에서 저는 2.1.3판을 받았습니다. 이제 manage.py 파일이 있는 경로에 static이라는 이름으로 디렉터리를 만들고, 다시 static 디렉터리 안에 js라는 디렉터리를 만든 다음에 js 디렉터리에 내려 받은 jQuery 파일을 넣습니다. settings.py에는 앞서 나온 예시대로 STATICFILES_DIRS 항목을 추가합니다. 이제 findstatic 명령어로 파일을 찾아 보겠습니다.

$ python manage.py findstatic js/jquery-2.1.3.min.js
Found 'jquery-2.1.3.min.js' here:
  /(중략)/pystagram/static/js/jquery-2.1.3.min.js

물론 $ 기호는 입력하지 않습니다. 쉘의 프롬프트 기호이니까요.

이번엔 STATICFILES_DIRSos.path.join(BASE_DIR, 'static2') 항목을 추가합니다.

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
    os.path.join(BASE_DIR, 'static2'),
)

그런 뒤 manage.py 파일이 있는 경로에 static2 디렉터리를 만들고 이 안에 js 디렉터리를 만들어서 그곳에 jQuery 파일을 복사합니다. 마지막으로 photo 디렉터리(Django photo 앱)에 static 디렉터리를 만들고 이 디렉터리에 jQuery 파일을 복사하고, 또 static 디렉터리 안에 js 디렉터리를 더 만든 뒤 그 안에 jQuery 파일을 복사합니다. 디렉터리 구조는 다음과 같으며, 강좌 연재가 너무 지연되어서 photo 디렉터리가 뭔지 기억이 나지 않는다면 “3. Photo 앱과 모델 만들기”편을 참고하시길 바랍니다. :)

  • .
  • static/
    • js/
  • static2/
    • js/
  • photos/
    • static/
    • static/js/

그런 뒤 다음 세 줄을 실행하여 화면에 나온 결과가 무엇을 의미하는지 고민해 보세요.

$ python manage.py findstatic jquery-2.1.3.min.js
$ python manage.py findstatic js/jquery-2.1.3.min.js
$ python manage.py findstatic javascript/jquery-2.1.3.min.js

충분히 고민하셨으리라 믿습니다. js/jquery-2.1.3.min.js를 찾으려 하면 static 디렉터리에 있는 것과 static2 디렉터리에 있는 것, 그리고 photos/static 디렉터리에 있는 것이 나타납니다. 나타난 순서는 static, static2, photos/static 디렉터리 순인데, 이 배치된 순서는 실제로 정적 파일을 찾아다 사용할 때 우선순위로 작용합니다. 이 우선순위는 STATICFILES_DIRS에 명기된 디렉터리가 더 상위인데, STATICFILES_FINDERS라는 settings.py 설정 항목에서 기본 파일 시스템 파인더(finder)가 Django App 디렉터리보다 상위순위로 지정되어 있기 때문입니다.

이와 같이 정적 파일 경로가 일치할 경우 우선순위에 따라 실제 사용하는 정적 파일이 결정됩니다. 실제 물리 경로는 그대로 유지하지만 우선순위 문제를 겪지 않으려면 접두사(prefix)를 붙여서 구분하면 됩니다. static2는 이제 곧 지울 항목이니까 byebye라는 접두사를 쓰겠습니다.

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
    ('byebye', os.path.join(BASE_DIR, 'static2'),),
)

이 설정을 적용하면 static2 디렉터리가 마치 byebye라는 디렉터리 안에 위치한 것처럼 static2에 있는 정적 파일에 접근해야 합니다. 다음 두 명령을 실행해 보세요.

$ python manage.py findstatic js/jquery-2.1.3.min.js
$ python manage.py findstatic byebye/js/jquery-2.1.3.min.js

STATIC_URL

STATIC_URL은 웹 페이지에서 사용할 정적 파일의 최상위 URL 경로입니다. 이 최상위 경로 자체는 실제 파일이나 디렉터리가 아니며, URL로만 존재하는 단위입니다. 그래서 이용자 마음대로 정해도 무방하며, 저는 assets라는 URL 경로를 쓰겠습니다.

STATIC_URL = '/assets/'

문자열은 반드시 /로 끝나야 합니다. findstatic 명령어로 탐색되는 정적 파일 경로에 STATIC_URL 경로를 합치면 실제 웹에서 접근 가능한 URL이 됩니다.

  • findstatic js/jquery-2.1.3.min.js : http://pystagram.com/assets/js/jquery-2.1.3.min.js
  • findstatic byebyejs/jquery-2.1.3.min.js : http://pystagram.com/assets/byebye/js/jquery-2.1.3.min.js

STATIC_URL은 정적 파일이 실제 위치한 경로를 참조하며, 이 실제 경로는 STATICFILES_DIRS 설정 항목에 지정된 경로가 아닌 STATIC_ROOT 설정 항목에 지정된 경로입니다. 그런데 static2 경로는 byebye 접두사가 붙어서 실제 물리 경로와 다릅니다. 이에 대해선 STATIC_ROOT에서 자세히 다루겠습니다.

STATIC_ROOT

STATIC_ROOT는 Django 프로젝트에서 사용하는 모든 정적 파일을 한 곳에 모아넣는 경로입니다. 한 곳에 모으는 기능은 manage.py 파일의 collectstatic 명령어로 수행합니다. Django가 모든 파일을 검사하여 정적 파일로 사용하는지 여부를 확인한 뒤 모으는 건 아니고, 각 Django 앱 디렉터리에 있는 static 디렉터리와 STATICFILES_DIRS에 지정된 경로에 있는 모든 파일을 모읍니다.

개발 과정에선, 정확히는 settings.pyDEBUGTrue로 설정되어 있으면 STATIC_ROOT 설정은 작용하지 않으며, STATIC_ROOT는 실 서비스 환경을 위한 설정 항목입니다. 그래서 개발 과정에선 STATIC_ROOT에 지정한 경로가 실제로 존재하지 않거나 아예 STATIC_ROOT 설정 항목 자체가 없어도 문제없이 동작합니다.

그렇다면 실 서비스 환경에서 STATIC_ROOT는 왜 필요할까요? 이 경로에 있는 모든 파일을 웹 서버가 직접 제공(serving)하기 위함입니다. 실제 실습하며 확인해 보겠습니다.

settings.py에 다음과 같이 STATIC_ROOT 설정 항목을 추가합니다.

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

listtuple형인 STATICFILES_DIRS와는 달리 문자열 경로를 할당합니다. 이제 collectstatic 명령어로 현 프로젝트가 사용하는 모든 정적 파일을 모읍니다.

$ python manage.py collectstatic

지정한 경로에 있는 기존 파일을 전부 덮어 쓰는데 정말로 모을 거냐고 묻습니다. 원본 파일을 덮어 쓰는 게 아니니 yes라고 입력합니다. 정적 파일을 모을 경로를 manage.py 파일이 있는 경로에 staticfiles 디렉터리로 지정했으므로 이 디렉터리가 만들어지고, 이 안에 사용하는 모든 정적 파일이 복사됩니다. 이 디렉터리 안을 보면 STATICFILES_DIRS에 넣은 경로들 중 byebye라는 접두사를 붙인 디렉터리도 보입니다.

이렇게 정적 파일을 모아놓은 STATIC_ROOT는 Django가 직접 접근하진 않습니다. Django가 접근하여 다루는 설정은 STATICFILES_DIRS이며, STATIC_ROOT는 정적 파일을 직접 제공(serving)할 웹 서버가 접근합니다. collectstatic 명령어를 수행하면 STATICFILES_DIRS나 앱 디렉터리에 있는 static 디렉터리 안에 있는 파일을 STATIC_ROOT에 모으는데, STATICFILES_DIRS에 지정된 경로인 경우 따로 명시한 접두사으로 디렉터리를 만들어 그 안에 파일을 복사하고, 앱 디렉터리에 있는 static 디렉터리인 경우는 앱 이름으로 디렉터리를 만들어 그 안에 static 디렉터리 안에 있는 파일을 복사합니다. 즉, 개발 단계(DEBUG = True)에서는 정적 파일 URL 경로가 논리 개념이고, 서비스 환경(DEBUG = False)에서는 실제 물리 개념인 정적 파일 URL 경로가 되는 것입니다.

그렇다면 경로가 동일해서 우선순위가 발생하는 경우에 collectstatic을 수행하면 어떤 파일이 실제로 복사될까요? 물론 1순위 경로에 위치한 파일이 복사됩니다. photos/js/jquery-2.1.3.min.js 파일을 열어서 내용을 몽땅 지워서 0 byte 파일로 만들고, collected_static 디렉터리를 지운 뒤에 다시 collectstatic 명령어를 실행해 보세요. collected_static 디렉터리 안의 js 디렉터리 안에 있는 jquery-2.1.3.min.js 파일을 보면 0 byte인 photos/js/jquery-2.1.3.min.js이 아닌 정상 파일인 js/jquery-2.1.3.min.js이 복사되어 있습니다.

주의할 점. STATIC_ROOT 경로는 STATICFILES_DIRS 등록된 경로와 같은 경로가 있어서는 안 됩니다. 남들이 잘 안 쓸만한 이상한 이름(staticfiles?)을 쓰세요.

'django.contrib.staticfiles'

개발 단계에서 정적 파일을 제공(serving)하는 기능은 Django에서 제공하는데, 사용 방법은 아주 간단합니다. django.conf.urls.static 모듈에 있는 static 함수를 이용해 URL 패턴을 만들어 urls.pyurlpatterns에 추가하는 것입니다. 지난 5회 강좌분에서 이미 사용한 바로 그 방식입니다. 이 함수를 조금 더 살펴볼까요?

이 함수를 urls.py에서 URL 패턴을 만드는 데 사용한 걸 보면 이 함수 자체가 정적 파일을 제공한다기 보다는 정적 파일 URL에 그런 기능을 하는 무언가를 연결할 것이라 예상되지요? 실제로 그렇게 동작합니다. 정적 파일에 접근할 URL 접두사(staticfiles)를 첫 번째 인자로 넣고 정적 파일이 위치한 경로를 document_root라는 키워드 인자로 전달하면, 이런 내용을 django.views.static.serve라는 뷰 함수가 사용합니다. 이 serve 함수는 서버에 위치한 파일을 읽어서(open(fullpath, 'rb')) 스트리밍 방식으로 응답(StreamingHttpResponse)합니다. 실제 파일 서빙을 하는 것입니다. 물론 성능은 웹 서버가 직접 서빙하는 것보다 떨어지므로 개발 단계에서만 쓰는 게 좋을텐데, django.conf.urls.staticstatic 함수는 settingsDEBUG가 True인 경우에만 이런 정적 파일 제공에 필요한 URL 패턴을 만듭니다. 간단히 말해서 DEBUG=True인 경우에만 static 함수는 우리가 원하고 기대하는 동작을 합니다.

그런데 Static file은 이런 처리를 하지 않아도 개발 단계에서는 잘 제공(serving)됩니다. Media file(업로드 파일)은 urls.pystatic 함수를 사용해 정적 파일을 제공하도록 강제했지만, Static file은 그런 처리를 하지 않아도 저절로 제공(serving)됩니다. 이런 저절로 동작하는 기능은 Django 프레임워크에 내장된 Django App인 'django.contrib.staticfiles'가 맡고 있습니다. settings.py 파일을 열어서 INSTALLED_APPS 항목을 보면 우리가 앞서 추가한 'photos' 외에도 django.contrib으로 시작하는 몇 가지가 더 있는데, 그 중에 'django.contrib.staticfiles'가 있습니다. 'django.contrib.admin' 항목을 보니 지난 강좌에서 사용해 본 Django admin 기능도 Django App이라는 걸 알 수 있습니다.

django.contrib.staticfiles 앱에는 이 앱이 사용하는 URL 패턴을 담은 urls.py 파일이 있는데, 이 파일 내용은 다음과 같습니다.

def staticfiles_urlpatterns(prefix=None):
    """
    Helper function to return a URL pattern for serving static files.
    """
    if prefix is None:
        prefix = settings.STATIC_URL
    return static(prefix, view='django.contrib.staticfiles.views.serve')

# Only append if urlpatterns are empty
if settings.DEBUG and not urlpatterns:
    urlpatterns += staticfiles_urlpatterns()

단순하지요? settings.pySTATIC_URL 항목의 URL에 django.contrib.staticfiles.views.serve 뷰 함수를 연결했는데, 이 내용은 settings.DEBUGTrue인 경우에 반영됩니다.

정리

정리하면, 정적 파일이 있는 경로를 STATICFILES_DIRS에 지정하면 개발 단계에서는 더 신경쓸 게 없습니다.

3. Media file

간단히 설명

Media file은 이용자가 웹에서 업로드한 정적 파일입니다. 미리 준비해놓고 제공하는 Static file과는 달리 언제 어떤 파일이 추가될 지 모르므로 findstaticcollectstatic같은 명령어는 Media file에 대해서는 무의미합니다.

settings.py에 Media file와 관련된 항목이 두 가지 존재합니다.

  • MEDIA_ROOT
  • MEDIA_URL

파일 업로드와 관련하여 세부 조정하는 설정이 몇 가지 더 있지만, 대개는 기본 설정(global_settings)대로 써도 무방합니다.

MEDIA_ROOT는 이름이 STATIC_ROOT과 비슷한데, 업로드가 끝난 파일을 배치할 최상위 경로를 지정하는 설정 항목입니다. STATIC_ROOT보다는 STATICFILES_DIRS이 더 비슷한 역할을 하는데, STATICFILES_DIRS는 Django 1.3판에 새롭게 도입된 설정이자 기능이다 보니 설정 항목 이름을 미처 교통정리하지 못했나 봅니다. MEDIA_ROOTSTATIC_ROOT와 다른 경로를 지정해야 합니다.

MEDIA_URLSTATIC_URL과 이름도 비슷하고 역할도 비슷합니다. /로 끝나는 URL 경로 문자열로 설정해야 한다는 점도 같습니다. MEDIA_URLMEDIA_ROOT와 마찬가지로 STATIC_URL과 URL 경로가 달라야 합니다.

주요 개념을 Static file 영역에서 설명하니 Media file은 간결하게 정리 되는군요.

지난 Media file 관련 코드 수정

지난 5회에서 Media file 관련 설정을 하며 models.py 파일도 고쳤습니다. 이에 대한 내용을 설명하겠습니다.

먼저 settings.py에서 MEDIA_URLMEDIA_ROOT 부분을 추가했습니다.

MEDIA_URL = '/upload_files/'

MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')

Media file에 접근하는 URL이 /upload_files/로 시작하며, 실제 파일이 위치하는 서버 상 경로는 MEDIA_ROOT에 할당했습니다.

다음엔 urls.py에 Media file을 제공(serving)하는 URL 패턴 등록 부분을 고쳤습니다.

urlpatterns += static('/upload_files/', document_root=settings.MEDIA_ROOT)

static 함수에 첫 번째 인자로 Media file URL을, 키워드 인자 document_root로 Media file이 위치한 경로를 전달했습니다.

마지막으로 photos 앱의 models.py 파일에서 이미지 파일이 저장될 경로를 지정한 upload_to 필드 옵션을 고쳤습니다.

class Photo(models.Model):
    image = models.ImageField(upload_to='%Y/%m/%d/orig')
    filtered_image = models.ImageField(upload_to='%Y/%m/%d/filtered')
    # 후략

upload_to='uploads/%Y/%m/%d/orig' 부분에서 uploads/를 떼버린 것입니다. uploads는 이미 MEDIA_ROOT에 지정되어 있으므로 더이상 업로드 경로에 넣을 필요가 없습니다.


강좌 6편을 마칩니다.


  1. 2015년 4월 기준으로 제 블로그는 실제로 이런 방식으로 운영합니다. 그다지 자주 내용물을 고치거나 새로 만들지 않기 때문에 오히려 정적으로 자원을 제공하는 것이 더 효율성 있기 때문입니다. 주 자원은 Github Pages라는 기능을 이용하여 제공하고, 가변하는 내용물인 댓글은 Disqus라는 서비스를 이용하여 본문에서 분리해서 운영합니다. 

  2. Django로 생성한 프로젝트를 Django 프로젝트라 하고, Django 프로젝트는 뭔가를 수행하는 기능 단위인 Django App을 모아놓은 좀 더 큰 단위입니다.