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을 모아놓은 좀 더 큰 단위입니다.