MVC 패턴, 장고의 구성

Django는 소프트웨어 공학에서 사용되는 MVC(Model-View-Controller)패턴과 유사한, Model-Template-View(MTV)패턴을 사용한다.

  • Model > Django Model
    • 데이터베이스의 테이블에 대응
    • 전체 데이터의 구조를 결정
  • View > Django Template
    • 클라이언트에 보여지는 부분을 담당
    • HTML/CSS/JavaScript
  • Controller > Django View
    • 사용자의 요청에 따라 Model에서 데이터를 얻어와서 View에 전달하는 역할, 즉 작동 로직

장고 프로젝트 구성

각 애플리케이션 단위 (app, 해당 애플리케이션 모듈명)

프로젝트를 기능에 따라 작게 나누는 단위. 의미상 같은 작업을 하는 단위로 구분한다. (테이블이 20개 이상이 되면 분리하는 것이 좋다.)

ex) Blog프로젝트 내부의 postcommentmember애플리케이션

Application Model (app/models.py, MVC에서 Model)

Model은 데이터베이스의 테이블의 형태를 정의한다. Django에서는 ORM(Object Relation Mapping)이라는 기법을 사용해서 데이터베이스를 다루며, 이 때 Model에 정의한 클래스 객체의 형태를 사용한다. Model의 내용이 변경되거나, 새로운 테이블을 만들 경우 실제 데이터베이스에 변경사항을 반영해야한다.

Application View (app/views.py, MVC에서 Controller)

View는 Model과 Template을 연결하는 로직을 담당한다. 뷰는 함수형 뷰(Function-based view)와 클래스형 뷰(Class-based view), 두 형태로 사용 가능하며, 어느쪽을 사용하는지는 개발자의 자유이다. 다만, 간단하고 일반적인 형태의 뷰는 장고가 제공하는 제네릭 뷰를 사용할 수 있으며, 또한 재활용면에서도 클래스형 뷰가 좀 더 유리하다. 초심자는 뷰의 동작을 알기 위해 FBV를 사용해서 시작하고, 이후 뷰에 대해 이해가 쌓이면 반복작업을 피할 수 있는 CBV로 진행하는것이 일반적이다.

프로젝트 공용 사용 단위

Template (templates/, MVC에서 View)

프로젝트 설정파일에 지정하며, 템플릿 파일들을 모아놓는 폴더 역할을 한다. 일반적으로는 프로젝트 디렉토리 바로 안쪽에 위치한다.

미디어 폴더 (media/)

개발시 사용자가 업로드하는 파일이 올라가는 위치. 실제 서비스에서는 독립된 파일전용 서버를 사용한다.

정적 파일 폴더 (static/)

프로젝트에서 사용할 정적 파일(CSSJavaScriptImage파일 등등)이 위치하는 폴더

프로젝트 설정 폴더 내 (프로젝트명/)

URLConf (urls.py)

URLConf는 Django로 들어온 URL요청을 View와 매핑해주는 urls.py파일을 말한다. 반드시 하나의 파일에 정의할 필요는 없으며, 여러 파일에 정의하고 프로젝트의 메인 URLConf에 불러와서 사용할 수 있다.

프로젝트 설정 (settings.py)

기본적인 사항은 장고가 자동으로 생성해주며, 필요한 부분만 등록해서 사용한다. 아래는 프로젝트에 맞춰 일반적으로 설정해야하는 항목.

  • Database설정
  • Template 디렉토리 설정
  • Static 디렉토리 및 URL 설정
  • Application(app) 등록
  • Timezone, Locale 등록

웹 사이트의 동작

사용자가 URL을 이용해 웹 서버에 접속을 요청하면,

  1. 웹 서버는 사용자의 URL을 보고 어떠한 요청이 들어왔는지 판단
  2. 요청이 장고를 통해 처리되어야 할 경우, 해당 요청을 처리하기 위해 장고에 요청내용을 전달
  3. 장고에서는 들어온 요청을 urls.py의 어떤 뷰(Controller)에서 요청을 처리할지 판단하여 해당 뷰의 로직을 실행
  4. 뷰는 로직을 실행하고 적절한 결과를 사용자에게 돌려줌. 이 과정에서 템플릿(View)을 사용
  5. 웹 서버는 해당 결과를 사용자에게 다시 전달
  6. 사용자는 요청에 대한 결과를 브라우저에서 받아 확인

Django girls tutorial

프로젝트 구성

# 프로젝트 폴더 생성
$ mkdir django_girls_class
$ cd django_girls_class

# 가상환경 적용
$ pyenv virtualenv 3.4.3 django_girls_class
$ pyenv local django_girls_class

# 장고 설치
$ pip install django

# 장고 프로젝트 생성
$ django-admin startproject mysite
$ mv mysite django_app

# 버전관리
$ vi .gitignore
$ git init
$ pip freeze > requirements.txt

$ l
total 24
drwxr-xr-x   7 limhm  staff   238B  2  7 11:09 .
drwxr-xr-x  13 limhm  staff   442B  2  7 11:06 ..
drwxr-xr-x   9 limhm  staff   306B  2  7 11:09 .git
-rw-r--r--   1 limhm  staff   2.6K  2  7 11:09 .gitignore
-rw-r--r--   1 limhm  staff    19B  2  7 11:07 .python-version
drwxr-xr-x   4 limhm  staff   136B  2  7 11:07 django_all
-rw-r--r--   1 limhm  staff    15B  2  7 11:09 requirements.txt

$ tree-python
.
├── django_app
│   ├── manage.py
│   └── mysite
│       ├── __init__.py
│       ├── settings.py
│       ├── urls.py
│       └── wsgi.py
└── requirements.txt

$ py .
# pycharm interpreter 설정

설정 파일에 기본 설정

# 개발단계에서 정적 파일을 저장할 디렉토리
STATIC_DIR = os.path.join(BASE_DIR, 'static')

# runserver에서 정적파일을 로드할 디렉토리 목록
STATICFILES_DIRS = [
    STATIC_DIR,
]

LANGUAGE_CODE = 'ko-kr'
TIME_ZONE = 'Asia/Seoul'
$ ./manage.py migrate
$ ./manage.py runserver

blog 앱 만들기

$ ./manage.py startapp blog
INSTALLED_APPS = [
    # 기본
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # 외부 라이러리

    # 직접 작성
    'blog.apps.BlogConfig',
]

blog 앱 모델 만들기

from django.db import models
from django.utils import timezone


# models.Model 상속받은 클래스는 데이터베이스를 생성
class Post(models.Model):
    # 저자, 외래키로 연결 (auth 애플리케이션의 User 모델 사용)
    author = models.ForeignKey('auth.User')
    # 제목, 길이 제한 텍스트
    title = models.CharField(max_length=200)
    # 내용, 길이 제한 없는 텍스트
    content = models.TextField()
    # 생성일자, auto_now_add 이용해서 객체가 생성될 때의 시간을 자동으로 기록
    created_date = models.DateTimeField(auto_now_add=True)
    # 발행일자,
    # 없는 값(null)을 혀용 : 데이터베이스를 위한 옵션
    # 빈 스트링(blank) 허용 : (admin 페이지) 위젯에 알려주기 위해 사용하는 옵션
    published_date = models.DateTimeField(blank=True, null=True)

    def __str__(self):
        return self.title

    def publish(self):
        """
        실행 시 해당 인스턴스의 published_date에 현재시각을 기록하고
        DB에 업데이트 해준다.
        """
        self.published_date = timezone.now()
        self.save()

데이터 베이스 생성

$ ./manage.py makemigrations
Migrations for 'blog':
  blog/migrations/0001_initial.py:
    - Create model Post

$ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Applying blog.0001_initial... OK

$ ./manage.py
Type 'manage.py help <subcommand>' for help on a specific subcommand.
Available subcommands:

$ ./manage.py help makemigrations
usage: manage.py makemigrations

관리자 계정

from django.contrib import admin
from .models import Post

admin.site.register(Post)
$ ./manage.py createsuperuser

admin 페이지에 접속해 post를 작성한다.

urls 설정

from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'', include('blog.urls')),
]

blog/urls.py 생성 후 작성합니다.

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^$', views.post_list, name='post_list')
]

blog 앱 뷰 만들기 (controller)

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


def post_list(request):
    # auto import : alt + enter
    return HttpResponse('post list view')

blog 앱 템플릿 만들기 (view)

djagno_app/templates/blog/post-list.html 생성 후 작성한다.

<!doctype html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Blog</title>
</head>
<body>
    <h1>Lim HM's Blog!</h1>
    <h3>Post list</h3>
</body>
</html>

경로를 추가한다.

# 템플릿을 저장할 디렉토리
TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates')

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            TEMPLATES_DIR,
        ],
        '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',
            ],
        },
    },
]

템플릿과 뷰 연결

from django.shortcuts import render
from .models import Post


def post_list(request):
    context = {
        'post_list': Post.objects.all(),
    }
    return render(request, 'blog/post-list.html', context)
<!doctype html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Blog</title>
</head>
<body>

    <h1>Lim's Blog!</h1>
    <h3>Post list</h3>
    {% for post in post_list %}
    <div>
        <h4>{{ post.title }}</h4>
        <p>{{ post.content }}</p>
    </div>
    {% endfor %}

</body>
</html>

쿼리셋

# 1. Post를 추가하고 저장해보기
# 2. id가 2인 Post 객체를 발행해보기
$ ./manage.py shell
(InteractiveConsole)
>>> from blog.models import Post
>>> Post.objects.all()
<QuerySet [<Post: first post>, <Post: second post>, <Post: hey!>, <Post: thrid post>]>

>>> from django.contrib.auth.models import User
>>> User.objects.all()
<QuerySet [<User: hm>]>
>>> me = User.objects.get(username='hm')
>>> me
<User: hm>
>>> Post.objects.create(author=me, title='sample title', content='Test')
<Post: sample title>

>>> Post.objects.get(id=2)
<Post: second post>
>>> p = Post.objects.get(id=2)
>>> p.publish()
>>> p
<Post: second post>
>>> q.published_date
datetime.datetime(2017, 2, 7, 5, 25, 36, 308620, tzinfo=<UTC>)

# QnA
# lte: less than equal
# gt : grater than
# 쿼리문에서 부등호를 쓸 수 없기 때문에
>>> from django.utils import timezone
>>> Post.objects.filter(published_date__lte=timezone.now())
[]

프로젝트 기능 개선 및 추가

1. 뷰 수정

발행된 포스트만 노출되도록 한다.

from django.shortcuts import render
from django.utils import timezone
from .models import Post


def post_list(request):
    context = {
        'post_list': Post.objects.filter(
            published_date__lte=timezone.now()
        ),
    }
    return render(request, 'blog/post-list.html', context)

2. Detail view 생성

  1. view에 post_detail 함수 추가, post_detail은 인자로 post_id를 받는다.
  2. urls.py에 해당 view와 연결하는 url을 추가, 정규표현식으로 패턴네임 post_id 이름을 갖는 수자 1개 이상의 패턴을 등록한다.
  3. post_detail 뷰가 원하는 URL에서 잘 출력되는지 확인 후 (stub 메서드 사용, 임시함수로 구조만 미리 짜두는 것)
    1. get 쿼리셋을 사용
    2. id 값이 인자로 전달된 post_id 와 같은 Post객체를 context에 담아 post-detail.html을 render한 결과를 리턴
  4. 템플릿에 post-detail.html을 만들고, 인자로 전달된 Post객체의 title, content, created_date, published date를 출력
# blog/views.py
def post_detail(request, post_id):
    return HttpResponse('post detail {}'.format(post_id))
# blog/urls.py
from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^$', views.post_list, name='post_list'),
    url(r'^post/(?P<post_id>[0-9]+)/$', views.post_detail, name='post_id'),
]
# blog/views.py
def post_detail(request, post_id):
    # ORM을 이용해서 id가 전달받은 post_id와 일치하는 Post객체를 post변수에 할당
    post = Post.objects.get(id=post_id)
    # 전달할 context 딕셔너리 키 'post'에 post 변수를 전달
    context = {
        'post': post,
    }
    # blog/post-detail.html 템플릿을 render한 결과를 리턴
    return render(request, 'blog/post-detail.html', context)
<!doctype html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Post detail</title>
</head>
<body>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
    <p>{{ post.created_date }}</p>
    <p>{{ post.published_date }}</p>
</body>
</html>

3. 예외 처리

def post_detail(request, post_id):
    # 방법 1
    # try:
    #     # ORM을 이용해서 id가 전달받은 post_id와 일치하는 Post객체를 post변수에 할당
    #     post = Post.objects.get(id=post_id)
    # except Post.DoesNotExist as e:
    #     return HttpResponse(e)

    # 방법 2
    post = get_object_or_404(Post, id=post_id)

    # 전달할 context 딕셔너리 키 'post'에 post 변수를 전달
    context = {
        'post': post,
    }
    # blog/post-detail.html 템플릿을 render한 결과를 리턴
    return render(request, 'blog/post-detail.html', context)

4. 링크 제작

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^post$', views.post_list, name='post_list'),
    url(r'^post/detail/(?P<post_id>[0-9]+)/$', views.post_detail, name='post_detail'),
]

urls.py 에 작성한 urlpatterns의 url() 함수url(정규표현식 패턴을 이용해, 해당 뷰로 이동시키고, name을 정의)한다. 템플릿 파일에서 사용하는 url 태그는 {% url ‘url name과’ 옵션파라미터 %}를 이용해 URL path를 반환한다. 원래의 순서를 역으로 하기 때문에 역참조라고 한다.

5. 템플릿 확장

base.html 생성하여 작성한다. 그 후 나머지 템플릿 파일을 정리한다.

6. 글 추가하는 페이지 추가

post-add.html 생성한다.

def post_add(request):
    return render(request, 'blog/post-add.html')
# shell에서 csrf_token 값을 확인할 수 있다.
<QueryDict: {'input_content': [''], 'input_title': [''], 'csrfmiddlewaretoken': ['6RyXdgyDYO1QdSRRLLVTT8JoCLQHS84x6YOwiihDa5qDtD3MevQMqgIYSKO5rWxS']}>

<QueryDict: {'input_content': ['내용입니다.'], 'csrfmiddlewaretoken': ['Wo6olitW19huFqaRDNZ4JNCiDtlh5u9iWvmXqkcWdqGhVbmM6xUXgVBSTsjFEiCD'], 'input_title': ['확인해봅시다.']}>
확인해봅시다.
내용입니다.

post_add 뷰의 기능을 개선시켜보자.

def post_add(request):
    if request.method == 'POST':
        # 요청의 method가 POST일 경우
        # 요청받은 데이터를 출력
        data = request.POST
        # html의 name이 키값
        title = data['input_title']
        content = data['input_content']
        author = User.objects.get(id=1)
        # print(request.POST)
        # ret = ','.join([title, content])

        # 받은 데이터를 사용해세 Post 객체를 생성
        p = Post(title=title, content=content, author=author)
        p.save()

        # redirect메서드는 인자로 주어진
        # URL 또는
        # urlpattern의 name을 이용해 만들어낸 URL을 사용해서
        # 브라우저가 해당 URL로 이동하도록 해줌
        return redirect('post_list')


    else:
        # 요청의 method가 POST가 아닐 경우
        # 글 쓰기 양식이 있는 템플릿을 렌더해서 리턴
        return render(request, 'blog/post-add.html')

QnA

  • DateTImeField에는 date 값이나 Null만 올 수 있다. str이나 그 외의 값들은 올 수 없다.
  • migrations 파일 관리
    • 상태 확인: ./manage.py showmigrations
    • 이전 상태로 migrate: ./manange.py migrate <app> 0002_initial
    • 최초 상태로 migrate: ./manage.py migrate <app> zero
  • git add . 과 git add -A 차이 : .은 현재 폴더 기준으로 하기 때문에 하위폴더에 있다면, 상위폴더의 파일들은 add 되지 않는다.