[장고 기본편] “Static Files - CSS/JavaScript 파일을 어떻게 관리해야 할까요?” VOD 링크
장고는 One Project, Multi App 구조
한 App을 위한 static 파일을 app/static/app경로에 두세요.
프로젝트 전반적으로 사용되는 static 파일을 settings.STATICFILES_DIRS에서 참조 하는 경로에 두세요.
1 2 3 4 5
# myproj/settings.py STATIC_URL = '/static/' # Static 파일 요청에 대한 URL Prefix STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'myproj', 'static'), ]
장고에서의 STATIC 파일 서빙
장고의 개발서버에서
1 2 3 4 5 6 7 8 9 10 11 12
myproj/static/main.css => http://localhost:8000/static/main.css 경로로 접근 가능 myproj/static/jquery/jquery-2.2.4.min.js => http://localhost:8000/static/jquery/ jquery-2.2.4.min.js myproj/static/bootstrap/3.3.7/css/bootstrap.min.css => http://localhost:8000/static/ bootstrap/3.3.7/css/bootstrap.min.css blog/static/blog/style.css => http://localhost:8000/static/blog/style.css 경로로 접근 가능 blog/static/blog/blog.js => http://localhost:8000/static/blog/blog.js 경로로 접근 가능 shop/static/shop/shop.js => http://localhost:8000/static/shop/shop.js 경로로 접근 가능
URL을 통해 STATIC 파일이 저장된 파일시스템에 직접 접근하는 것이 아니라, 지정 이름의 STATIC 파일 을 장고의 StaticFiles Finder에서 대신 찾아 그 내용을 읽어서 응답하는 것
브라우저 캐시
브라우저 캐시 기간을 설정해 주면(header에서설정) 그 기간 동안은 웹브라우저가 해당 파일을 다시 다운받지 않고 캐싱된 내용을 사용하기 때문에 트래픽이 줄어들고, 속도도 빨라집니다.
Expires 헤더 MDN #doc : 만료일시를 지정
Expires: Wed, 21 Oct 2015 07:28:00 GMT
응답 내에 “max-age” 혹은 “s-max-age” directive를 지닌 CacheControl 헤더가 존재할 경우, Expires 헤더는 무시
Cache-Control
이후에 해당 파일이 변경되었습니다. 그런데, 새로운 내용이 반영되지 않습니다. ???
유저는 /blog/ 페이지에 방문하면서 브라우저에 /static/blog/style.css 파일이 다운로드되었습니다. 이때 이 파 일이 24시간 동안 브라우저 캐싱이 되어있다고 생각해봅시다.
그런데, 개발하면서 CSS파일이 변경되었습니다. 파일경로는 바뀌지 않았습니다. 변경된 CSS파일이 유저페이지에 적용되길 원하지만 적용되지 않습니다. 캐싱된 이전 파일에 계속 접근하게 됩니다.
해결하기
방법1) 해당 파일의 캐싱이 만료될 때까지 기다립니다.
방법2) 브라우저 설정에서 캐싱된 내용을 삭제합니다. 크롬 브라우저에서는 “강력 새로고침” 3을 통해 수행 가능.
방법3) 해당 STATIC 리소스의 URL을 변경합니다.
Tip: 방법2)개발 시에 유용합니다. 윈도우 단축키 Ctrl+Shift+R, 맥 단축키 Command+Shift+R (개발자도구띄어놓은상황에서)
클라이언트측 캐싱과 빠른 업데이트를 할려면
리소스의 URL을 변경하고 콘텐츠가 변경될 때마다 사용자가 새 응답을 다운로드하도록 하면 됩니다.
GET인자 붙이기 : 실제 파일명은 변경하지 않으면서, 브라우저가 인지하는 URL만 변경
개발 시에 유용
버전을 숫자로 붙이거나 (아래 예시는 get인자로 버전숫자붙임)(새로운 url로 인식되므로 모든 리소스 새롭게 다운받게됨)
import time from django import template from django.conf import settings from django.templatetags.static import StaticNode
register = template.Library()
class VersioningStaticNode(StaticNode): #장고에서 기본 지원해주는 템플릿 태그 def url(self, context): url = super().url(context) #기존 스테틱노드에서 url얻고 if settings.DEBUG: #개발모드 일때만 t = str(int(time.time())) #소수점까지 붙는 시간을 int로 정수형반환>문자열로 반환 if '?' not in url: #(url안에 ? 없다면) url += '?_=' + t else: url += '&_=' + t #?가 이미있다면 get인자가 이미존재하는것이므로 &_뒤에씀 return url
Deprecated된 라이브러리이지만, yarn보다는 심플한 컨셉입니다. 한 번 경험해봅시다
1 2 3
npm을 통한 설치 : 쉘> npm install -g bowe
homebrew를 통한 설치 : 쉘> brew install bower
CSS/JavaScript 라이브러리 설치
1 2
bower install jquery bower install "jquery#3.2.1"
bower_components 디렉토리에 다운로드됩니다.
1 2
bower uninstall jquery # 혹은 해당 디렉토리를 직접 제거하셔도 됩니다. 쉘> bower list
bower.json
1 2 3 4 5 6 7 8 9
쉘> bower init 명령을 통해 bower.json 파일 생성 혹은 직접 생성 본 JavaScript/CSS 팩키지를 배포하는 것은 아니기에, 다른 항목은 불필요 { "name": "example", "dependencies": { "jquery": "~3.2.1", "bootstrap": "~3.3.7" } }
Tip: 버전지정 Rule : “~3.2.1”은 “3.2.1” 이상 “3.3.0” 미만을 뜻합니다
class PostListView(ListView): model = Post paginate_by = 10 # 페이징 처리가 필요할 때, 지정
페이지 이전 다음 만들기 {% if is_paginated %} {% if page_obj.has_previous %} <a href="?page={{ page_obj.previous_page_number }}">이전</a> {% endif %} {{ page_obj.number }} 페이지 {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number }}">다음</a> {% endif %} {% endif %}
2) 서버에서 Raw 데이터 응답을 하면 (주로 JSON포맷), 웹프론트엔드 JavaScript 단에서 이를 HTML포맷으로 변환
1 2 3 4 5 6 7 8 9 10 11 12 13
from django.http import JsonResponse
def my_view_fn_2(request): qs = Post.objects.all() # list comprehension 문법을 통해, 수동 직렬화 post_list = [ {'id': post.id, 'title': post.title } for post in qs] return JsonResponse(post_list, safe=False) # safe=True일 때에는 dict타입만 받고, 아닐 경우 TypeError 예외 발생 #jsonresponse는 직렬화에서 qs를 문자열로 변환해야되는 룰을 알지못해
JSON응답을 하기 위해서는, JSON 직렬화가 필요
ex) QuerySet/Model 객체를 list/tuple/dict으로 변환
직접 직렬화 코딩을 하거나
django-rest-framework 활용
아래 예시 사용하기전에 serializers설치후 settings.py에 추가
pip3 install djangorestframework
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# myapp/serializers.py from rest_framework.serializers import ModelSerializer
class PostSerializer(ModelSerializer): # Django Form/ModelForm과 유사 class Meta: model = Post fields = '__all__' # myapp/views.py from django.http import HttpResponse from rest_framework.renderers import JSONRenderer from .serializers import PostSerializer
def post_list(request): qs = Post.objects.all() serializer = PostSerializer(qs, many=True) json_utf8_string = JSONRenderer().render(serializer.data) # return HttpResponse(json_utf8_string) # Content-Type헤더가 text/html; charset=utf-8 로 디폴트 지정 return HttpResponse(json_utf8_string, content_type='application/json; charset=utf8') # 커스텀 지정
{% block extra_body %} <script> $(function() { //road가 끝나면 아래와 같은 함수가 실행되도록함. var $win = $(window) //jquery로 객체를 만들었고 아래를 보면 $(window).height(), $(window). scrollTop()등을 수행할수있는것. //$(여기)에있는 것은 html에 dom이라는 html 문서에서 각 객체들을 불러올수있음. var is_loading = false; //???
// 매 화면 스크롤마다 호출 $win.scroll(function() { // 문서의 끝에 도달했는가? var diff = $(document).height() - $win.height(); //현재 전체 문서의 세로길이 - 윈도우의 세로길지== if ( (!is_loading) && diff == $win.scrollTop() ) { //is_loading이 false이므로 이것은 True, 값이 같아질때는 아래일어나야함 var search_params = new URLSearchParams(window.location.search); // location.search는 "?page=2"를 가져오고 이것을 URLSearchRarams를 사용해 현재 페이지의 GET인자를 가공 var current_page = parseInt(search_params.get('page')) || 1; // GET인자 page를 획득하고 없으면 1을 반환 var next_page_url = '?page=' + (current_page + 1); // 다음 페이지를 요청하기 위한 URL생성 JS는 문자열과 숫자를 더하면 문자열로 반환함, is_loading = true;
{% block extra_body %} <script> $(function() { //road가 끝나면 아래와 같은 함수가 실행되도록함. var $win = $(window) //jquery로 객체를 만들었고 아래를 보면 $(window).height(), $(window). scrollTop()등을 수행할수있는것. //$(여기)에있는 것은 html에 dom이라는 html 문서에서 각 객체들을 불러올수있음. var is_loading = false; //??? var current_page = null;
var load_more = function() { //함수로 따로 뺌!! if (! is_loading ) { // var search_params = new URLSearchParams(window.location.search); // location.search는 "?page=2"를 가져오고 이것을 URLSearchRarams를 사용해 현재 페이지의 GET인자를 가공 // var current_page = parseInt(search_params.get('page')) || 1; // GET인자 page를 획득하고 없으면 1을 반환, 현재코드는 get인자 이용하지않으므로삭제 var next_page = (current_page || 1) +1 //current_page를 받아오는데 null이면 1로 대체한다. var next_page_url = '?page=' + next_page; // 다음 페이지를 요청하기 위한 URL생성 JS는 문자열과 숫자를 더하면 문자열로 반환함, is_loading = true;
// 매 화면 스크롤마다 호출 $win.scroll(function() { // 문서의 끝에 도달했는가? var diff = $(document).height() - $win.height(); //현재 전체 문서의 세로길이 - 윈도우의 세로길지== if ( diff == $win.scrollTop() ) { //is_loading이 false이므로 이것은 True, 값이 같아질때는 아래일어나야함 console.log("바닥왔음");
load_more(); //위에 함수 구현해놓음 $("#load-more-btn").click(load_more); //버튼누르면 함수호출
}
Modal을 활용한 Detail
포스팅 리스트에서 링크에 click 리스너 걸기
중요한것은 원래 클릭하면 다음링크로 넘어가야하는데 그 사이에 modal을 띄우려하므로 clikck 리스너 필요
1 2 3 4 5 6 7
$(function() { //단지 새 포스트들이 로딩되면 안걸려있음. 왜냐하면 로딩후의 function들이므로 $('#post-list tbody a').click(function(e) { e.preventDefault(); var detail_url = $(this).attr('href'); alert(detail_url); }); });
그런데, 새로이 추가된 다음 페이지 포스팅에 대해서는 이벤트가 먹지 않습니다. click 리스너를 등록하고 나서, 추가된 항목에 대해서는 click 리스너가 등록이 되어 있지 않는 거죠
$.ajaxSetup({ // 모든 Ajax 요청 전에 호출되는 함수를 지정 beforeSend: function(xhr, settings) { // CSRF Token 설정이 필요한 요청이면 if (! csrfSafeMethod(settings.type) && !this.crossDomain ) { // Token 값을 가져와서, 요청 헤더에 심어줍니다. xhr.setRequestHeader("X-CSRFToken", csrftoken); } } });
function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } var csrftoken = getCookie('csrftoken');
function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); }
이 파일을 jquery.csrf.js 파일로 static경로에 저장하고, 템플릿에 추가해주세요. 그럼 삭제가 됩니다