주의 ❗️

이 글은 장고 튜토리얼 문서를 보고 따라한 프로젝트지만, 내가 필요한 부분만 잘라서 기록해둔 것이기 때문에 따라하는 용도로 봐서는 안된다.

Django 가 뭐지?

장고(Django)는 파이썬 웹 서버 프레임워크이다. 아주 크고 탄탄하게 구성되어 있어서 마치 성과 같다고 표현한다. 왠만한 웹 서버 기능은 다 갖추어져 있어서 잘 꺼내서 쓰기만 하면된다. 장고 개발 철학이 '웹 기능을 만드는데 시간을 쓰지말자'는 것이다

설치

나중을 위해 가상환경에서 pip를 이용하여 설치하자

$ pyenv activate py34
(py34) $ pip install django

가상환경을 모른다면 여기로 👣

Django 프로젝트 생성

아래와 같이 입력하면, 프로젝트 파일이 만들어진다

(py34) $ django-admin startproject studyPy

프로젝트명은 studyPy 로 하였다

파일구조는 다음과 같다

├── manage.py
└── studyPy
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
  • manage.py : 장고에 담겨있는 기능을 사용할 수 있게 상호작용하는 파일
  • studyPy : 장고 전체적인 설정 파일들을 담고 있는 폴더
  • studyPy/__ init__.py : 이 폴더를 패키지 처럼 다루라고 알려주는 파일
  • setting.py : 장고의 환경을 설정 및 구성해주는 파일
  • urls.py : URL을 선언해주는 파일
  • wsgi.py : 웹서버의 진입점. 웹서버와 통신하기 위한 인터페이스

서버 구동

manage.py 파일을 이용하여 서버를 손쉽게 구동할 수 있다

(py34) $ python manage.py runserver {PORT}

(py34) $ ./manage.py runserver {PORT}

위의 두 명령어 중 아무거나 써도 상관없다 (난 개인적으로 아래가 편한다)

포트 번호를 안넣으면 default 로 8000 번으로 실행된다

앱 생성

장고에는 프로젝트안에 1개 이상의 을 생성할 수 있다
개념은 ..... 그냥 앱이다. 기본적으로 장고의 프로젝트 개념이 좀 큰거 같다

(py34) $ ./manage.py startapp polls

polls는 앱 이름인데 마음대로 정하면 된다
참고로 이 프로젝트는 장고 튜토리얼 문서에서 나온 내용과 같다

polls
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

이게 앱의 구조이다 (장고가 성이라면 앱은 집 정도?)

view는 MVC 패턴에서 Conroller에 해당하는데... 자세한건 따로 정리해야겠다

라우팅

웹서버의 기본 중 하나인 장고의 라우팅이다

views.py파일이 컨트롤러에 해당한다고 했는데, 간단하게 컨트롤러를 만들어보자

polls/views.py

from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello World with Django")

HttpResponse 라는 메서드가 있는데, Node에 res.send 랑 같다고 본다

이제 polls 라우터에 연결해주자 (polls 폴더에 urls.py 파일을 만들어주자)

polls/urls.py

from django.urls import path

from . import views

# polls URLconf (/polls/)
urlpatterns = [
    path('', views.index, name='index')
]

장고에서는 이런 라우터를 URLconf 라고 한다

이제 프로젝트 전체 라우터 에서 polls의 URLconf 를 바라보게 해야한다

프로젝트 전체 URLconf 는 studyPy 폴더에 있는 urls.py 이다

studyPy/urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]

include() 함수는 다른 URLconf 를 참조할 수 있도록 도와준다

자 이제 http://127.0.0.1:8000/polls 로 들어가면
studyPy/urls.py 전체 URLconf를 통하고
polls/urls.py polls URLconf를 통하여
polls/views.py index 함수가 실행되어
"Hello World with Django" 가 뜨게 된다!

SQLite

파이썬에는 기본적으로 SQLite가 내장되어있다.
장고도 이에 따라 SQLite 를 사용하도록 구성되어 있다.

다른 데이터베이스도 사용할 수 있지만 일단은 먼저 장고가 어떻게 구동하기 위한 목적이기 때문에 기본으로 지원하는 SQLite 부터 해보자

지역 시간 설정

데이터 베이스를 수정하기 전에 settings.pyTIME_ZONE 이라는 변수를 한국에 맞게 변경해야한다. 데이터를 저장할 때 생성 날짜와 시간을 자동으로 정할 경우, 한국 시간으로 저장하게 하기 위해서이다

https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
위 링크는 국가별 리스트이다

studyPy/settings.py

# 처음엔 UTC 라고 저장되어있다
TIME_ZONE : "Asia/Seoul"

테이블 생성

TIME_ZONE 윗 편을 보면 INSTALLED_APPS 라는 리스트가 있다. 이 리스트는 장고에서 활성화된 모든 장고 APP들을 나타낸다

  • django.contrib.admin - 관리용 사이트
  • django.contrib.auth - 인증 시스템
  • django.contrib.contenttypes - 컨텐츠 타입을 위한 프레임워크
  • django.contrib.sessions - 세션 프레임워크
  • django.contrib.messages - 메세징 프레임워크
  • django.contrib.staticfiles - 정적 파일을 관리하는 프레임워크
$ ./manage.py migrate

이 명령어를 실행하면 위의 INSTALLED_APPS 리스트 안의 App 에서 제공되는 데이터베이스 migrations (이후에 설명한다) 와 settings.py데이터베이스 설정에 따라 테이블이 생성된다

Root 경로에 db.sqlite 라는 파일이 생겼을 것이다

모델 생성

polls 앱의 모델을 만들어보자

polls/models.py

from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date_published')

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

Question 과 Choice 라는 데이터 모델을 만들었다

각 모델은 models.Model을 상속받는 객체이고 객체 내부 데이터들은 models의 Field 로 타입을 지정한다. 각 Field 마다 필수 인자가 있을 수 있다

ex ) CharField 는 max_length 인자를 꼭 넣어야 한다

모델 활성화

먼저 모델을 활성화 하기 전에, 장고에게 polls 라는 앱의 존재를 알려야한다

아까 전에 얘기한 settings.py 의 INSTALL_APPS 에 polls를 추가시켜주자

studyPy/settings.py

INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

파일 확인을 해보면 polls 폴더에 apps 파일이 있고 그 안에 PollsConfig 라는 클래스가 있다. 이걸 추가 시켜주자

이제 장고가 polls 의 존재를 알게 되었다

자 이제 아까 얘기한 migration 을 사용할 차례이다❗️

migration ?
migration 은 사전의 뜻은 '이동', '이주' 이던데, 내 생각엔 모델의 변경사항 을 뜻하는 것 같다. (아님말고😝)

polls 의 모델이 생성되고 변경되었으니 이 변경사항(=migration) 을 만들자

$ ./manage.py makemigrations polls

이렇게하면 migration이 생성되었다

생성된 migration 은 polls/migrations/0001_initial.py에 저장되었을 것이다. 확인해봐도 되지만 만들때마다 확인할 필요는 없다

자 이제 migrate 를 시켜주면 추가된 migration 에 따라 테이블이 생성될 것이다

 $ ./manage.py migrate 

추가로 실행되는 SQL 문을 확인하고 싶다면 아래의 명령어를 입력하면된다

 $ ./manage.py sqlmigrate polls 0001

sqlmigrate 는 실제로 데이터베이스를 수정하지 않고 결과물만 콘솔에 출력해준다

관리자

나는 django-admin 으로 장고 프로젝트를 만들었다. 이 django-admin 은 관리자 사이트가 따로 생성한다.

http://127.0.0.1:8000/admin

image.png

이 관리자 사이트를 들어가기 위해서는 admin 계정이 필요하다

관리자 생성하기

$ ./manage.py createsuperuser

// 아래 양식대로 계정을 만들자

Username :
Email address:
Password: 
Password (again): 

이렇게 생성한 관리자 계정으로 로그인하면 아래의 페이지가 나온다

image.png
이 페이지에서는 데이터베이스를 UI로 수정할 수 있다

이 기능은 settings.py의 INSTALLED_APPS 에 있던 'django.contrib.auth' 가 지원해준다

관리자 사이트에 모델 추가하기

그런데 전에 만들고 데이터베이스에 넣어줬던 Question, Choice 모델이 안보인다😱

간단하게 admin.py에 등록해주면 관리자 페이지에 나타나게 된다

polls/admin.py

from django.contrib import admin

from .models import Question, Choice

admin.site.register(Question)
admin.site.register(Choice)

아래처럼 Question과 Choice 가 나타난다 😄

image.png

View

장고에서 뷰(View)는 MVC 패턴의 Controller와 같다

먼저 만들 뷰는 다음과 같다

  • index -- 목차. 최근 Question들을 표시
  • detail -- 질문 내용과 투표 버튼을 표시
  • result -- 특정 질문에 대한 결과를 표시
  • vote -- 특정 질문에 대해 특정 선택을 할 수 있는 기능

polls/views.py 에 뷰를 추가하자

from django.shortcuts import render
from django.http import HttpResponse

response = "Question %s : %s"

def index(request):
    return HttpResponse("Hello World with Django")

def detail(request, question_id):
    return HttpResponse(response % (question_id, "Detail"))

def results(request, question_id):
    return HttpResponse(response % (question_id, "Results"))

def vote(request, question_id):
    return HttpResponse(response % (question_id, "Vote"))

이제 polls/urls.py에 라우팅 연결을 해주자

from django.urls import path

from . import views

urlpatterns = [
    path('<int:question_id>/results/', views.results, name="results"),
    path('<int:question_id>/vote/', views.vote, name="vote"),
    path('<int:question_id>/', views.detail, name="detail"),
    path('', views.index, name='index'),
]

위에서 보듯이 <> 괄호로 URL 파라미터를 받을 수 있다

< int:question_id > 는 정수로 파라미터를 받지만
< question_id > 로 작성하면 문자열로 받는다

이제 runserver 를 실행하고 http://127.0.0.1:8000/polls/123/results/ 에 접속하면 뷰에서 반환한 문자열이 나오게 될 것이다

템플릿

이제 뷰를 통해 템플릿을 출력해보겠다

템플릿(Template) ?
HTML은 파이썬 코드를 담을 수 없다. 템플릿은 파이썬 코드를 HTML에 넣을 수 있게 해준다

먼저 polls 폴더에 templates 라는 폴더를 만들고, 그안에 polls 라는 폴더를 또 만든다 (이건 장고의 룰이니 그냥 정해진대로 해야한다)

polls/templates/polls/index.html

{% if latest_question_list %}
<ul>
  {% for question in latest_question_list %}
  <li>
    <a href="/polls/{{ question.id }}">{{ question.question_text }}</a>
  </li>
  {% endfor %}
</ul>
{% else %}
<p>No polls are available!</p>
{% endif %}

{% %} 안에 파이썬 코드를 넣고, {{ }} 안에 전달될 값을 넣으면 된다

자 이제 View에서 저 index 템플릿을 출력하는 코드를 작성하자

polls/views.py

# from django.http import HttpResponse
# 이제 HttpResponse 말고 shortcut인 render을 써보자
from django.shortcuts import render
from .models import Question


def index(request):
  # question 인스턴스들을 불러오는 코드
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    return render(request, 'polls/index.html', {'latest_question_list': latest_question_list})

이러면 이제 템플릿을 통해 HTML이 랜더링 된다😌

템플릿 확장

장고 템플릿에서 HTML 을 확장해서 사용할 수 있다

index.htmlbase.html을 확장시켜보자

polls/templates/polls/base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
  {% bloack content %}
  {% endblock content %}
</body>
</html>

polls/templates/polls/index.html

{% extends "base.html" %}

{% bloack content %}

<h1>Index Page</h1>

{% endblock content %}

이런식으로 확장하는데 중요한건 settings.py에서 템플릿 경로를 설정해줘야한다

settings.py

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

템플릿에서 하드코딩된 URL 제거

방금전 만든 파일에서 이런 코드가 있다

polls/templates/polls/index.html

<a href="/polls/{{ question.id }}">{{ question.question_text }}</a>

이렇게 하드코딩된 URL은 수많은 템플릿을 사용할 때 변경하기 어렵다

그래서 라우팅 부분에서 선언해줬던 name을 불러다 쓰면 이런 문제를 해결할 수 있다
polls/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('<int:question_id>/results/', views.results, name="results"),
    path('<int:question_id>/vote/', views.vote, name="vote"),
    path('<int:question_id>/', views.detail, name="detail"),
    path('', views.index, name='index'),
]

아까 전에 라우팅할 때 name을 선언했었다
그 중 detail을 사용해보자

polls/templates/polls/index.html

<a href="{% url 'detail' question.id %}">{{ question.question_text }}</a>
<!-- 이렇게 사용하면 아래와 같은 코드로 사용된다 -->
<a href="/polls/{{ question.id }}">{{ question.question_text }}</a>

이제 URL은 urls.py에서만 바꾸면 된다

템플릿 URL에 앱 이름 적용

방금처럼 템플릿을 URL의 하드코딩을 제거했다. 하지만 장고에서 쓰는 앱은 한개 이상이 될수 있으며, 앱 마다 URL 이름이 중복되면 첫 라우팅 쪽으로 처리된다

이런 문제를 해결하기 위해 앱에 namespace을 정해서 같은 이름의 URL을 앱마다 다르게 처리할 수 있다

polls/urls.py

from django.urls import path

from . import views

app_name = 'polls'

urlpatterns = [
  ...
]

이렇게 urls.py에 앱 이름을 정하고

polls/templates/polls/index.html

<a href="{% url 'polls:detail' question.id %}">{{question.question_text}}</a>

detail 앞에 앱 이름을 붙이면 된다. 이러면 App마다 URL이 구분될 것이다

Form 데이터 처리

일단 detail.html 파일에 간단한 POST Form 을 넣었다

polls/templates/polls/detail.html

<h1>{{ question.question_text }}</h1>

{% if error_message %}
  <p><strong>{{ error_message }}</strong></p>
{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">
  {% csrf_token %}
  {% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label>
  {% endfor %}

  <input type="submit" value="Vote">
</form>

이제 POST 메서드로 들어온 폼 데이터를 처리해보자

{% csrf_token %} : 해커의 CSRF 공격을 막아주는 코드이다

CSRF 공격이란?
사용자가 피싱사이트에 접근했을 때, 사용자의 권한을 가지고 지정된 사이트의 정보를 마음대로 조작하는 해킹방법이다

polls/views.py

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse

from .models import Question, Choice

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice"
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
  1. 위의 vote 뷰는 URL로 들어온 question_id 를 가진 Question을 찾는다
  2. POST로 들어온 id 와 같은 기본키를 가진 Choice를 찾아 그 Question에 단다.
  3. 만약 KeyError(초이스를 선택안했거나)나 Choice를 못찾으면 다시 Detail 페이지로 ErrorMessage와 함께 랜더링 시킨다
  4. 에러가 없다면 찾은 Choice의 votes 값을 1올리고 Result 페이지로 리다이렉트 한다
  • reverse 함수는 app_name처럼 하드코딩을 제거해주는 역할을 하고 '/polls/3/results/' 를 반환해준다 (3은 임의의 id)

Form Data 값은 아래 처럼 받으면 된다

request.POST['choice'] # choice는 폼데이터 name 이다

제너릭 뷰 (Generic View)

제너릭 뷰는 만들어진 템플릿 클래스를 상속받아서 최소한의 설정으로만 템플릿을 랜더링 시켜주는 뷰이다.

모든 뷰에 적용하기엔 어려움이 있고, 거의 변화 없는 비슷한 패턴에 사용하기에 적합한 도구라고 생각된다. 알아두면 생산성은 더 좋아질듯 하다

index 뷰와 detail 뷰를 제너릭 뷰로 변경해보자

polls/urls.py

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail')
]

먼저 urls.py에서 제너릭 뷰를 쓴다고 선언해두자
IndexView 와 DetailView는 views.py에서 만들 것이다

detail url을 보면 < int:question_id > 에서 < int:pk >로 변경했다. 이건 따로 question_id 에 대해 설정을 안하고 뷰에서 모델만 가르쳐 줄테니 알아서 primary key로 받아들여라는 것이다

polls/views.py

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'

...

IndexView는 generic.ListView를 상속 받았고
DetailView는 generic.DetailView를 상속받았다

내가 보기엔 ListView는 템플릿에 리스트를 보내주는 뷰 같고, DetailView는 모델 정보만 보내주는 뷰인 것 같다. 그리고 둘다 template_name 을 기본으로 설정한다

복잡한 템플릿에는 적용이 어려울 것 같고, 비슷한 패턴의 템플릿에 사용하자