Let’s be honest: No developer wakes up in the morning and thinks, “Oh goody! Today I get to internationalize my giant website with tons of content and files. I bet supporting right-to-left languages is going to be a blast.”
However, I’m here to tell you that it’s not nearly as bad as you would expect.
In fact, Django makes it downright easy to do. Unfortunately, there’s not a lot of information on the web about internationalizing (also known as i18n) in Django besides the official documentation. Hopefully these tips and tricks will be useful for you.
What Django gives you
- Preferred language of the user, and uses the files you generate to serve translated and localized templates.
- Gives you tools for translating strings in both HTML files (i.e. templates) and Javascript files.
- Gives you helpful variables in your templates to help you serve the correct content for left-to-right and right-to-left users.
Step 1: Enabling Localization in Django
Create a folder in your site root’s directory (or elsewhere if you see fit), called locale
. This will contain a folder for each language, as well as the files used for translation themselves.
Open up your settings.py
and include or update the following settings:
# Path to locale folder
LOCALE_PATHS = (
'/path/to/folder/locale',
)
# The language your website is starting in
LANGUAGE_CODE = 'en'
# The languages you are supporting
LANGUAGES = (
('en', 'English'), # You need to include your LANGUAGE_CODE language
('fa', 'Farsi'),
('de', 'German'),
)
# Use internationalization
USE_I18N = True
# Use localization
USE_L10N = True
Also, in each of your views (e.g. in views.py
), you should be setting the request language as a session. For example:
if hasattr(request.user, 'lang'):
request.session['django_language'] = request.user.lang
Step 2: Internationalizing your Django content
This is really the easy part. Chances are, you’ve got a folder in your Django app called “templates”. Inside, you’ve got HTML, some variables, and whatnot. All you have to do is go through and mark up the strings that need to be translated, like so:
{% trans "My English" %}
{% trans myvar %}
You get a lot of flexibility here, as described in the documentation. Essentially what happens is that you label all of your strings that should be translated, and then Django generates a handy file that your translator can use to localize the interface.
Just make sure that at the top of any template you want localized, you actually load the i18n library.
{% load i18n %}
Test it out You only have to translate a string or two in order to see whether it’s working. Create your translation files using the following command:
$ django-admin.py makemessages --locale=de --extension=html --ignore=env --ignore=*.py
Explanation of the options:
--locale=de
Change this from de to whatever locale you’re going for.--extension=html
Tells the django engine only to look for .html files.--ignore=env
In my app, env/ is the folder where my virtual environment exists. I probably don’t want to localize everything that exists in this folder, so we can ignore it.--ignore=*.py
For some reason, django keeps trying to localize some of my python files that exist at the project root. To avoid this, I explicitly ignore such files.
Once you’ve run this django-admin.py
command, you should take a look inside your locale/
directory. If your app exists at something like /opt/app/
, you’ll find a file structure like this:
/opt/app --- /locale ------ /LC_MESSAGES --------- /de ------------ django.po
And within each of these django.po
files, you’ll find pairs of a string, and then a space for a translation, as so:
# path/to/templates/blah.html:123
msgid "My English."
msgstr ""
Obviously, if you’re in /opt/app/locale/LC_MESSAGES/de/django.po
you’d better provide a German translation as a msgstr
.
Now, compile your messages and we’ll see what we get!
$ django-admin.py compilemessages
Next to each django.po
file, you’ll now also have a django.mo
file. This is the binary file that Django actually uses to fetch translations in real time.
Restart uWSGI and your web server.
Add the language you just localized for to your preferred languages in your browser settings, and pull it to first place. In Chrome, this is Preferences » Advanced » Manage Languages.
When you reload your site, you should see that your string has been translated! Anything that you haven’t translate will remain visible in its original language (in my case, English).
Step 3: Translation Javascript (Javascript itself)
Open up your urls.py
. Append the following:
# 'Packages' should include the names of the app or apps you wish to localize
js_info_dict = {
'packages': ('app',)
}
And in your urlpatterns
, include:
url(r'^jsi8n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
Now, in your base template (whichever manages loading your javascript) and place this script first:
http://%%20url%20'django.views.i18n.javascript_catalog'%20%
Now you can go into any javascript file and simply place gettext("")
around any string and that string can be localized. For example:
this.$el.find('.a')[0].attr('title', gettext('Show Resources'));
Generating the Javascript messages file Just as before, when you ran the django-admin.py
command to gather all the strings needing translations in your html templates, you can do the same in your javascript files.
$ django-admin.py makemessages -d djangojs --locale de --ignore=env
Again, specify the locale and ignore the files inside my virtual environment. Now, look at the files you have in your locale/
subdirectories.
/opt/app --- /locale --- /LC_MESSAGES ------ /de --------- django.po --------- django.mo --------- djangojs.po
Simply open up djangojs.po
, translate a string, and run django-admin.py compilemessages
again. You’ll find, as you probably expected, a new file called djangojs.mo
. As before, restart uWSGI and your server, and spin it up in the browser. Again, be sure that you’ve got your test language set as your preferred language in your browser settings.
Step 3b: Translating Javascript Templates (Underscore)
This is where things get a little more interesting. The critical point is this: We want our underscore templates to be served through Django, not through our web server directly (e.g. through Apache or Nginx). These are the steps I took to achieve this:
- Move my underscore templates out of my
static/
folder, and into mytemplates/
folder. - Write a urlpattern that will cause my underscore templates to be run through the django template engine first.
- Update the references to templates in my Javascript (I use RequireJS and the text plugin).
1. Move Underscore Templates Previously, my project structure was something like this:
app/ — static/ —— css/ —— js/ ———- views/ ———- templates/ ————– underscore-template.html — templates/ —— django-template.html
And I had Nginx serving everything inside of static/
, well, directly, using the following directive in my Nginx conf file:
location /static {
alias /opt/app/static;
}
Now, instead of this, I want Django to do its magic before Backbone and Underscore go to town on the templates. So I create a folder inside app/templates/
called js/
. I move all my underscore templates here. So now I have:
app/ --- static/ ------ css/ ------ js/ --------- views/ --- templates/ ------ js/ --------- underscore-template.html --------- django-template.html
2. Write a urlpattern Now, I’m not positive this is the best way to do this, but it does work. Open up your urls.py
and add this line:
url(r'^templates/(?P<path>w+)', 'web.views.static'),
What happens now is that whenever Django receives a request for a URL that looks likemysite.com/templates/some/thing.html, it assigns some/thing.html
to a variable path
, and passes that to our web view. So now I open up app/web/views.py
and append this code:
def static(request, path):
# Update this to use os.path
directory = '/opt/app/' + request.META['REQUEST_URI'];
template = loader.get_template(directory)
# This allows the user to set their language
if hasattr(request.user, 'lang'):
request.session['django_language'] = request.user.lang
# I use this email_hash to generate gravatars, incidentally
context = RequestContext(request, {
'email_hash': hashlib.md5(request.user.email).hexdigest() if request.user.is_authenticated() else ''
})
return HttpResponse(template.render(context))
Now, we’re taking whatever request it was, grabbing that file, and passing it throughtemplate.render
. If needed, add this folder to your settings.py
:
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
'/opt/app/templates/',
'/opt/app/templates/js'
)
Now you can go into any of your underscore template files and mark them up using typical django syntax. Just make sure you remember to include {% load i18n %}
at the top of your underscore templates. For example:
{% load i18n %}
<!-- Page of Greek text for Reader view -->
<!-- Page header -->
<h1><%= work %> <small>{% trans "by" %} <%= author %>{% trans "," %} <a href="#" data-toggle="tooltip" title="{% trans 'Jump to another section' %}">section</a></small></h1>
<hr>
<!-- Greek goes here! -->
<span class="page-content">
<% _.each(words, function(word) { %>
<% if (word.get('sentenceCTS') == cts) { %>
<span lang="<%= word.get('lang') %>" data-cts="<%= word.get('wordCTS') %>" class="<% if (word.get('value').match(/[.,-·/#!$%^&*;:{}=-_`~()]/)) print('punct'); %>"><%= word.get('value') %></span>
<% } %>
<% }); %>
</span>
</div>
In the long run, it may be worth your time to simply switch your html templates purely to Django. However, since the syntax of Underscore and Django don’t clash, it’s a viable solution as far as I’ve experienced.
Once you’ve marked up your underscore templates, simply re-run the same django_admin.py makemessages
command as before.
Just don’t forget to go into your javascript files and change the paths where you’re importing your templates from, so they’re no longer pointing to a static directory. For example:
define(['jquery', 'underscore', 'backbone', 'text!/templates/js/underscore-template.html'], function($, _, Backbone, Template) {
var View = Backbone.View.extend({
tagName: 'div',
template: _.template(Template),
render: function() {
this.$el.html(this.template(this.model));
return this;
}
});
return View;
});
Supporting bidirectional languages
So far, I have had great success with the techniques suggested in this blogpost: RTL CSS with Sass. I’ll just give you a couple of pointers on how to make it easy to implement this with Django.
First, I installed the set_var template tag. This is because I want to use some of the usefulget_language
functions that Django makes available to me. Alternatively, you could probably clean this up by putting this logic in your views.py
.
Then, in my app/templates/base.html
, I make use of this template tag and template inheritance as so:
{% load i18n %}
{% load set_var %}
{% get_current_language_bidi as LANGUAGE_BIDI %}
{% if LANGUAGE_BIDI %}
{% set dir = "rtl" %}
{% else %}
{% set dir = "ltr" %}
{% endif %}
<!DOCTYPE html>
<html dir="{{ dir }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
{% trans "My app" %}
</title>
{% block css %}
<link href="/static/css/{{ css_file }}.{{ dir }}.css" rel="stylesheet">
{% endblock %}
http://%%20url%20'django.views.i18n.javascript_catalog'%20%
/static/js/lib/require.js
var csrf_token = "{{ csrf_token }}"; var locale = "{{ LANGUAGE_CODE }}"; var dir = "{{ dir }}";
</head>
<body>
{% block content %} {% endblock %}
</body>
</html>
What do we have here?
- We’re using Django to get the direction our page is – either ltr or rtl.
- We’re making it possible to replace the CSS file based on the page we’re on and the text direction.
- We make a couple of variables global (eek!) for use in our javascript.
Now, you can take any page which inherits from your base template, and set the css_file. For example:
{% extends "base.html" %}
{# Determine which CSS file to load #}
{% block css %}
{% with 'generic' as css_file %}
{{ block.super }}
{% endwith %}
{% endblock %}
{% block content %}
<r;!-- Content here -->
{% endblock %}
Note: This assumes that you are generating your CSS files with a command such as this:
$ sass generic.scss generic.ltr.css
And that inside of generic.scss
you’ve got an @import "directional"
wherein you switch the direction between LTR and RTL in order to generate your sets of CSS.
And that’s a wrap!
It’s essentially everything you need to internationalize your Django website and get django to do a first pass over your underscore templates. If you’ve got suggestions for improving this work flow, by all means, pass them my way! I hope this helps give you some ideas on how to use Django’s built in internationalization and localization tools to make your life easier 🙂
Django Tutorial | Internationalization & Localization
First, we will create a folder to save all the translation files. Those files will be created automatically by Django, writing the strings that we want to translate. I’ll show you how you can tell Django which strings you want to translate latter, but the idea is that you edit the files and put the translation for each string manually. This way, Django will choose one language or another depending on the user preferences.
The translation’s folder will be located inside the taskbuster folder:
1$ mkdir taskbuster/locale
Next, open your settings/base.py file and make sure you have
1USE_I18N = True
and the template context processor django.template.context_processors.i18n is inside the TEMPLATES[‘OPTIONS’][‘context_processors’] setting:
1234567891011TEMPLATES = [ { … ‘OPTIONS’: { ‘context_processors’: [ … ‘django.template.context_processors.i18n’, ], }, },]
Note: You can also find the value of a specific setting by using the Django shell. For example:
123$ python manage.py shell>>> from django.conf import settings>>> settings.TEMPLATES
and it will output the current value of that variable.
Next, add the Locale middleware in the correct position, to be able to determine the user’s language preferences through the request context:
1234567MIDDLEWARE_CLASSES = ( … ‘django.contrib.sessions.middleware.SessionMiddleware’, ‘django.middleware.locale.LocaleMiddleware’, ‘django.middleware.common.CommonMiddleware’, …)
Next, specify the languages you want to use:
12345from django.utils.translation import ugettext_lazy as _LANGUAGES = ( (‘en’, _(‘English’)), (‘ca’, _(‘Catalan’)),)
We will use English and Catalan (but feel free to put the languages you want, you can find its codes
). The ugettext_lazy function is used to mark the language names for translation, and it’s usual to use the function’s shortcut _.
Note: there is another function, ugettext, used for translation. The difference between these two functions is that ugettext translates the string immediately whereas ugettext_lazy translates the string when rendering a template.
For our settings.py, we need to use ugettext_lazy because the other function would cause import loops. In general, you should use ugettext_lazy in your model.py and forms.py files as well.
Moreover, the LANGUAGE_CODE setting defines the default language that Django will use if no translation is found. I’ll leave the default:
1LANGUAGE_CODE = ‘en-us’
And finally, specify the locale folder that we created before:
123LOCALE_PATHS = ( os.path.join(BASE_DIR, ‘locale’),)
Don’t forget the trailing coma.
Internationalization – Urls
Ok, now that we have configured the settings, we need to think about how we want the app to behave with different languages. Here we will take the following approach: we will include a language prefix on each url that will tell Django which language to use. For the Home page it will be something like:
- mysite.com/en
- mysite.com/ca
And for the rest of urls, like mysite.com/myapp, it will be:
- mysite.com/en/myapp
- mysite.com/ca/myapp
This way the user may change from one language to another easily. However, we don’t want that neither the robots.txt nor the humans.txt files follow this structure (search engines will look atmysite.com/robots.txt and mysite.com/humans.txt to find them).
One way to implement this is with the following urls.py file:
123456789101112131415# -*- coding: utf-8 -*-from django.conf.urls import include, urlfrom django.contrib import adminfrom django.conf.urls.i18n import i18n_patternsfrom .views import home, home_files urlpatterns = [ url(r’^(?P<filename>(robots.txt)|(humans.txt))$’, home_files, name=’home-files’),] urlpatterns += i18n_patterns( url(r’^$’, home, name=’home’), url(r’^admin/’, include(admin.site.urls)),)
Note that we left the robots.txt and humans.txt files with the same url, and the ones that we want to be translated use the i18n_patterns function.
Run your local server and visit the home page, it should redirect to /en or /ca. You can learn more about how Django discovers language preference in the official documentation.
But how does the user change its language preferences? Well, Django comes with a view that does this for you
This view expects a POST request with a language parameter. However, we will take care of that view in another time in this tutorial, when customizing the top navigation bar. The idea is to have a drop-down menu with all the possible languages, and just select one to change it.
Before proceeding, let’s run our tests,
1$ python mange.py test taskbuster.test
Oh…. one failed! Well actually both unittest in taskbuster/test.py fail, as the template rendered when trying to use reverse(“home”) is not found. This is because we need to set an active language for the reverse to work properly. First, write this at the top of the file:
1from django.utils.translation import activate
and next, activate the selected language just after the test declaration. For example:
1234def test_uses_index_template(self): activate(‘en’) response = self.client.get(reverse(“home”)) self.assertTemplateUsed(response, “taskbuster/index.html”)
And the same for the other test: test_uses_base_template.
Now, tests pass. You should do the same for the functional_tests/test_all_users.py: import the activate method at the beginning of the file and add the activate(‘en’) as the last step on the setUp method.
Internationalization – Templates
Let’s focus now on how we can translate the h1 Hello World title of the Home Page. Open the index.html template and look for <h1>Hello, world!</h1>.
We will use two different template tags:
- trans is used to translate a single line – we will use it for the title
- blocktrans is used for extended content – we will use it for a paragraph
Change the h1 and p contents of the jumbotron container for the following code:
12345678
{% trans “Welcome to TaskBuster!”%}
{% blocktrans %}TaskBuster is a simple Task manager that helps you organize your daylife. You can create todo lists, periodic tasks and more! If you want to learn how it was created, or create it yourself!, check www.django-tutorial.com{% endblocktrans %}
</div></div>
Moreover, to have access to the previous template tags, you will have to write {% load i18n %} near the top of your template. In this case, after the extends from base.html tag.
Internationalization – Translation
Finally, we are able to translate our strings!
Go to the terminal, inside the taskbuster_project folder (at the same level as the manage.py file), and run:
1$ python manage.py makemessages -l ca
This will create a message file for the language we want to translate. As we will write all our code in english, there is no need to create a message file for that language.
But ohh!! we get an ugly error that says that we don’t have the GNU gettext installed (if you don’t get the error, good for you! skip this installation part then!). Go to the GNU gettext home page and download the last version. Inside the zip file you’ll find the installation instructions on a file named INSTALL.
Basically, you should go inside the package folder (once unzipped) and type:
1$ ./configure
to configure the installation for your system. Next, type
1$ make
to compile the package. I always wonder why some installations print all that awful code on your terminal!
If you want, use
1$ make check
to run package tests before installing them, to see that everything works. Finally, run
1$ make install
to install the package.
Okey! Let’s go back to our developing environment, and try to generate our message file!
1$ python manage.py makemessages -l ca
Yes! It worked!
Now go to the taskbuster/locale folder to see what’s in there.
12$ cd taskbuster/locale$ ls
There is a folder named ca (or the language you chose to translate) with a folder named LC_MESSAGES inside. If you go inside it, you’ll find another file named django.po. Inspect that file with your editor.
There is some metadata at the beginning of the file, but after that you’ll see the strings we marked for translation:
- The language’s names “English” and “Catalan” in the base.py settings file
- The Welcome to TaskBuster! title on the index.html file
- The paragraph after the title on the index.html file
Each of these sentences appear in a line beginning with msgid. You have to put your translation in the next line, the one that starts with msgstr.
Translating the title is simple:
12msgid “Welcome to TaskBuster!"msgstr "Benvingut a TaskBuster!”
And with a paragraph, you have to be careful to start and end each line with “”:
12345678910msgid “”“TaskBuster is a simple Task manager that helps you organize your daylife. </”“br> You can create todo lists, periodic tasks and more! </br></br> If you ”“want to learn how it was created, or create it yourself!, check www.django-”“tutorial.com"msgstr ”“"TaskBuster és un administrador senzill de tasques que t’ajuda a administrar ”“el teu dia a dia. </br> Pots crear llistes de coses pendents, tasques ”“periòdiques i molt més! </br></br> Si vols apendre com s’ha creat, o”“crear-lo tu mateix!, visita la pàgina <a href=’http://www.marinamele.com/taskbuster-django-tutorial’ target=_’blank’>Taskbuster Django Tutorial</a>.”
Also, note the final space at the end of the line. If you don’t include that space, the words at the end of the line and at the beginning of the next line will concatenate.
Once you have all the translations set, you must compile them with:
1$ python manage.py compilemessages -l ca
You can run your local server and see the effect by going to the home page, but I prefer writing a test first!
In the functional_tests/test_all_users.py add the following tests:
1234567def test_internationalization(self): for lang, h1_text in [(‘en’, ‘Welcome to TaskBuster!’), (‘ca’, ‘Benvingut a TaskBuster!’)]: activate(lang) self.browser.get(self.get_full_url(“home”)) h1 = self.browser.find_element_by_tag_name(“h1”) self.assertEqual(h1.text, h1_text)
Remember to change the Benvingut a TaskBuster! sentence and the activate(‘ca’) if you’re using another language!
I hope all of your tests passed!
HTTP Cache 튜토리얼 | Knowledge Logger
ETag란?
컨텐트 기반의 캐쉬를 위해 특정 컨텐트에 대해 MD5 해쉬 등의 방법으로 다이제스트를 생성하면 해당 컨텐츠가 변경되었는지를 판별할수있는데, 이러한 다이제스트 값을 ETag(entity tag)로 사용한다.4
대부분의 HTTP서버들은 정적인 컨텐츠(파일이나, 내용이 변하지 않는 웹페이지 등)에 대해 ETag와 Last-Modified를 생성하여 헤더에 추가하도록 설정되어있으며, 이를 HTTP서버 관리자가 원하는대로 수정하는 것이 가능하다.
클라이언트가 특정 URL을 서버에 요청을 하면 웹서버는 해당 요청에 대한 응답을 하게되는데 해당 응답 헤더에 ETag, Last-Modified 항목이 포함되어있다. 클라이언트가 동일한 URL로 다시 요청을 할 경우 클라이언트는 요청 헤더의 If-None-Match필드에 ETag값을 포함시켜서 보내게 되고, 서버는 클라이언트에서 보내온 ETag와 현재 컨텐츠의 ETag를 비교하여 유효성을 검사한다.
ETag가 동일한 경우 응답 바디없이 헤더만 HTTP 304 Not modified를 리턴하고, ETag가 다른것이 발견되면 전체응답을 완전히 재전송하게된다.
HTTP Cache 튜토리얼 | Knowledge Logger
ETag란?
컨텐트 기반의 캐쉬를 위해 특정 컨텐트에 대해 MD5 해쉬 등의 방법으로 다이제스트를 생성하면 해당 컨텐츠가 변경되었는지를 판별할수있는데, 이러한 다이제스트 값을 ETag(entity tag)로 사용한다.4
대부분의 HTTP서버들은 정적인 컨텐츠(파일이나, 내용이 변하지 않는 웹페이지 등)에 대해 ETag와 Last-Modified를 생성하여 헤더에 추가하도록 설정되어있으며, 이를 HTTP서버 관리자가 원하는대로 수정하는 것이 가능하다.
클라이언트가 특정 URL을 서버에 요청을 하면 웹서버는 해당 요청에 대한 응답을 하게되는데 해당 응답 헤더에 ETag, Last-Modified 항목이 포함되어있다. 클라이언트가 동일한 URL로 다시 요청을 할 경우 클라이언트는 요청 헤더의 If-None-Match필드에 ETag값을 포함시켜서 보내게 되고, 서버는 클라이언트에서 보내온 ETag와 현재 컨텐츠의 ETag를 비교하여 유효성을 검사한다.
ETag가 동일한 경우 응답 바디없이 헤더만 HTTP 304 Not modified를 리턴하고, ETag가 다른것이 발견되면 전체응답을 완전히 재전송하게된다.
HTTP Cache 튜토리얼 | Knowledge Logger
HTTP Cache 튜토리얼
HTTP를 이용하는 어플리케이션을 개발하다보면 효율적인 네트워크 송수신을위해 서버/클라이언트에서 캐쉬(cache)를 이용하는것이 필수적이다. HTTP를 이용할때 어떤식의 캐쉬방식이 있는지, 어떤 종류의 캐쉬들이 있는지, 어플리케이션 개발에있어서 상식적으로 알고있어야 할 내용들을 정리해보았다.
캐쉬의 종류
캐쉬의 위치에 따라 다음과 같이 분류가 가능하다.1
- Browser cache
- Proxy cache
- Gateway cache(reverse proxy cache)
- 웹브라우져 혹은 HTTP요청을 하는 클라이언트 어플리케이션들이 내부적으로 갖고있는 캐시이다.
- 실제 서버가 있는곳이 아닌 네트워크 관리자에의해 네트워크상에 설치되는하는 캐시다.
- 일반적으로 큰회사나 ISP의 방화벽(firewall)에 설치된다.
- shared cache의 일종으로 많은 수의 사용자들에 의해 공유되어 사용되며, 레이턴시와 트래픽을 줄이는데 매우 도움이된다.
- 네트워크상에 설치되지 않고 실제 서버의 관리자에의해 설치 및 운영된다.
- 실제 서버의 앞단에 설치되어 요청에대한 캐쉬 및 효율적인 분배를 통해 서버의 응답 성능을 좋게하고, scalable하게 만들어 준다.
- 로드밸런서 등을 사용해서 실제 서버가 아닌 gateway cache로 요청을 reroute한다.
- CDN은 이런 gateway 캐시를 유료로 제공해주는 서비스라고 볼 수 있다.
기본적인 캐쉬 동작 방식
- 응답 헤더의 캐쉬가 캐쉬 하지말라고 지정되어있는 경우 캐쉬하지 않는다
요청이 HTTPS거나 HTTP 인증을 사용한 경우 캐쉬되지 않는다.
캐쉬된 데이터는 캐쉬의 만료일자(expired) 혹은 만료시간(max-age)이 아직 남아있는경우 의해 최신상태(fresh)인 것으로 인정된다. 최신상태일 경우 서버에 요청을 보내지않고 캐쉬를 사용한다.
캐쉬가 최신 상태가 아닐(stale)경우 클라이언트는 서버에 유효성 검사(validation) 요청을 보내서 캐쉬에 존재하는 데이터를 새로 내려받지 않아도 아직 유효한지 검사를 한다. 이때 유효하지 않을경우 전체 재전송이 일어나며, 유효할경우에는 재전송이 일어나지 않으며(HTTP 304 Not modified 결과를 수신) 캐쉬의 만료일자/만료시간을 새롭게 업데이트한다.
이제 이러한 기본적인 동작들이 캐쉬 지시자들에 의해 어떤 방식으로 세부적으로 컨트롤되는지 살펴보자.
HTTP Request Header의 캐쉬 지시자
일반적으로 캐시여부를 결정하는데 사용되는 정보인 캐쉬 지시자는 서버의 HTTP응답에 포함되어있다. 하지만 가끔씩 HTTP 요청을 보낼경우에도 캐쉬 지시자를 사용하는 경우가 있다. 프록시 서버가 중간에 있을경우, 프록시서버에 있는 캐쉬데이터가 최신(fresh)일 경우 이를 바로 클라이언트에 돌려주게된다. 하지만 캐쉬가 최신임에도 프록시가 아닌 실제서버에서 데이터를 직접 받아오고 싶은경우 Cache control: no-cache
필드를 지정하면 해당 요청은 프록시를 거쳐 실 서버까지 도달하게 된다.Cache control: no-store
필드가 지정될 경우에는 캐쉬가 해당 요청이나 그에대한 응답을 절대로 저장하지 않도록 한다. 2
이 두가지 필드외에도 다른 필드들이 있지만 요청(request) 측면에서는 의미가 없다. 하지만 응답(response)쪽에서 설정되었을때는 중요한 의미를 가지므로 더 자세한 내용은 다음 응답헤더의 캐쉬 지시자 섹션에서 더 살펴보도록 하자.
HTTP Response Header의 캐쉬 지시자
서버에서 클라이언트에게 응답을 보낼때 캐쉬관련 정보를 알려주기위해 HTML 문서내에 캐쉬관련 정보가 포함된 meta 태그를 이용하는 방법도있지만 이방법은 지원하는 브라우저들이 한정되어있기때문에 거의 사용되지 않는다. 일반적으로는 캐쉬컨트롤을 위해 응답 헤더에 캐쉬관련 필드를 추가하는 방법많이 사용하므로, 이를 더 자세히 알아보도록 하자.
1. Pragma
Pragma: cache
, 혹은 Pragma: no-cache
등의 Pragma를 이용할경우 특정브라우저에서만 동작하기때문에 정확하게 캐쉬컨트롤이 되는지 보장받을 수 없으므로,
HTTP/1.0에서 캐시 등을 제어하기 위해 사용되었으나 HTTP/1.1에서는 좀더 정교한 캐시컨트롤을 위해 별도의 지시자가 추가되었으므로 Pragma는 사용하지 않는것이 좋다(1.0과의 하위 호환성을 위해 남아있음).
2. Expires
- 사용 예:
Expires: Fri, 30 Oct 2014 20:11:21 GMT
HTTP date 형태로 날짜를 지정하며 로컬타임이 아닌 GMT를 사용한다.
웹서버에서 last access time 혹은 last modified time 기준으로 absolute 시간값을 셋팅하여 response
규칙적인 시간간격으로 컨텐츠가 바뀔때 유용하다.
- 단점:
- 웹서버와 클라이언트간의 time sync가 맞지않으면 무의미.(클라이언트의 경우 임의로 시간 변경이 가능한 경우가 많기때문에 문제의 소지가 크다)
- 한번 expires 를 설정해두고 업데이트하지 않으면, 컨텐츠가 바뀌지 않았음에도 새롭게 요청이 들어와서 로드가 커질 수 있다.
3. Cache-Control
- 사용 예:
Cache-Control: max-age=3600, must-revalidate
- 옵션:
- 해석: 3600초까지는 서버에 요청없이 캐쉬데이터를 사용하며, 3600초 경과 후에는 서버에 꼭 유효성검사를 해야하며 네트웍 상황등의 이유로 유효성검사가 불가능할경우에도 절대로 캐쉬데이터를 사용하면 안된다는것을 명시.
max-age=[seconds]
— Expires와 동일한 의미지만 고정된 절대시간 값이아닌 요청 시간으로부터의 상대적 시간을 표시한다. 명시된경우 Expires보다 우선시된다.
s-maxage=[seconds]
— max-age와 동일한 의미지만 shared caches (예: proxy)에만 적용됨. 명시된경우 max-age나 Expires보다 우선시된다.3
public
— 일반적으로 HTTP 인증이 된 상태에서 일어나는 응답은 자동으로 private 이 되지만 public을 명시적으로 설정하면 인증이된 상태더라도 캐시하도록 한다.
private
— 특정 유저(사용자의 브라우저)만 캐쉬하도록 설정. 여러사람이 사용하는 네트워크상의 중간자(intermediaries)역할을 하는 shared caches (예: proxy) 에는 경우 캐쉬되지 않는다.
no-cache
— 응답 데이터를 캐쉬하고는 있지만, 일단 먼저 서버에 요청해서 유효성 검사(validation)을 하도록 강제한다. 어느정도 캐쉬의 효용을 누리면서도 컨텐츠의 freshness를 강제로 유지하는데 좋다.
no-store
— 어떤 상황에서도 해당 response 데이터를 저장하지 않음.
no-transform
— 어떤 프록시들은 어떤 이미지나 문서들을 성능향상을위해 최적화된 포멧으로 변환하는 등의 자동화된 동작을 하는데 이러한 것을 원치 않는다면 이 옵션을 명시해주는 것이 좋다.
must-revalidate
— HTTP는 특정 상황(네트워크 연결이 끊어졌을때 등)에서는 fresh하지 않은 캐쉬 데이터임에도 불구하고 사용하는 경우가 있는데, 금융거래 등의 상황에서는 이러한 동작이 잘못된 결과로 이어질 가능성이 있기 때문에 이 지시자를 통해서 그러한 사용을 방지한다.
proxy-revalidate
— must-revalidate
와 비슷하지만 proxy caches 에만 적용된다.
캐쉬 만료(cache expiration) 체크하기
Expired나 Cache-Control의 max-age 필드를 이용하여 캐쉬에 저장된 데이터가 오래된(stale)경우 서버로 재요청을 한다. 캐쉬 만료전까지는 서버에 요청할 필요가 없으므로 네트워크 요청에 대한 왕복(roundtrip)시간을 절약할 수 있다.
캐쉬 유효성 검사(cache validation) 방법
현재 캐쉬하여 갖고있는 데이터가 최신(freshness)인지 확인하여 새로 전송이 필요한지 확인하는 것을 캐쉬 유효성 검사라고 한다. 캐쉬데이터가 유효한 경우에는 서버에서 full response를 할 필요가 없어지므로 네트워크 대역폭(bandwidth)을 절약하게 해준다. 유효성 검사를 수행하기 위해 앞서 언급한 캐쉬 지시자나 서버에서 생성/관리하는 ETag를 사용하게 된다.
ETag란?
컨텐트 기반의 캐쉬를 위해 특정 컨텐트에 대해 MD5 해쉬 등의 방법으로 다이제스트를 생성하면 해당 컨텐츠가 변경되었는지를 판별할수있는데, 이러한 다이제스트 값을 ETag(entity tag)로 사용한다.4
대부분의 HTTP서버들은 정적인 컨텐츠(파일이나, 내용이 변하지 않는 웹페이지 등)에 대해 ETag와 Last-Modified를 생성하여 헤더에 추가하도록 설정되어있으며, 이를 HTTP서버 관리자가 원하는대로 수정하는 것이 가능하다.
클라이언트가 특정 URL을 서버에 요청을 하면 웹서버는 해당 요청에 대한 응답을 하게되는데 해당 응답 헤더에 ETag, Last-Modified 항목이 포함되어있다. 클라이언트가 동일한 URL로 다시 요청을 할 경우 클라이언트는 요청 헤더의 If-None-Match
필드에 ETag값을 포함시켜서 보내게 되고, 서버는 클라이언트에서 보내온 ETag와 현재 컨텐츠의 ETag를 비교하여 유효성을 검사한다.
ETag가 동일한 경우 응답 바디없이 헤더만 HTTP 304 Not modified를 리턴하고, ETag가 다른것이 발견되면 전체응답을 완전히 재전송하게된다.
동적인 컨텐츠는 어떻게 캐싱하나?
PHP, ASP, JSP등의 서버사이드 프로그램을 통해서 동적으로 생성된 컨텐츠는 Last-Modified, ETag, Expires, Cache-Control 등의 정보가 없어서 손쉽게 캐쉬할 수 없다.
이러한 문제점을 해결하기 위해 다음과 같은 방법들이 사용가능하다.
- 추천: 동적으로 생성된 컨텐츠를 파일로 저장해두고 정적인 컨텐츠처럼 취급한다. Last-Modified를 보존할 수 있도록 내용이 바뀔때만 새롭게 파일로 저장한다.
응답 헤더에 freshness를 판별할 수 있도록 캐쉬 지시자를 추가
2번으로 부족할경우 서버사이드 프로그램 자체에서 ETag 같은 validator를 생성하여 응답헤더에 추가하고, 클라이언트에서 http 요청이 올경우 이를 파싱하여 직접 validation을 한다.
- RFC 2616 ↩
- http://www.mobify.com/blog/beginners-guide-to-http-cache-headers/ ↩
- https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers ↩
http://www.mnot.net/cache_docs/ ↩
관련 포스트:
HTTP Cache 튜토리얼 | Knowledge Logger
HTTP Cache 튜토리얼
HTTP를 이용하는 어플리케이션을 개발하다보면 효율적인 네트워크 송수신을위해 서버/클라이언트에서 캐쉬(cache)를 이용하는것이 필수적이다. HTTP를 이용할때 어떤식의 캐쉬방식이 있는지, 어떤 종류의 캐쉬들이 있는지, 어플리케이션 개발에있어서 상식적으로 알고있어야 할 내용들을 정리해보았다.
캐쉬의 종류
캐쉬의 위치에 따라 다음과 같이 분류가 가능하다.1
- Browser cache
- Proxy cache
- Gateway cache(reverse proxy cache)
- 웹브라우져 혹은 HTTP요청을 하는 클라이언트 어플리케이션들이 내부적으로 갖고있는 캐시이다.
- 실제 서버가 있는곳이 아닌 네트워크 관리자에의해 네트워크상에 설치되는하는 캐시다.
- 일반적으로 큰회사나 ISP의 방화벽(firewall)에 설치된다.
- shared cache의 일종으로 많은 수의 사용자들에 의해 공유되어 사용되며, 레이턴시와 트래픽을 줄이는데 매우 도움이된다.
- 네트워크상에 설치되지 않고 실제 서버의 관리자에의해 설치 및 운영된다.
- 실제 서버의 앞단에 설치되어 요청에대한 캐쉬 및 효율적인 분배를 통해 서버의 응답 성능을 좋게하고, scalable하게 만들어 준다.
- 로드밸런서 등을 사용해서 실제 서버가 아닌 gateway cache로 요청을 reroute한다.
- CDN은 이런 gateway 캐시를 유료로 제공해주는 서비스라고 볼 수 있다.
기본적인 캐쉬 동작 방식
- 응답 헤더의 캐쉬가 캐쉬 하지말라고 지정되어있는 경우 캐쉬하지 않는다
요청이 HTTPS거나 HTTP 인증을 사용한 경우 캐쉬되지 않는다.
캐쉬된 데이터는 캐쉬의 만료일자(expired) 혹은 만료시간(max-age)이 아직 남아있는경우 의해 최신상태(fresh)인 것으로 인정된다. 최신상태일 경우 서버에 요청을 보내지않고 캐쉬를 사용한다.
캐쉬가 최신 상태가 아닐(stale)경우 클라이언트는 서버에 유효성 검사(validation) 요청을 보내서 캐쉬에 존재하는 데이터를 새로 내려받지 않아도 아직 유효한지 검사를 한다. 이때 유효하지 않을경우 전체 재전송이 일어나며, 유효할경우에는 재전송이 일어나지 않으며(HTTP 304 Not modified 결과를 수신) 캐쉬의 만료일자/만료시간을 새롭게 업데이트한다.
이제 이러한 기본적인 동작들이 캐쉬 지시자들에 의해 어떤 방식으로 세부적으로 컨트롤되는지 살펴보자.
HTTP Request Header의 캐쉬 지시자
일반적으로 캐시여부를 결정하는데 사용되는 정보인 캐쉬 지시자는 서버의 HTTP응답에 포함되어있다. 하지만 가끔씩 HTTP 요청을 보낼경우에도 캐쉬 지시자를 사용하는 경우가 있다. 프록시 서버가 중간에 있을경우, 프록시서버에 있는 캐쉬데이터가 최신(fresh)일 경우 이를 바로 클라이언트에 돌려주게된다. 하지만 캐쉬가 최신임에도 프록시가 아닌 실제서버에서 데이터를 직접 받아오고 싶은경우 Cache control: no-cache
필드를 지정하면 해당 요청은 프록시를 거쳐 실 서버까지 도달하게 된다.Cache control: no-store
필드가 지정될 경우에는 캐쉬가 해당 요청이나 그에대한 응답을 절대로 저장하지 않도록 한다. 2
이 두가지 필드외에도 다른 필드들이 있지만 요청(request) 측면에서는 의미가 없다. 하지만 응답(response)쪽에서 설정되었을때는 중요한 의미를 가지므로 더 자세한 내용은 다음 응답헤더의 캐쉬 지시자 섹션에서 더 살펴보도록 하자.
HTTP Response Header의 캐쉬 지시자
서버에서 클라이언트에게 응답을 보낼때 캐쉬관련 정보를 알려주기위해 HTML 문서내에 캐쉬관련 정보가 포함된 meta 태그를 이용하는 방법도있지만 이방법은 지원하는 브라우저들이 한정되어있기때문에 거의 사용되지 않는다. 일반적으로는 캐쉬컨트롤을 위해 응답 헤더에 캐쉬관련 필드를 추가하는 방법많이 사용하므로, 이를 더 자세히 알아보도록 하자.
1. Pragma
Pragma: cache
, 혹은 Pragma: no-cache
등의 Pragma를 이용할경우 특정브라우저에서만 동작하기때문에 정확하게 캐쉬컨트롤이 되는지 보장받을 수 없으므로,
HTTP/1.0에서 캐시 등을 제어하기 위해 사용되었으나 HTTP/1.1에서는 좀더 정교한 캐시컨트롤을 위해 별도의 지시자가 추가되었으므로 Pragma는 사용하지 않는것이 좋다(1.0과의 하위 호환성을 위해 남아있음).
2. Expires
- 사용 예:
Expires: Fri, 30 Oct 2014 20:11:21 GMT
HTTP date 형태로 날짜를 지정하며 로컬타임이 아닌 GMT를 사용한다.
웹서버에서 last access time 혹은 last modified time 기준으로 absolute 시간값을 셋팅하여 response
규칙적인 시간간격으로 컨텐츠가 바뀔때 유용하다.
- 단점:
- 웹서버와 클라이언트간의 time sync가 맞지않으면 무의미.(클라이언트의 경우 임의로 시간 변경이 가능한 경우가 많기때문에 문제의 소지가 크다)
- 한번 expires 를 설정해두고 업데이트하지 않으면, 컨텐츠가 바뀌지 않았음에도 새롭게 요청이 들어와서 로드가 커질 수 있다.
3. Cache-Control
- 사용 예:
Cache-Control: max-age=3600, must-revalidate
- 옵션:
- 해석: 3600초까지는 서버에 요청없이 캐쉬데이터를 사용하며, 3600초 경과 후에는 서버에 꼭 유효성검사를 해야하며 네트웍 상황등의 이유로 유효성검사가 불가능할경우에도 절대로 캐쉬데이터를 사용하면 안된다는것을 명시.
max-age=[seconds]
— Expires와 동일한 의미지만 고정된 절대시간 값이아닌 요청 시간으로부터의 상대적 시간을 표시한다. 명시된경우 Expires보다 우선시된다.
s-maxage=[seconds]
— max-age와 동일한 의미지만 shared caches (예: proxy)에만 적용됨. 명시된경우 max-age나 Expires보다 우선시된다.3
public
— 일반적으로 HTTP 인증이 된 상태에서 일어나는 응답은 자동으로 private 이 되지만 public을 명시적으로 설정하면 인증이된 상태더라도 캐시하도록 한다.
private
— 특정 유저(사용자의 브라우저)만 캐쉬하도록 설정. 여러사람이 사용하는 네트워크상의 중간자(intermediaries)역할을 하는 shared caches (예: proxy) 에는 경우 캐쉬되지 않는다.
no-cache
— 응답 데이터를 캐쉬하고는 있지만, 일단 먼저 서버에 요청해서 유효성 검사(validation)을 하도록 강제한다. 어느정도 캐쉬의 효용을 누리면서도 컨텐츠의 freshness를 강제로 유지하는데 좋다.
no-store
— 어떤 상황에서도 해당 response 데이터를 저장하지 않음.
no-transform
— 어떤 프록시들은 어떤 이미지나 문서들을 성능향상을위해 최적화된 포멧으로 변환하는 등의 자동화된 동작을 하는데 이러한 것을 원치 않는다면 이 옵션을 명시해주는 것이 좋다.
must-revalidate
— HTTP는 특정 상황(네트워크 연결이 끊어졌을때 등)에서는 fresh하지 않은 캐쉬 데이터임에도 불구하고 사용하는 경우가 있는데, 금융거래 등의 상황에서는 이러한 동작이 잘못된 결과로 이어질 가능성이 있기 때문에 이 지시자를 통해서 그러한 사용을 방지한다.
proxy-revalidate
— must-revalidate
와 비슷하지만 proxy caches 에만 적용된다.
캐쉬 만료(cache expiration) 체크하기
Expired나 Cache-Control의 max-age 필드를 이용하여 캐쉬에 저장된 데이터가 오래된(stale)경우 서버로 재요청을 한다. 캐쉬 만료전까지는 서버에 요청할 필요가 없으므로 네트워크 요청에 대한 왕복(roundtrip)시간을 절약할 수 있다.
캐쉬 유효성 검사(cache validation) 방법
현재 캐쉬하여 갖고있는 데이터가 최신(freshness)인지 확인하여 새로 전송이 필요한지 확인하는 것을 캐쉬 유효성 검사라고 한다. 캐쉬데이터가 유효한 경우에는 서버에서 full response를 할 필요가 없어지므로 네트워크 대역폭(bandwidth)을 절약하게 해준다. 유효성 검사를 수행하기 위해 앞서 언급한 캐쉬 지시자나 서버에서 생성/관리하는 ETag를 사용하게 된다.
ETag란?
컨텐트 기반의 캐쉬를 위해 특정 컨텐트에 대해 MD5 해쉬 등의 방법으로 다이제스트를 생성하면 해당 컨텐츠가 변경되었는지를 판별할수있는데, 이러한 다이제스트 값을 ETag(entity tag)로 사용한다.4
대부분의 HTTP서버들은 정적인 컨텐츠(파일이나, 내용이 변하지 않는 웹페이지 등)에 대해 ETag와 Last-Modified를 생성하여 헤더에 추가하도록 설정되어있으며, 이를 HTTP서버 관리자가 원하는대로 수정하는 것이 가능하다.
클라이언트가 특정 URL을 서버에 요청을 하면 웹서버는 해당 요청에 대한 응답을 하게되는데 해당 응답 헤더에 ETag, Last-Modified 항목이 포함되어있다. 클라이언트가 동일한 URL로 다시 요청을 할 경우 클라이언트는 요청 헤더의 If-None-Match
필드에 ETag값을 포함시켜서 보내게 되고, 서버는 클라이언트에서 보내온 ETag와 현재 컨텐츠의 ETag를 비교하여 유효성을 검사한다.
ETag가 동일한 경우 응답 바디없이 헤더만 HTTP 304 Not modified를 리턴하고, ETag가 다른것이 발견되면 전체응답을 완전히 재전송하게된다.
동적인 컨텐츠는 어떻게 캐싱하나?
PHP, ASP, JSP등의 서버사이드 프로그램을 통해서 동적으로 생성된 컨텐츠는 Last-Modified, ETag, Expires, Cache-Control 등의 정보가 없어서 손쉽게 캐쉬할 수 없다.
이러한 문제점을 해결하기 위해 다음과 같은 방법들이 사용가능하다.
- 추천: 동적으로 생성된 컨텐츠를 파일로 저장해두고 정적인 컨텐츠처럼 취급한다. Last-Modified를 보존할 수 있도록 내용이 바뀔때만 새롭게 파일로 저장한다.
응답 헤더에 freshness를 판별할 수 있도록 캐쉬 지시자를 추가
2번으로 부족할경우 서버사이드 프로그램 자체에서 ETag 같은 validator를 생성하여 응답헤더에 추가하고, 클라이언트에서 http 요청이 올경우 이를 파싱하여 직접 validation을 한다.
- RFC 2616 ↩
- http://www.mobify.com/blog/beginners-guide-to-http-cache-headers/ ↩
- https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers ↩
http://www.mnot.net/cache_docs/ ↩
관련 포스트:
Static Root and Static Url confusion in Django
From the django docs,
STATIC_ROOT
is the absolute path to the directory where collectstatic will collect static files for deployment.
STATIC_URL
is the URL to use when referring to static files located in STATIC_ROOT
.
So, when you request some specific static resource, it is searched in STATIC_ROOT + STATIC_URL
and then served.
Now in your problem, you do
STATIC_ROOT = os.path.join(BASE_DIR, 'play/static_root')
STATIC_URL = '/static/'
which means django would have effectively been searching in BASE_DIR/play/static_root/static/
which would be incorrect, so looking at other paths you can figure out that you need to do
STATIC_ROOT = os.path.join(BASE_DIR, 'play/')
내의견
STATICFILES_DIRS 는 실제 개발자가 화일들을 저장해놓은 곳이며 django가 검색해서 STATIC_ROOT의 경로에 복사해서 놓는다.이 곳으로부터 화일이 user들의 브라우저로 전송된다. STATIC_URL은 STATIC_ROOT에 덧붙여져서 화일의 URL경로가 완성되게 한다.