본문 바로가기

Web/Python-django

장고 기초 정리

인생과 개발기간은 짧으니 Django를 씁시다

AskCompany의 장고 강의 내용을 정리한 내용입니다.

목차

$ 장고 주요 기능들

본 코스

  1. Function Based Views : 함수로 HTTP 요청 처리
  2. Models : 데이터베이스와의 인터페이스
  3. Templates : 복잡한 문자열 조합을 보다 용이하게. 주로 HTML 문자열 조합 목적으로 사용하지만, 푸쉬 메세지나 이메일 내용을 만들 때에도 쓰면 편리.
  4. Admin 기초 : 심플한 데이터베이스 레코드 관리 UI
  5. Logging : 다양한 경로로 메세지 로깅
  6. Static files : 개발 목적으로의 정적인 파일 관리
  7. Messages framework : 유저에게 1회성 메세지 노출 목적

별도 코스

  1. Class Based Views : 클래스로 함수 기반 뷰 만들기
  2. Forms : 입력폼 생성, 입력값 유효성 검사 및 DB로의 저장
    • Validators & Fields & Widgets
  3. 테스팅
  4. 국제화 & 지역화
  5. 캐싱
  6. Geographic : DB의 Geo 기능 활용 (PostgreSQL 중심)
  7. Sending Emails
  8. Syndication Feeds (RSS/Atom)
  9. Sitemaps

$ 장고 앱

장고 앱 설명

  • 재사용성을 목적으로 한 파이썬 패키지
    • 앱은 하나의 작은 서비스
  • 앱 이름 중복 불가
  • 앱 생성 명령어 : python manage.py startapp app_name
  • settings.INSTALLED_APPS 에 등록 필수

모듈의 패키지화

앱 복잡도가 높아진다면, 모듈을 패키지화할 수 있다.

  • 모듈 : 파이썬 소스코드 파일
    • shop/models.py 의 Item Class, Review Class
  • 패키지 : 파이썬 디렉토리
    • shop/models/item.py 내 Item Class
      shop/models/review.py 내 Review Class
    • _init_.py 내에서는 from .item import *의 방법을 쓰면
      모델 외부에서 일반 모듈과 같이 사용 가능

앱 생성 시, 작업할 것들

  1. 앱 생성
  2. 앱이름/urls.py 파일 생성
  3. 프로젝트/urls.py에 include 적용
  4. 프로젝트/settings.py의 INSTALLED_APPS에 앱 이름 등록
# 프로젝트/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    '앱이름',
]

# 앱/urls.py
from django.urls import path
from 앱이름 import views

app_name = '앱이름'
urlpatterns = [
]

# 프로젝트/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('앱이름/', include('앱이름.urls')),
]

$ URLConf와 정규 표현식

장고 URL Dispatcher에서는 정규표현식을 통한 URL 매칭

다양한 정규 표현식 패턴 예시

  • 1자리 숫자
    • “[0123456789]” 혹은 “[0-9]” 혹은 “ [\d]” 혹은 “\d”
  • 2자리 숫자
    • “[0123456789] [0123456789]” 혹은 “[0-9] [0-9]” 혹은 “\d\d”
  • 3자리 숫자 : “\d\d\d” 혹은 “\d{3}”
  • 2자리~4자리 숫자 : “\d{2,4}”
  • 휴대폰 번호 : “010[1-9]\d{7}”
  • 알파벳 소문자 1글자
    • “abcdefghijklmnopqrstuvwxyz” 혹은 “[a-z]”
  • 알파벳 대문자 1글자
    • “[ABCDEFGHIJKLMNOPQRSTUVWXYZ]” 혹은 “[A-Z]”

URL Dispatcher

  • “특정 URL 패턴 => View”의 List
  • 프로젝트/settings.py에서 최상위 URLConf 모듈을 지정
    • 최초의 urlpatterns로부터 include를 통해, TREE구조로 확장
  • HTTP 요청이 들어올 때마다, 등록된 urlpatterns 상의 매핑 리스트를 처음부터 순차적으로 URL 매칭을 시도
    • 매칭이 되는 URL Rule이 다수 존재하더라도, 처음 Rule만을 사용
    • 매칭되는 URL Rule이 없을 경우, 404 Page Not Found 응답을 발생

urlpatterns 예시

# shop/urls.py
from django.urls import path, re_path
from shop import views
urlpatterns = [
    path('', views.item_list, name='item_list'), # Item 목록
    path('new/', views.item_new, name='item_new'), # 새 Item
    path('<int:id>/', views.item_detail, name='item_detail'), # Item 보기
    re_path(r'^(?P<id>\d+)/$', views.item_detail, name='item_detail'), # 혹은 re_path 활용
    path('<int:id>/edit/', views.item_edit, name='item_edit'), # Item 수정
    path('<int:id>/delete/', views.item_delete, name='item_delete'), # Item 삭제
]

path()와 re_path()의 등장

장고 1.x에서의 Django.conf.urls.url() 사용이 2가지로 분리

  • django.urls.re_path()
    • django.conf.urls.url()과 동일
  • django.urls.path()
    • 기본 지원되는 Path Converters를 통해 정규표현식 기입이 간소화 (만능은 아닙니다)
    • 자주 사용하는 패턴을 Converter로 등록하면, 재활용면에서 편리
from django.conf.urls import url # django 1.x 스타일
from django.urls import path, re_path # django 2.x 스타일
urlpatterns = [
    # 장고 1.x에서의 다음 코드를
    url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
    # 다음과 같이 간소화 가능
    path('articles/<int:year>/', views.year_archive),
    # 물론 다음과 같이 동일하게 쓸 수 있습니다.
    re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
]

커스텀 Path Converter

  • 예시

      # 앱이름/converters.py
      class FourDigitYearConverter:
          regex = '\d{4}'
          def to_python(self, value): # url로부터 추출한 문자열을 뷰에 넘겨주기 전에 변환
              return int(value)
          def to_url(self, value): # url reverse 시에 호출
              return "%04d" % value
    
      # 앱이름/urls.py
      from django.urls import register_converter
    
      register_converter(FourDigitYearConverter, 'yyyy')
    
      urlpatterns = [
          path('articles/<yyyy:year>/', views.year_archive),
      ]
    
  • Slug Unicode

    • 정규 표현식
      • slug_unicode_re = [-\w]+
    • Converter

        from django.urls.converters import StringConverter
      
        class SlugUnicodeConverter(StringConverter):
            regex = r"[-\w]+"
      

$ 다양한 응답의 함수 기반 뷰 만들기

View

  • 1개의 HTTP 요청에 대해 1개의 뷰가 호출
  • urls.py/urlpatterns 리스트에 매핑된 호출 가능한 객체
    • 함수도 “호출 가능한 객체” 중의 하나
  • 웹 클라이언트로부터의 HTTP 요청을 처리
  • 크게 2가지 형태의 뷰
    • 함수 기반 뷰 (Function Based View) : 장고 뷰의 기본.
      • 호출 가능한 객체. 그 자체
    • 클래스 기반 뷰 (Class Based View)
      • 클래스.as_view() 를 통해 호출가능한 객체를 생성/리턴

View 호출

  • 호출 시, 인자
    HttpRequest 객체 및 URL Captured Values

    • 1번째 인자 : HttpRequest 객체
      • 현재 요청에 대한 모든 내역을 담고 있습니다.
    • 2번째 인자 : 현재 요청의 URL로부터 Capture된 문자열들
      • url/re_path 를 통한 처리에서는 모든 인자는 str 타입으로 전달
      • path 를 통한 처리에서는 매핑된 Converter의 to_python에 맞게 변환된 값이 인자로 전달
  • 호출에 대한 리턴값
    HttpResponse 객체

    • 필히 HttpResponse 객체를 리턴해야 합니다.
      • 장고 Middleware에서는 뷰에서 HttpResponse 객체를 리턴하기를 기대합니다. (다른 타입을 리턴하면 Middleware에서 처리 오류.)
      • django.shortcuts.render 함수는 템플릿 응답을 위한 shortcut 함수
    • 파일like객체 혹은 str/bytes 타입의 응답 지원
      • str 문자열을 직접 utf8로 인코딩할 필요가 없습니다.
        (장고 디폴트 설정에서 str 문자열을 utf8로 인코딩해줍니다.)
      • response = HttpResponse( 파일like객체 또는 str객체 또는 bytes객체 )
    • 파일 like 객체
      • response.write( str객체 또는 bytes객체 )

$ 적절한 HTTP 상태코드로 응답하기

  • HTTP 상태코드

    • 웹서버는 적절한 상태코드로서 응답해야 합니다.
    • 각 HttpResponse 클래스마다 고유한 status_code가 할당 (소스코드)
    • REST API를 만들 때, 특히 유용
  • 대표 상태코드

    • 200번대 : 성공
      • 200 : 서버가 요청을 잘 처리했다. => OK
      • 201 : 작성됨. 서버가 요청을 접수하고, 새 리소스를 작성했다.
    • 300번대 : 요청을 마치기 위해, 추가 동작을 취해야 한다.
      • 301 : 영구 이동, 요청한 페이지가 새 위치로 영구적으로 이동했다.
      • 302 : 임시 이동, 페이지가 현재 다른 위치에서 요청에 응답하고 있지만, 요청자는 향후 원 래 위치를 계속 사용해야 한다.
    • 400번대 : 클라이언트측 오류
      • 400 : 잘못된 요청.
      • 401 : 권한없음.
      • 403 (Forbidden) : 필요한 권한을 가지고 있지 않아서, 요청을 거부
      • 404 : 서버에서 요청한 리소스를 찾을 수 없다.
      • 405 : 허용되지 않는 방법. POST 방식만을 지원하는 뷰에 GET요청을 할 경우
    • 500번대 : 서버측 오류
      • 500 : 서버 내부 오류 발생
  • 200 응답의 예시

          from django.http import HttpResponse, JsonResponse
          from django.shortcuts import render
    
          def view1(request):
              return HttpResponse('Hello, Ask Company')
    
          def view2(request):
              return render(request, 'template.html')
    
          def view3(request):
              return JsonResponse({'hello': 'Ask Company'}
    
  • 302 응답의 예시

          from django.http import HttpResponseRedirect
          from django.shortcuts import redirect, resolve_url
    
          def view1(request):
              return HttpResponseRedirect('/shop/')
    
          def view2(request):
              url = resolve_url('shop:item_list') # URL Reverse 적용
              return HttpResponseRedirect(url)
    
          def view3(request):
              # 내부적으로 resolve_url 사용
              # 인자로 지정된 문자열이 url reverse에 실패할 경우,
              # 그 문자열을 그대로 URL로 사용하여, redirect 시도
              return redirect('shop:item_list')
    
  • 404 응답의 예시

          from django.http import Http404, HttpResponseNotFound
          from django.shortcuts import get_object_or_404
          from shop.models import Item
    
          def view1(request):
              try:
                  item = Item.objects.get(pk=100)
              except Item.DoesNotExist:
                  raise Http404
          # ...
    
          def view2(request):
              item = get_object_or_404(Item, pk=100) # 내부에서 raise Http404
          # ...
    
          def view3(request):
          try:
              item = Item.objects.get(pk=100)
          except Item.DoesNotExist:
              return HttpResponseNotFound() # 잘 쓰지 않는 방법
          # ...
    
  • 500 응답의 예시

    • 뷰에서 요청 처리 중에, 미처 잡지못한 오류가 발생했을 경우
      (IndexError, KeyError, django.db.models.ObjectDoesNotExist 등)

       from shop.models import Item
      def view1(request):
        # IndexError
        name = ['Tom', 'Steve'][100]
      
        # 지정 조건의 Item 레코드가 없을 때, Item.DoesNotExist 예외
        # 지정 조건의 Item 레코드가 2개 이상 있을 때, Item.MultipleObjectsReturned 예외
        item = Item.objects.get(pk=100)
      

$ 장고 쉘

쉘 실행 명령어

python manage.py shell

장고 진입점 설정

import os
# DJANGO_SETTINGS_MODULE 환경변수 미지정 시에, "project_name.settings"로 지정
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings')

django-extensions

pip install django-extensions
settings.INSTALLED_APPS 에 ‘django-extensions’ 추가

  • shell_plus 명령어
    구동 시에 장고 앱의 모델, 주요 함수들을 자동으로 임포트

  • SQL 출력 옵션
    shell 수행 결과에 대한 SQL 내역을 출력해준다.
    python manage.py shell_plus --print-sql


$ 장고 모델 (ORM)

ORM

object-relational mapping
장고 ORM은 RDBMS만 지원한다.

Django model

DB테이블과 python class를 1:1로 Mapping


$ 장고 모델 필드

기본 지원하는 모델 필드 타입

장고 공식문서 2.1
DB에 따라 지원하는 기능이 모두 다르다.

자주 쓰는 필드 공통 옵션

  • blank : 파이썬 validation시에 empty 허용 여부 (디폴트: False)
  • null (DB 옵션) : null 허용 여부 (디폴트: False)
  • db_index (DB 옵션) : 인덱스 필드 여부 (디폴트: False)
  • default : 디폴트 값 지정, 혹은 값을 리턴해줄 함수 지정
  • 사용자에게 디폴트값을 제공코자 할 때
  • unique (DB 옵션) : 현재 테이블 내에서 유일성 여부 (디폴트: False)
  • choices : select 박스 소스로 사용
  • validators : validators를 수행할 함수를 다수 지정
  • 모델 필드에 따라 고유한 validators들이 등록 (ex- 이메일만 받기)
  • verbose_name : 필드 레이블, 미지정시 필드명이 사용
  • help_text : 필드 입력 도움말

$ 마이그레이션을 통한 데이터베이스 스키마 관리

Migrations

python manage.py makemigrates appname
python manage.py sqlmigrate appname 0001_initial

언제 makemigrations를 하는가?

모델 필드 관련된 어떠한 변경이라도 발생 시에 마이그레이션 파일 생성
마이그레이션 파일은 모델의 변경내역을 누적하는 역할

적용된 마이그레이션 파일은 절대 삭제하시면 안 됩니다.
마이그레이션 파일이 너무 많아질 경우,
squashmigrations 명령으로 다수의 마이그레이션 파일을 통합할 수 있습니다

마이그레이션 Migrate (정방향 / 역방향)

python manage.py migrate <앱이름>
미적용 <마이그레이션-파일>부터 <최근-마이그레이션-파일>까지 정
방향으로 순차적으로 수행

python manage.py migrate <앱이름> <마이그레이션-이름>
지정된 <마이그레이션-이름>이 현재 적용된 마이그레이션보다
이후라면 정방향으로,
순차적으로 지정 마이그레이션까지 forward 수행 이전이라면 역방향으로,
순차적으로 지정 마이그레이션 이전까지 backward 수행

주의사항

팀원 각자 마이그레이션 파일을 생성하는 방법은 피하도록 하자.
충돌이 생기기 때문.

담당자 1명을 정한 후 전담하여 생성, 버전 관리
다른 팀원들은 받은 파일로만 migrate 수행

서버에 아직 반영 안 된 다수의 마이그레이션 파일이 있다면?

그대로 서버에 반영(migrate)하기보다는,
하나의 마이그레이션으로 합쳐서 적용하는 것을 권장.
방법 1) 미적용 마이그레이션 롤백 후, 모두 제거, 새로운 마이그레이션 파일 생성
방법 2) 미적용 마이그레이션들을 하나로 합치기 => squashmigrations


$ 장고 Admin을 통한 데이터 관리

admin 페이지 보안

/admin/ 디폴트 경로를 다른 주소로 변경 권장
또는 django-admin-honeypot 앱 사용

모델 클래스를 admin에 등록하기

from django.contrib import admin
from .models import Item

# 등록법 1
admin.site.register(Item) # 기본 ModelAdmin으로 동작

# 등록법 2
class ItemAdmin(admin.ModelAdmin):
  pass
admin.site.register(Item, ItemAdmin) # 지정한 ModelAdmin으로 동작

# 등록법 3
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
  pass

admin 모델 리스트에서 모델명 object 출력 변경

객체 _str_() 의 리턴값 활용

from django.db import models

class Item(models.Model):
  name = models.CharField(max_length=100)
  desc = models.TextField(blank=True)
  price = models.PositiveIntegerField()
  is_published = models.BooleanField(default=False)

def __str__(self):
  return f'<{self.pk}> {self.name}'

admin 모델 리스트 속성 정의

  • list_display

    • 모델 리스트에 출력할 컬럼 지정
  • list_display_links

    • list_display 지정된 이름 중에, detail 링크를 걸 속성 리스트
  • list_filter

    • 지정 필드값으로 필터링 옵션 제공
  • search_fields

    • admin내 검색UI를 통해, DB를 통한 where 쿼리 대상 필드 리스트
  • 예시 코드

      from django.contrib import admin
      from .models import Item
    
      @admin.register(Item)
    
      class ItemAdmin(admin.ModelAdmin):
        list_display = ['pk', 'name', 'short_desc', 'price', 'is_publish']
        list_display_links = ['name']
        list_filter = ['is_publish']
        search_fields = ['name']
    
      def short_desc(self, item):
        return item.desc[:20]
    

$ 모델을 통한 데이터 조회

Model Manager

데이터베이스 질의 인터페이스 제공
(default manager로서 ModelCls.objects)

# 생성되는 대강의 SQL 윤곽 à SELECT * FROM app_model;
ModelCls.objects.all()
# 생성되는 대강의 SQL 윤곽 à SELECT * FROM app_model ORDER BY id DESC LIMIT 10;
ModelCls.objects.all().order_by('-id')[:10]
# 생성되는 대강의 SQL 윤곽 à INSERT INTO app_model (title) VALUES (“New Title”);
ModelCls.objects.create(title="New Title")

QuerySet

SQL을 생성해주는 인터페이스

Iterable 한 객체
Chaining 지원 (Lazy Access)
Post.objects.all().filter(…).exclude(…).filter(…)

다양한 조회요청 방법

  • 조건에 맞는 쿼리셋 획득 준비
    • queryset.filter(…)
    • queryset.exclude(…)
  • 특정 모델객체 1개 획득 시도

filter, exclude

  • 1개 이상의 인자 지정, 모두 AND 연산으로 묶임.
  • 인자는 “필드명 = 조건값”
  • OR 조건을 쓰려면 django.db.models.Q 사용

  • filter
    Item.objects.filter(name="aaa", price=100)

    실행 쿼리문

    SELECT ...
    FROM "shop_item"
    WHERE ("shop_item"."name" = 'aaa' AND "shop_item"."price" = 100)
    
  • exclude
    Item.objects.exclude(name="aaa", price=100)

    실행 쿼리문

    SELECT ...
    FROM "shop_item"
    WHERE NOT ("shop_item"."name" = 'aaa' AND "shop_item"."price" = 100)
    
  • OR 사용 시
    Item.objects.filter(Q(name="aaa") | Q( price=100))

    실행 쿼리문

    SELECT ...
    FROM "shop_item"
    WHERE ("shop_item"."name" = 'aaa' OR "shop_item"."price" = 100)
    

    위 filter문에서 |연산자를 &연산자로 바꿔적으면 일반 filter와 동일하다.

필드 타입별 다양한 조건 매칭

  • 숫자/날짜/시간 필드
    필드명__lt = 조건값 —> 필드명 < 조건값
    필드명__lte = 조건값 —> 필드명 <= 조건값
    필드명__gt = 조건값 —> 필드명 > 조건값
    필드명__gte = 조건값 —> 필드명 >= 조건값

  • 문자열 필드
    필드명__startswith = 조건값 —> 필드명 LIKE “조건값%”
    필드명__endswith = 조건값 —> 필드명 LIKE “%조건값”
    필드명__contains = 조건값 —> 필드명 LIKE “%조건값%”
    필드명__istartswith = 조건값 —> 필드명 ILIKE “조건값%”
    필드명__iendswith = 조건값 —> 필드명 ILIKE “%조건값”
    필드명__icontains = 조건값 —> 필드명 ILIKE “%조건값%”

정렬 조건

  • 정렬 조건 지정 방법
    1. (추천) 모델 클래스의 Meta 속성으로 ordering 설정 : list로 지정
    2. 모든 queryset에 order_by(…) 에 지정
  • 정렬 조건을 추가하지 않으면 일관된 순서를 보장받을 수 없음
  • Meta 클래스 사용 예제

      class Item(models.Model):
          name = models.CharField(max_length=100)
          desc = models.TextField(blank=True)
          price = models.PositiveIntegerField()
          # ...
    
          class Meta:
              ordering = ['id']
    

쿼리셋 범위 조건

python의 슬라이싱 queryset.[start : end : step]
슬라이싱의 step을 사용하면 즉시 DB에 접근.
단, 역순 슬라이싱은 지원하지 않음 (이유는, 데이터베이스에서 지원 불가)


$ 모델을 통한 데이터 생성/수정/삭제

INSERT

방법1) 각 Post.objects의 create 함수 호출 ➔ 반환값 : 모델 객체

post = Post.objects.create(field1=value1, field2=value2, …)
post.pk # DB로부터 할당받은 pk

방법2) 각 모델 인스턴스의 save 함수 호출 ➔ 반환값 : None

post.pk # .pk => None
post = Post(field1=value1, field2=value2)
post.save()
post.pk # DB로부터 할당받은 pk

방법3) 관련 ModelForm을 통한 save 함수 호출 ➔ 반환값 : 모델객체

form = PostModelForm(request.POST, request.FILES)
if form.is_valid(): # 유효성 검사 수행
  post = form.save() # 내부적으로 모델객체.save() 호출하고, 그 객체를 리턴
  post.pk # DB로부터 할당받은 pk

UPDATE

방법1) 개별 모델 인스턴스의 save 함수 호출 ➔ 반환값: None

post = Post.objects.all().first()
post.field1 = new_value1
post.field2 = new_value2
post.save() # 변경된 필드에 한해서 수행되는 것이 아니라, 모든 필드에 대해서 수행

방법2) QuerySet의 update 함수 호출 ➔ 반환값 : 업데이트한 Row 개수 (정수)

qs = Post.objects.all().filter(...).exclude(...)
qs.update(field1=new_value1, field2=new_value2)

방법3) 관련 ModelForm의 save 함수 호출 ➔ 반환값 : 모델객체

form = PostForm(request.POST, request.FILES, instance=post)
if form.is_valid(): # 유효성 검사 수행
  post = form.save() # 내부적으로 모델객체.save() 호출하고, 그 객체를 리턴

DELETE

방법1) 개별 모델 인스턴스의 delete 함수 호출 ➔ 반환값 : 삭제된 Record 갯수

post = Post.objects.all().first()
post.delete()

방법2) QuerySet의 delete 함수 호출 ➔ 반환값 : 삭제된 Record 갯수

qs = Post.objects.all().filter(...).exclude(...)
qs.delete()

비슷한 동작, 다른 성능

예시1)과 같은 행동은 절대 하지 말자.

예시1) 각 인스턴스 별로 별도의 SQL

qs = Post.objects.all()
for post in qs:
  post.title = 'changed title'
  post.save()

예시2) 하나의 SQL

qs = Post.objects.all()
qs.update(title='changed title')

$ 관계를 표현하는 모델 필드

ForeignKey

N 관계에서 N측에 명시
Post:Comment, User:Post, User:Comment,

  • to : 대상모델
    • 클래스를 직접 지정하거나,
    • 클래스명을 문자열로 지정. 자기 참조는 “self” 지정
  • on_delete : Record 삭제 시 Rule (공식문서)
    • CASCADE : FK로 참조하는 다른 모델의 Record도 삭제 (장고 1.X에서의 디폴트값)
    • PROTECT : ProtectedError (IntegrityError 상속) 를 발생시키며, 삭제 방지
    • SET_NULL : null로 대체. 필드에 null=True 옵션 필수.
    • SET_DEFAULT : 디폴트 값으로 대체. 필드에 디폴트값 지정 필수.
    • SET : 대체할 값이나 함수 지정. 함수의 경우 호출하여 리턴값을 사용.
    • DO_NOTHING : 어떠한 액션 X. DB에 따라 오류가 발생할 수도 있습니다.

올바른 User 모델 지정

# django/contrib/auth/models.py
class User(AbstractBaseUser):
...

# blog/models.py
class Post(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) # default는 auth.User
title = models.CharField(max_length=100)

>>> user.post_set.all()

디폴트는 “모델명소문자_set”

from django.db import models
class Post(models.Model):
  title = models.CharField(max_length=100)
  content = models.TextField()

class Comment(models.Model):
  post = models.ForeignKey(Post, on_delete=models.CASCADE)
  message = models.TextField()
>>> comment.post
>>> post.comment_set.all() <==> Comment.objects.filter(post=post)

related_name 디폴트 명은 앱이름 고려 X, 모델명만 고려
makemigrations 명령이 실패
다음의 경우, user.post_set 이름에 대한 충돌

blog앱 Post모델, author = FK(User)
shop앱 Post모델, author = FK(User)

디폴트 양식 앞에 앱 이름을 붙여서 충돌을 피하자.

FK(User, …, related_name="blog_post_set")
FK(User, …, related_name="shop_post_set")

ForeignKey.limit_choices_to

Form을 통한 Choice 위젯에서 선택항목 제한 가능.

  • dict/Q 객체를 통한 지정 : 일괄 지정
  • dict/Q 객체를 리턴하는 함수 지정 : 매번 다른 조건 지정 가능

ManyToManyField에서도 지원

OneToOneField

1:1 관계에서 어느 쪽이라도 가능
User : Profile

# django/contrib/auth/models.py
class User(AbstractBaseUser):
...

# accounts/models.py
class Profile(models.Model):
  author = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

디폴트는 “모델명소문자_set”

# accounts/models.py
class Profile(models.Model):
  author = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
  phone = models.CharField(max_length=11, blank=True)
  birth = models.DateField(null=True)
>>> profile.author  
>>> user.profile

ManyToManyField

M :N 관계에서 어느 쪽이라도 가능
Post : Tag
ManyToManyField(to, blank=False)

# 방법 1)
class Post(models.Model):
  tag_set = models.ManyToManyField('Tag', blank=True)

class Article(models.Model):
  tag_set = models.ManyToManyField('Tag', blank=True)

class Tag(models.Model):
  name = models.CharField(max_length=100, unique=True)

# 방법 2) 가독성이 더 높음
class Post(models.Model):
  ...

class Article(models.Model):
  ...

class Tag(models.Model):
  name = models.CharField(max_length=100, unique=True)
  post_set = models.ManyToManyField('Post', blank=True)
  article_set = models.ManyToManyField('Article', blank=True)

$ django-debug-toolbar를 통한 SQL 디버깅

설치하기

pip install django-debug-toolbar

# project_name/settings.py

# ...
INSTALLED_APPS = [
  # ...
  'django.contrib.staticfiles',
  # ...
  'debug_toolbar',
]
# ...
MIDDLEWARE = [
  # ...
  'debug_toolbar.middleware.DebugToolbarMiddleware',
  # ...
]
# ...
INTERNAL_IPS = [
'127.0.0.1',
'10.0.2.15',  
# wsgi 등을 이용하여 localhost 외에 다른 IP로 연결 시 추가가 필요하다.
]

# ...
# project_name/urls.py

from django.conf import settings

# ...

if settings.DEBUG:
  import debug_toolbar
  urlpatterns += [
      path('__debug__/', include(debug_toolbar.urls)),
  ]
# ...

자세한 내용은 공식문서 참고

  • 주의사항

    DDT의 html/script 디폴트 주입 타겟이 body 태그이기 때문에,
    웹페이지의 템플릿에 필히 body 태그가 있어야만, DDT가 동작


$ 장고 Logging과 SQL Logging 처리

전통적인 웹페이지에서는 django-debug-toolbar만으로도 SQL 내역을 조회할 수 있지만,
Ajax로 구현되는 페이지에서는 SQL 내역을 살펴보기 어렵기 때문입니다.

# project_name/settings.py

# ...
LOGGING = {
  'version': 1,
  'disable_existing_loggers': False,
  'filters': {
      'require_debug_true': {
          '()': 'django.utils.log.RequireDebugTrue'
      },
  },
  'handlers': {
      'console': {
          'level': 'DEBUG',
          'filters': ['require_debug_true'],
          'class': 'logging.StreamHandler',
      },
      'write_to_file': {
          'level': 'DEBUG',
          'filters': ['require_debug_true'],
          'class': 'logging.FileHandler',
          'filename': 'db.log',
      },
  },
  'loggers': {
      'django.db.backends': {
          'handlers': ['write_to_file'],
          'level': 'DEBUG',
      },
      'blog': {
          'handlers': ['console'],
          'level': 'DEBUG',
      },
      'shop': {
          'handlers': ['console'],
          'level': 'DEBUG',
      },
  },
}

$ 장고 템플릿 엔진

왜 템플릿을 사용하는가?

  • 문자열 조합을 쉽게 하기 위해 사용
    • 단지 HTML 응답 뿐 아니라, 다양한 문자열 조합에 사용할 수 있습니다.
      (이메일, 푸쉬메세지, SMS메세지 등)
  • 템플릿 기능에 제한을 두어, 비즈니스 로직을 템플릿에 구현함을 방지
    • 비즈니스 로직은 Model에
    • Form/Model Form을 통한 유효성 검사 및 저장을 권장.
    • 다른 템플릿엔진을 씀으로서, 이러한 제약에서 벗어날 수 있으나, 비권장.

템플릿 엔진 활용 시 주요 코드

  • django.shortcuts.render => HttpResponse
  • django.template.loader.render_to_string => str

django 의 빌트인 백엔드

  • django.template.backends.django.DjangoTemplates
    • 장고의 일반 템플릿 엔진
    • 인자 없는 함수만 호출 가능
      (소괄호 사용하지 않음. Callable Object 인지 내부적으로 확인함)
  • django.template.backends.jinja2.Jinja2
    • 부분 지원

settings.TEMPLATES 설정 리스트

# project_name/settings.py

#...
TEMPLATES = [
  {
      'BACKEND': 'django.template.backends.django.DjangoTemplates',
      # 어떤 백엔드 엔진을 쓸 것인지
      'DIRS': [
              os.path.join(BASE_DIR, 'project_name', 'templates'),
          ],
      # 템플릿을 둘 디렉토리 경로 리스트 (by File System Template Loader)
      'APP_DIRS': True,
      # 앱 별 templates 경로 추가 여부 (by App Directory Template Loader)
      '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',
          ],
      },
  },
]
#...

권장사항

  • 각 앱들을 위한 템플릿은 각 “앱/templates/” 경로에 배치
    • 장고 앱은 재사용성에 포커스가 맞춰져 있기때문.
  • 프로젝트 전반적으로 사용할 템플릿은 DIRS에 명시한 경로에 배치

템플릿 태그 / 필터

태그
{% 태그명 '인자1' '인자2' .. %}
필터
{{ 값 | 필터1:인자 | 필터2:인자 | 필터3 .. }}
공식문서

$ Jinja2 템플릿 엔진도 같이 써보기

jinja2

pip install jinja2
DTL과 비교하여 Jinja2에서는 함수 호출 시에 괄호를 사용하여, 자유도를 높였다

구분 jinja2 Dango Template Language
함수 호출 {{post.get_comments()}} {{post.get_comments}}
필터 {{tags | join(“,”)}} {{tags | join:”,”}}

django-jinja

장고/Jinja2 통합 라이브러리
(장고 기본에서의 Jinja2 지원만으로는 부족)
공식문서

pip install django-jinja

#project_name/setting.py

# ...
INSTALLED_APPS = [
# ...
'django_jinja',
# ...
]
# ...
TEMPLATES = [
  {
      "BACKEND": "django_jinja.backend.Jinja2",
      "APP_DIRS": True,
      "OPTIONS": {
          "match_extension": ".jinja",
          "context_processors": [
            "django.contrib.auth.context_processors.auth",
            "django.template.context_processors.debug",
            "django.template.context_processors.i18n",
            "django.template.context_processors.media",
            "django.template.context_processors.static",
            "django.template.context_processors.tz",
            "django.contrib.messages.context_processors.messages",
          ],
      },
  }, # .jinja 의 확장자를 가지면 django-jinja를, 아니면 아래 DTL을 사용
  {
      'BACKEND': 'django.template.backends.django.DjangoTemplates',
      # ...
  },
]
# ...

$ 장고가 템플릿 파일을 찾는 원리

Django Template Loader (공식문서)

  • 다수 디렉토리 목록에서 지정 상대경로를 가지는 템플릿을 찾아줌.
    • 다양한 로더가 지원되며, 템플릿 설정의 OPTIONS내 loaders를 통해 각기 활성화
    • 우선순위: 파일 시스템 로더 > 앱 디렉토리 로더
  • 다양한 템플릿 로더
    • 파일 시스템 로더
      • settings.TEMPLATES의 DIRS=[] 설정에 의존
      • 지정 경로 리스트를 리스트에추가
    • 앱 디렉토리 로더
      • settings.TEMPLATES의APP_DIRS=True 설정에 의존
      • 각 장고 앱 디렉토리 내, templates경로를 리스트에 추가
    • cached 로더 (참고)
      • 템플릿은 매번 파일 읽기/컴파일 과정이 들어가는데, 이를 로컬 메모리에 캐싱

템플릿 디렉토리 리스트

  • 템플릿 로더는 서버를 시작할 때마다, 템플릿 로더 설정에 기반하여 템플릿 디렉토리 리스트를 생성.

    • settings.DEBUG=True일 때는 소스코드가 변경될 때 마다 서버를 재시작하므로, 템플릿 디렉토리 리스트도 새로 생성된다.
  • find template 로직 수행 시, 이미 생성된 템플릿 디렉토리 리스트에서 템플릿을 순차적으로 찾는다.

디렉토리 매칭 메커니즘

템플릿 디렉토리 리스트 예시

project_name/templates(폴더 이름이 달라도 가능하나, 이왕이면 일관성있게!)/
blog/templates
shop/templates

다음 함수가 호출될 때, 아래 내용을 순차적으로 매칭 시도한다.

render(request, "blog/post_list.html")

project_name/templates
blog/templates/blog/post_list.html
shop/templates/blog/post_list.html

app/templates/app은 namespace 역할

  • 만약 다음과 같이 템플릿 파일이 있을 경우,
    • project_name/templates/post_list.html
    • blog/templates/post_list.html
    • shop/templates/post_list.html
  • shop/templates/post_list.html 파일 활용을 위해
    • render(request,“post_list.html”)로 호출해보지만,
    • 사용되는 것은 매번 project_name/templates/post_list.html 입니다.
  • 권장
    • 앱 내 디렉토리 배치는 app/templates/app/ 구조를 필히 쓰세요.
    • 그리고 “app/파일명” 구조로 활용하세요.

$ 템플릿 상속을 통한 중복 제거

템플릿 상속의 특징

  • 기본 특징
    • 상속은 여러단계로 이뤄질 수 있다.
    • block에는 이름을 할당해야 하며, 이름을 통해 구분하며, 한 템플릿 내에서 그 이름은 유일해야 한다.
  • 부모 템플릿
    • 전체 레이아웃을 정의
    • 자식 템플릿이 비집고 들어올 수 있는 영역(block)을 다수 정의할 수 있다.
      • block이 없다면, 자식 템플릿은 상속만 받을 뿐, 어떠한 변경도 수행할 수 없다.
  • 자식 템플릿
    • 상속 받을 부모를 1개 지정할 수 있다.
    • 상속 받은 부모에서 정의한 block에 대해, block 내용을 재정의하여 그 내용을 추가/변경/제거
      • block 바깥에 정의한 내용은 모두 무시된다.
      • 부모가 정의하지 않은 block을 재정의해도 이는 무시된다.

템플릿 태그

  • {% extends “부모 템플릿 경로” %}
  • {% block 블럭이름 %} 블럭 내용 {% endblock %}
    • 부모에서 사용하면, 블럭 정의
    • 자식이 사용하면, 부모의 블록 재정의
  • {{ block.super }}
    • 자식 템플릿에서 사용할 때, 지정 위치에 부모 block 내용 출력

기본적으로 2단계의 상속을 추천

  • 프로젝트 전반적인 레이아웃 템플릿: project_name/templates/layout.html
    • 각 앱 별 레이아웃 템플릿: app/templates/app/layout.html -> layout.html상속
      • 템플릿#1: app/templates/app/post_list.html -> app/layout.html상속
      • 템플릿#1: app/templates/app/post_detail.html -> app/layout.html상속
      • 템플릿#1: app/templates/app/post_form.html -> app/layout.html상속

$ 자주 사용하는 템플릿 필터

장고 템플릿 필터 (공식문서)

  • 함수 형태로 구현하여, 템플릿에 등록
  • 언제 사용하는가?
    • 템플릿 단에서 출력된 값에 대해서, 값 변환이 필요할 때
    • ex) 개행 적용, 숫자에 콤마찍기, 소스코드 highlight 등
  • 필터에서 취하는 인자 (1개~2개)
    • 인자 A : 변환할 값
    • 인자 B (옵션) : 추가 옵션

예시

  • add
    다양한 타입에 대한 +연산 제공
    {{ value|add:"2" }}

    # django/template/defaultfilters.py#658
    @register.filter(is_safe=False)
    def add(value, arg):
      """Add the arg to the value."""
      try:
        return int(value) + int(arg)
      except (ValueError, TypeError):
        try:
          return value + arg
        except Exception:
          return ''
    
  • cut
    주어진 문자열에서 arg 모두 제거
    {{ value|cut:" " }}

    # django/template/defaultfilters.py#378
    @register.filter
    @stringfilter
    def cut(value, arg):
      """Remove all values of arg from the given string."""
      safe = isinstance(value, SafeData)
      value = value.replace(arg, '')
      if safe and arg != ';':
        return mark_safe(value)
      return value
    

유용한 값 처리

  • default : 값이 False판정일 때, 인자로 지정한 디폴트값을 사용.
    • default_if_none : 값이 None판정일 때, 인자로 지정한 디폴트값을 사용
  • filesizeformat : 숫자를 파일크기로서 단위를 붙임.
    • 지원 단위 : KB, MB, GB, TB, PB
    • ex) 123456789 => 117.7MB
  • join : 문자열 join과 유사
    • 리스트를 하나로 합쳐서 표현하고자 할 때
  • linebreaks : 1개 개행은 <br>태그, 2개 개행은 <p>태그로 변환
    • 줄바꿈 시에 유용
  • linebreaksbr : 모든 개행을 <br>태그로 변환
  • pprint : pprint.pprint() 래핑. 디버깅 목적의 출력.
  • truncatechars : 지정 글자수 만큼을 자르고, 말줄임표(…)를 붙임.
    • truncatecharts_html : 글자 단위로 html 요소를 살려서 자르기
    • truncatewords : 단어 단위로 자르기
    • truncatewords_html : 단어 단위로 html 요소를 살려서 자르기

json script (New in Django 2.1)

(공식문서 참고)


$ 자주 사용하는 템플릿 태그

장고 템플릿 태그 (공식문서)

  • 함수/클래스 형태로 구현하여, 템플릿에 등록
    • 원하는 개수 만큼의 인자를 받을 수 있습니다.
    • 템플릿에 따라 현재 템플릿 내 Context를 받을 수도 있습니다.
  • 언제 사용하는가?
    • 단순 값 변환이 아닌, 다양한 처리가 필요할 때
      • ex) for/endfor, if/endif, ifchanged 등
    • 템플릿 필터보다 많은 인자 처리가 필요할 때
  • 필터에서 취하는 인자 (0개 이상~)

기본 태그

  • extends : 템플릿 상속
    • load : 빌트인 템플릿태그/필터 외에 추가 로딩
      • 각 장고앱의 templatetags/ 디렉토리 내, 파일명을 지정
        (django/contrib/humanize/tempaltetags/humanize.py)
  • include : 템플릿 가져오기
    • 현재의 context가 그대로 전달.
    • with옵션을 통해 추가 키워드 인자 전달
      • only 추가옵션을 통해 지정
  • block … endblock : 블락 영역 지정
    • 템플릿 상속을 위한 영역 지정
  • comment … endcomment : 주석 영역 지정

조건문 / 반복문

  • if … elif … else … endif : 조건문
  • ifchanged … endifchanged : 대상 값이 변경될 시에, 렌더링
    • 인자없이 사용할 경우
      • 대상 값 : 해당 블락에 속한 템플릿 내역
    • 인자를 1개 이상 사용할 경우
      • 대상 값 : 인자 목록
  • for … empty … endfor : 반복문
    • empty는 해당 Iterable Object가 비었을 때, 수행

템플릿 태그

  • lorem : 무작위 채우기 텍스트 생성 (한글 로렘입숨 생성기)
    • {% lorem 횟수 단어단락선택 랜덤여부 %}
    • 횟수 : 디폴트 1
    • 단어단락선택 : 단어(w), HTML단락(p), PlainText단락(b, 디폴트)
  • spaceless … endspaceless
    • HTML 태그 사이의 공백을 모두 제거
  • url : URL Reverse
  • verbatim … endverbatim
    • 해당 영역에 대해서 템플릿 엔진 처리를 하지 않습니다.
  • with … endwith
    • 템플릿 단계에서 변수 생성 문법
총 {{ business.employees.count }}명의 직원이 있습니다.
{{ business.employees.count }}명의 직원 중에 3명이 업무 중에 있습니다.

{% with total=business.employees.count %}
총 {{ total }}명의 직원이 있습니다.
{{ total }}명의 직원 중에 3명이 업무 중에 있습니다.
{% endwith %}

$ Static 파일을 다루는 방법

Static 파일

  • 개발 리소스로서의 정적인 파일 (js, css, image 등)
  • 앱 / 프로젝트 단위로 저장/서빙

Static 파일, 관련 settings 예시 (공식문서)

각 설정의 디폴트 값

  • STATIC_URL = None
    • 각 static 파일에 대한 URL Prefix
      • 템플릿 태그 {% static “경로” %} 에 의해서 참조되는 설정
    • 항상 / 로 끝나도록 설정
  • STATICFILES_DIRS = []
    • File System Loader에 의해 참조되는 설정
  • STATIC_ROOT = None
    • python manage.py collectstatic 명령이 참조되는 설정
    • 여러 디렉토리로 나눠진 static파일들을 이 경로의 디렉토리로 복사하여, 서빙
    • 배포에서만 의미가 있는 설정

Static Files Finders

  • Template Loader와 유사
    • 설정된 Finders를 통해, static 템플릿이 있을 디렉토리 목록을 구성
      • 장고 서버 시작 시에만 1회 작성
    • 디렉토리 목록에서 지정 상대경로를 가지는 static 파일 찾기.
  • 대표적인 2가지 Static Files Finders
    • File System Finder
      • settings.STATICFILES_DIRS 설정값을 “디렉토리 목록”에 추가
    • App Directories Finder
      • “장고앱/static/” 경로를 “디렉토리 목록”에 추가

템플릿에서 static URL 처리 예시

<img src="/static/blog/title.png" />

위 예시처럼, settings.STATIC_URL, Prefix를 하드코딩하는 방식은 피하자.
대신 Template Tag를 통해 프로젝트 설정에 따라, 유연하게 static url prefix가 할당하자.
이런 식으로!

{% load static %}
...
<img src="{% static "blog/title.png" %}" />

개발 환경에서의 static 파일 서빙

  • 개발서버를 쓰고, and settings.DEBUG = True 일 때에만, 지원
    • 프로젝트/urls.py에 Rule이 명시되어 있지 않아도, 자동 Rule 추가
    • 이는 순수 개발목적으로만 제공
  • 개발서버를 쓰지 않거나, settings.DEBUG = False 일 때에는
    • 별도로 static 서빙 설정을 해줘야합니다.

static 서빙을 하는 여러가지 방법

  1. 클라우드 정적 스토리지나 CDN 서비스를 활용
  2. apache/nginx 웹서버 등을 통한 서빙
  3. 장고를 통한 서빙

collectstatic 명령

python manage.py collectstatic

  • 실 서비스 배포 전에는 필히 본 명령을 통해, 여러 디렉토리에 나눠져있는 static 파일들을 한 곳으로 복사
    • 복사하는 대상 디렉토리 : settings.STATIC_ROOT
    • 왜냐하면, 여러 디렉토리에 나눠 저장된 static 파일들의 위치는 “현재
      장고 프로젝트” 만이 알고 있음. 외부 웹 서버는 전혀 알지 못함.
    • 외부 웹 서버에서 Finder의 도움 없이도 static 파일을 서빙하기 위함.
    • 한 디렉토리에 모두 모여있기에, Finder의 도움이 필요가 없음

외부 웹 서버에 의한 static/media 컨텐츠 서비스

  • 정적인 컨텐츠는, 외부 웹 서버를 통해 처리하면, 효율적인 처리
  • 정적 컨텐츠만의 최적화 방법 사용
    • memcache/redis 캐시 등
    • CDN (Content Delivery Network)

nginx 웹 서버에서의 static 설정 예시

server {
# ...
location /static {
  autoindex off;
  alias /var/www/staticfiles; # settings.STATIC_ROOT
}
location /media {
  autoindex off;
  alias /var/www/media; # settings.MEDIA_ROOT
}
}

배포 시에 static 처리 프로세스

  1. “서비스용settings”에 배포 static 설정
  2. 관련 클라우드 스토리지 설정, 혹은 아파치/nginx static 설정
  3. 개발이 완료된 static파일을, 한 디렉토리로 복사
    • python manage.py collectstatic —settings=서비스용settings
      • Storage 설정에 따라, 한 번에 클라우드 스토리지로의 복사를 수행되기도 함.
    • settings.STATIC_ROOT 경로로 복사됨.
  4. settings.STATIC_ROOT경로에 복사된 파일들을 배포서버로 복사
    • 대상 : 클라우드 스토리지, 혹은 아파치/nginx에서 참조할 경로
  5. static 웹서버를 가리키토록 sesttings.STATIC_URL 수정

static 관련 라이브러리


$ Media 파일을 다루는 방법

Media 파일

  • FileField/ImageField를 통해 저장한 모든 파일
  • DB필드에는 저장경로를 저장하며, 파일은 파일 스토리지에 저장
    • 실제로 문자열을 저장하는 필드
  • 프로젝트 단위로 저장/서빙

Media 파일 처리 순서

  1. HttpRequest.FILES 를 통해 파일이 전달
  2. 뷰 로직이나 폼 로직을 통해, 유효성 검증을 수행하고,
  3. Field/ImageField 필드에 ”경로(문자열)”를 저장하고,
  4. settings.MEDIA_ROOT 경로에 파일을 저장합니다.

Media 파일, 관련 settings 예시 (공식문서)

각 설정의 디폴트 값

  • MEDIA_URL = “”
    • 각 media 파일에 대한 URL Prefix
      • 필드명.url 속성에 의해서 참조되는 설정
  • MEDIA_ROOT = “”
    • 파일필드를 통한 저장 시에, 실제 파일을 저장할 ROOT 경로

추천 settings

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

FileField와 ImageField

  • FileField
    • File Storage API를 통해 파일을 저장
      • 장고에서는 File System Storage만 지원. django-storages를 통해 확장 지원.
    • 해당 필드를 옵션 필드로 두고자 할 경우, blank=True 옵션 적용
  • ImageField (FileField 상속)
    • Pillow (이미지 처리 라이브러리)를 통해 이미지 width/height 획득
      • Pillow 미설치 시에, ImageField를 추가한 makemigrations 수행에 실패합니다.
  • 위 필드를 상속받은 커스텀 필드를 만드실 수도 있습니다.
    • ex) PDFField, ExcelField 등

모델 필드 예시

class Post(models.Model):
    author_name = models.CharField(max_length=20)
    title = models.CharField(max_length=100)
    content = models.TextField()
    photo = models.ImageField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

사용할 만한 필드 옵션

  • blank 옵션
    • 업로드 옵션처리 여부
    • 디폴트: False
  • upload_to 옵션
    • settings.MEDIA_ROOT 하위에서 저장한 파일명/경로명 결정
    • 디폴트 : 파일명 그대로 settings.MEDIA_ROOT 에 저장
  • 추천) 성능을 위해, 한 디렉토리에 너무 많은 파일들이 저장되지 않도록 조정하기
    • 동일 파일명으로 저장 시에, 파일명에 더미 문자열을 붙여 파일 덮어쓰기 방지

파일 업로드 시에 HTML Form enctype

  • form method는 필히 POST 로 지정
    • GET의 경우 enctype이 “application/x-www-form-urlencoded”로 고정
  • form enctype을 필히 “multipart/form-data” 로 지정
    • “applicaiton/x-www-form-urlencoded”의 경우, 파일명만 전송
<form action="" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <table>
      {{ form.as_table }}
    </table>
    <input type="submit" />
</form>

upload_to 인자

  • 파일 저장 시에 upload_to 함수를 호출하여, 저장 경로를 계산
    • 파일 저장 시에 upload_to 인자를 변경한다고 해서, DB에 저장된 경로 값이 갱신되진 않습니다.
  • 인자 유형
    • 문자열로 지정
      • 파일을 저장할 “중간 디렉토리 경로”로서 활용 (ex A/B/C)
    • 함수로 지정
      • “중간 디렉토리 경로” 및 “파일명”까지 결정 가능 (ex A/B/C/D.jpg)

파일 저장 경로

  • travel-20181225.jpg 파일을 업로드할 경우
    • MEDIA_ROOT/travel-20181225.jpg 경로에 저장되며,
    • DB에는 “travel-20181225.jpg” 문자열을 저장합니다.
  • 한 디렉토리에 파일을 너무 많이 몰아둘 경우, OS 파일 찾기 성능 저하.
    (디렉토리 Depth가 깊어지는 것은 성능에 큰 영향 없음.)
  • 필드 별로, 다른 디렉토리 저장 경로를 가지기
    • 대책 1) 필드 별로 다른 디렉토리에 저장
      • photo = models.ImageField(upload_to=“blog”)
      • photo = models.ImageField(upload_to=“blog/photo”)
    • 대책 2) 업로드 시간대 별로 다른 디렉토리에 저장
      • upload_to에서 strftime 포맷팅을 지원
      • photo = models.ImageField(upload_to=“blog/%Y/%m/%d”)

uuid를 통한 파일명 정하기 예시

import os
from uuid import uuid4
from django.utils import timezone

def uuid_name_upload_to(instance, filename):
    app_label = instance.__class__._meta.app_label # 앱 별로
    cls_name = instance.__class__.__name__.lower() # 모델 별로
    ymd_path = timezone.now().strftime('%Y/%m/%d') # 업로드하는 년/월/일 별로
    uuid_name = uuid4().hex
    extension = os.path.splitext(filename)[-1].lower() # 확장자 추출하고, 소문자로 변환
    return '/'.join([
      app_label,
      cls_name,
      ymd_path,
      uuid_name[:2],
      uuid_name + extension,
    ])

템플릿에서 media URL 처리 예시

  • 필드의 .url 속성을 활용하세요.
    • 내부적으로 settings.MEDIA_URL과 조합을 처리
      <img src="{{ post.photo.url }}" />
      
    • 필드에 저장된 경로에 없을 경우, .url 계산에 실패함에 유의. 그러니 안전하게 필드명 저장 유무를 체크
      {% if post.photo %}
        <img src="{{ post.photo.url }}" />
      {% endif %}
      
  • 참고
    • 파일 시스템 상의 절대 경로가 필요하다면 .path 속성을 활용하세요.
      • settings.MEDIA_ROOT와 조합

개발 환경에서의 media 파일 서빙

  • static 파일과 다르게, 장고 개발 서버에서 서빙 미지원
  • 개발 편의성 목적으로 직접 서빙 Rule 추가 가능
    from django.conf import settings
    from django.conf.urls.static import static
    # ...
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    

File Upload Handler

  • 파일크기가 2.5MB 이하일 경우
    • 메모리에 담겨 전달
    • MemoryFileUploadHandler
  • 파일크기가 2.5MB 초과일 경우
    • 디스크에 담겨 전달
    • TemporaryFileUploadHandler
  • 관련 설정
    • settings.FILE_UPLOAD_MAX_MEMORY_SIZE : 2.5MB

$ 개발 환경에서 static 캐싱 무효화하기

종종 이전 내역이 브라우저 캐싱되어, 변경된 내역이 반영되지 않을 때

이때, 변경된 내역이 반영되게 할려면?

  • 방법1) 브라우저의 캐시 내역을 강제로 비우기.
    • 크롬 브라우저의 ”강력 새로 고침”
  • 방법2) 해당 정적 파일 응답에서 Cache-Control 헤더 조절하기
  • 방법3) 해당 정적 파일의 파일명을 변경
  • 방법4) 해당 정적 파일, 요청 URL에 대해 Dummy QueryString을 추가
    • Query String 값이 변경되면, 브라우저에서는 새로운 리소스로 인식합니다.
    • 웹 프론트엔드에서 같은 URL로 Ajax 요청 시마다 dummy QueryString을 붙이는 것과 같은 이치

Dummy Query String을 붙이기 및 활용

from time import time
from django import template
from django.conf import settings
from django.templatetags.static import StaticNode


register = template.Library()

class FreshStaticNode(StaticNode):
  def url(self, context):
      url = super().url(context)
      if settings.DEBUG:
          url += '?_={}'.format(int(time()))
      return url

@register.tag('fresh_static')
def do_static(parser, token):
  return FreshStaticNode.handle_token(parser, token)

다음과 같이 static 대신 fresh_static 을 활용하여 디버그 모드에서 템플릿 응용이 가능하다

{% load fresh_static %}
...
{% block extra_head %}
    <link rel="stylesheet" href="{% fresh_static 'main.css' %}">
{% endblock %}

$ URL Reverse를 통해 유연하게 URL 생성하기

URL 계산은 장고에게 양보하자

<!-- 하드코딩 -->
<a href="/blog/">게시물 목록</a>

<!-- 유지보수에 훨씬 유리 -->
<a href="{% url 'blog:post_list' %}">게시물 목록</a>

URL Reverse를 수행하는 4가지 함수

url 템플릿태그

  • 내부적으로 reverse 함수를 사용
    {% url "blog:post_detail" 100 %}
    {% url "blog:post_detail" pk=100 %}
    # 문자열 URL
    

reverse 함수

  • 매칭 URL이 없으면 NoReverseMatch 예외 발생
    reverse('blog:post_detail', args=[100])
    reverse('blog:post_detail', kwargs={'pk': 100})
    # 문자열 URL
    

resolve_url 함수

  • 매핑 URL이 없으면 “인자 문자열”을 그대로 리턴
  • 내부적으로 reverse 함수를 사용
    resolve_url('blog:post_detail', 100)
    resolve_url('blog:post_detail', pk=100)
    resolve_url('/blog/100'})
    # 문자열 URL
    

redirect 함수

  • 매칭 URL이 없으면 “인자 문자열”을 그대로 URL로 사용
  • 내부적으로 resolve_url 함수를 사용
    redirect('blog:post_detail', 100)
    redirect('blog:post_detail', pk=100)
    redirect('/blog/100'})
    # HttpResponse 응답 (301 or 302)
    

모델 객체에 대한 detail 주소 계산

{% url "blog:post_detail" post.pk %}
resolve_url('blog:post_detail', pk=post.pk)
redirect('blog:post_detail', pk=post.pk)

매번 위과 같은 코드로 하실 수도 있겠지만,

{% post.get_absolute_url %}
resolve_url(post)
redirect(post)

이렇게 사용하실 수도 있습니다. 어떻게?

  • 가장 먼저 get_absolute_url() 함수의 존재 여부를 체크하고, 존재하면 reverse 대신 get_absolute_url() 함수를 실행하는 resolve_url 를 활용하면 가능.
# django/shortcuts.py

def resolve_rul(to, **args, **kwargs):
    if hasattr(to, 'get_absolute_url'):
      return to.get_absolute_url()
    # ...
    try:
      return reverse(to, args=args, kwargs=kwargs)
    except NoReverseMatch:
      # ...
  • get_absolute_url 함수는 아래처럼 작성할 수 있다.
    (Detail뷰에 대한 URLConf설정을 하자마자, 필히 get_absolute_url함수를 작성하길 추천)
from django.urls import reverse
# ...
class Post(models.Model):
  # ...
  def get_absolute_url(self):
      return reverse('blog:post_detail', kwargs={'id': self.pk})

$ 다양한 구동환경을 위한 settings / requirements.txt 분기

requirements.txt

의존성 있는 라이브러리 관리
pip install -r requirements.txt

  • pip에서는 설치할 패키지 목록을 파일을 통한 지정 지원
    • 일반적인 파일명이 requirements.txt
    • 다른 파일명/경로여도 괜찮음.

실행환경에 따라 다양한 패키지 목록이 필요

requirements.txt를 만들어본다면?

다른 파일을 포함할 수 있습니다. (-r 옵션)

  • 공통
  • 개발용
  • 서비스 2.0 개발용
  • 배포용 (공통)
  • 배포용 (AWS)
  • 배포용 (Azure)
  • 배포용 (Heroku)

settings란?

  • 다양한 프로젝트 설정을 담는 파이썬 소스 파일
    • 장고 앱 설정, DB 설정, 캐시 설정 등등
    • 디폴트 설정(django/conf/global_settings.py) 을 기본으로 깔고, 지정 settings를 통해 필요한 설정을 재정의
  • 장고 프로젝트 구동 시에 필히 DJANGO_SETTINGS_MODULE 환경변수를 통해, settings의 위치를 알려줘야합니다.
    manage.py
    wsgi.py

settings를 지정하는 2가지 방법

  • DJANGO_SETTINGS_MODULE 환경변수로 지정하기
    • 주의) OS마다/배포하는 방법마다 환경변수 세팅방법이 다릅니다.
    • 별도로 지정하지 않으면, manage.py/wsgi.py에 세팅된 설정값이 적용
  • manage.py 명령에서 —settings 옵션을 통해 지정하기
    • 환경변수 설정에 우선합니다.
    • 쉘> python manage.py (명령) --settings=project_name.settings.prod_heroku

settings를 파이썬 패키지로 만들기

  • 주의
    • settings.py 내 BASE_DIR 설정은 상대경로로 프로젝트 ROOT 경로를 계산.
  • 이전
    • 프로젝트/settings.py
  • 이후
    • 프로젝트/settings/
      • _init_.py
      • common.py
      • dev.py
      • prod_common.py
      • prod_aws.py
      • prod_heroku.py

$ 휴대폰 망을 통해 접속하는 방법

viewport meta 태그 적용으로 보기 좋게.

휴대폰으로 내부망의 개발서버에 접속하기

  1. 휴대폰을 같은 네트워크에 연결하거나
  2. 개발서버에 외부망 연결

$ 다양한 데이터베이스 엔진 연동하기

Databases in Django

(장고 2.1 기준 공식문서)

  • 장고 기본에서 다양한 RDB 관계형DB 백엔드 지원 (django/db/backends/)
    • sqlite3, mysql, oracle, postgresql
    • 관련 DB API 드라이버 추가 설치 필요
  • 데이터베이스와의 디폴트 인코딩
    • UTF-8
  • 멀티 데이터베이스를 지원

SQLite

  • 파일 기반의 데이터베이스
  • 파이썬에서 기본 지원
    (개발 초기에 보다 빠르게 개발을 시작할 수 있습니다.)
  • 실제 서비스에서는 다른 DB서버 필수.

MySQL

  • 스토리지 엔진 : InnoDB (장고 디폴트), MyISAM
  • DB API 드라이버 (PEP249Python Database API Specification v2.0을 구현)
    • mysqlclient (1.3.13)
      • 장고 공식문서에서 추천.
        pip install mysqlclient
    • MySQL Connector/Python
      • Oracle, Pure Python으로 개발
      • 장고 최신버전을 지원하지 않을 수도.
    • PyMySQL
      • Pure Python으로 개발

PostgreSQL

  • 장고의 Full Features를 지원하는 DB
  • 장고에서 PostgreSQL만을 위한 모델 필드도 지원 (공식문서)
    (ArrayField, HStoreField, JSONField, 각종 RangeFields)
  • 9.4 이상을 지원
  • DB API 드라이버

$ Heroku에 웹서비스 배포하기

Heroku

AWS 플랫폼에서 서비스되고 있는 PaaS 플랫폼

  • Plan
    • Free
      • 30분 동안 접속이 없으면, SLEEP
      • 550시간/월 제공 + (신용카드 정보 등록시) 450시간 추가 지원
    • Hobby ($7/월)
    • Standard/Performance ($25~)
  • 주의
    • Heroku 상의 디스크는 Persistent 스토리지가 아닙니다.
      • 매 프로세스 리부트마다 삭제됩니다.

배포할 내용

  • 장고 프로젝트는 gunicorn을 통한 구동
  • static 파일은 WhiteNoise를 통한 직접 서빙
    • AWS S3/CDN를 통한 정적파일 관리를 추천
  • 데이터베이스
    • Heroku 기본 지원 PostgreSQL 관리형 DB

배포 기본 설정

heroku-buildpack-python을 통한 배포가 이루어 집니다. (공식문서)

파이썬 런타임 지정

(공식문서)

  • 지원하는 버전 (2018년 11월 현재)
    • python-3.7.0
    • python-3.6.6 (디폴트)
    • python-2.7.15
  • 최상위 runtime.txt 파일에 지정
    • python-3.6.6

Procfile 파일 생성

gunicorn을 통한 기본옵션 실행

web: gunicorn project_name.wsgi --log-file -

설치할 라이브러리 명시

  • 최상위 requirements.txt 에 명시하면, Heroku에서 자동 수행
  • Heroku 필수 패키지
    • django-heroku (장고 2.0 이상을 지원)
      • DATABASE_URL 환경변수 처리,
      • 로깅 설정, staticfiles 처리,
      • ALLOWED_HOSTS에 [“*“] 추가,
      • whitenoise를 통한 정적파일 처리, Heroku CI 설정
    • whitenoise
      • 정적파일 직접 서빙
    • gunicorn 혹은 uwsgi
      • Python WSGI HTTP Server
    • psycopg2
      • PostgreSQL Driver

아직 작성 중..
개인프로젝트를 헤로쿠로 배포해보고 정리해서 올릴 예정입니다 :)