청) 컨트롤러(뷰) - 글 목록과 글 보기 기능 (5편 1/3) - django 강좌

이제부터는 실제로 눈에 보이는 블로그 기능들을 만든다. 만들다 보면 기능 구현 뿐 아니라 django 관련 내용도 다룰 것이므로 각 글 분량이 길 것이다. 그래서 조금 눈을 돌리면 흐름을 잃어 헷갈릴 수 있는데, 좀 산만한 강좌 진행이더라도 함께 잘 나아갔으면 좋겠다.

이번 글에서는 django 에 있는 관리자(admin) 기능과 글 목록 기능을 만들 것이다.

urls.py : 주소 체계 설정 파일

요즘 웹 서비스에 유행하는 주소 체계는 ? 나 = 같은 기호를 쓰지 않고 마치 디렉토리(폴더)나 파일에 바로 접근하는 것처럼 꾸민다.

  • 예전 대세 : /list.php?id=hannal&page=1
  • 요즘 대세 : /list/hannal/page_1

이런 주소는 사람이 봤을 때 좀 더 깔끔하고 예쁠 뿐더러, 웹 검색기들도 주소 체계를 더 잘 파악한다고 해서 유행처럼 많이 쓰이고 있다. 이를 FancyURL 혹은 BeautifulURL 이라고 부르는데, 여러 웹 서버에서 다양한 방법으로 이런 기능을 제공한다.

  • IIS : ISAPI 필터를 이용하여 구현
  • Apache : mod_rewrite 모듈을 이용하여 구현

다시 말하면 FancyURL은 php이나 파이썬 등으로 만든 cgi 에서 구현하는 것이 아니라 웹 서버에서 제공하는 기능이다. 그렇다면 urls.py 는 무슨 역할일까? 굳이 urls.py 를 쓰지 않아도 웹 서버에서 제공하는 기능으로 FancyURL을 구현할 수 있는데 말이다. 답은 편리함이다. 웹 서버단에서 일일이 FancyURL 규칙이나 체계를 설정하는 것보다는 cgi 차원에서 주소를 분석하여 각 주소에 맞는 처리를 하도록 하는 것이 더 편리하다.

원리는 간단하다. 이용자가 서비스에 접근할 때는 접근하려는 url 이 있는데, 이 url을 지정한 대표 cgi 로 건내주면 cgi 는 url 을 분석해서 그에 맞는 처리를 하는 것이다. 요청 받는 모든 url 을 hannal.py 가 받는다고 하면, /list/hello 로 접근을 하든 /read/hannal 로 접근을 하든 hannal.py 가 이 주소를 받은 뒤 urls.py 에서 저런 주소 규칙이 있는지 찾아서 있으면 그 주소 규칙에 연결된 행동을 하고, 없으면 없다는 오류를 낸다.

urls.py 을 열어보자. 이렇게 생겼을 것이다.

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    # Example:
    # (r'^hannal/', include('hannal.foo.urls')),

    # Uncomment this for admin:
#     (r'^admin/', include('django.contrib.admin.urls')),
)

맨 위에 from django.conf.urls.defaults import * 는 바로 이전 글에서 설명한 모듈을 가져오는 부분이다. 그 아래에 있는 urlpatterns 라는 변수가 바로 우리가 쓸 주소 체계/규칙을 담아놓는 변수이다. 이 변수는 django.conf.urls.defaults 에 있는 patterns 라는 함수가 만들어주는 값을 담는다.

patterns 함수는 크게 두 가지 인자를 넘겨 받는다. 하나는 접두사(prefix)고, 또 하나는 주소 체계(pattern)이다. 접두사는 patterns 함수의 첫 번째 인자로 넘기고, 두 번째 인자부터는 튜플(tuple) 자료형으로 주소 체계를 넣으면 된다. 주소 체계가 3개라면 두 번째 인자부터 네 번째 인자까지 넣는 방식이다.

접두사는 주소 체계가 아주 복잡하고 어플리케이션도 많다면 편하다. 주소 체계가 10개 있고 이 10개가 hannal.blog.views 에 있는 함수들과 연결되어 있다면(hannal.blog.views.index , hannal.blog.views.read , hannal.blog.views.speak 등) hannal.blog.views 를 접두사로 지정한 뒤 연결할 대상 이름은 hannal.blog.views 를 뺀 index, read, speak 등으로 쓸 수 있다. 다만, 파이썬에서 쓰는 객체 접근 꼴에 익숙해지게 하기 위해(점으로 프로퍼티나 메소드, 하위 객체를 구분하는 꼴) 이 강좌에서는 일부러 이 prefix 를 쓰지 않는다. 관련 내용은 Django 공식 문서에서 The view prefix를 참조하면 된다.

주소 체계는 방 두 개짜리 튜플 자료형을 한 덩어리로 쓴다. 첫 번째 방엔 주소 규칙이 들어가고, 두 번째 방엔 그 주소 규칙으로 요청(request)이 일어났을 때 실행할 행동을 지정한다.

주소 체계 덩어리 : ('주소 규칙', '실행할 행동 지정')

만약 /blog 로 접근을 하면 index_blog 라는 함수를 실행한다면

('/blog/', 'index_blog')

이런 비슷한 모양으로 주소 체계 덩어리를 만드는 것이다. /blog/2 엔 index_blog2 를 실행하는 걸 더하고 싶다면

('/blog/', 'index_blog'), ('/blog/2/', 'index_blog2')

이렇게 patterns 함수에 다음 인자로 주소 체계 덩어리를 더하면 된다. 근데 방금 전에 나는 “이런 모양으로”이라고 하지 않고 “이런 비슷한 모양으로”라고 했다. 이 말은 실제로 저런 모양이 아니라는 말인데, 보통은 저 두 주소 체계를 이렇게 설정해서 쓴다.

(r'^blog/(d+)/$', 'hannal.blog.views.index')

하나씩 살표보자. 우선 맨 앞에 있는 r 은 regular expression string, 그러니까 정규표현식 문자열로 문자열을 정의하는 역할을 한다.r 은 raw string 을 나타내며 역슬래시같은 일부 이스케이프 문자를 해석하지 않고 문자 그대로 다룬다.

그 안에 있는 문자열들은 정규표현식이다. 정규표현식은 매우 강력한 문자열 처리 방식인데, 간단히 설명을 하자면 일정한 규칙(pattern)을 지정하여 문자열을 다루는 데 쓴다. /blog/1, /blog/2, /blog/3 이런 문자열이 각 각 있을 때, /blog/ 는 공통되고 바로 뒤에는 숫자가 붙는다는 걸 쉽게 알 수 있다. 사람이야 이걸 한 눈에 알아보고 정규화 할 수 있지만 컴퓨터는 그러지 못한다. 바로 이런 상황, 그러지 못하는 상황에 처한 컴퓨터가 그럴 수 있게 해주는 것이 정규표현식이다.

이번엔 정규표현식 내용을 보자. ' 로 묶인 안쪽 문자열인 ^blog/(d+)/$ 이 바로 주소 규칙이다. 근데 주소 규칙이 암호처럼 이상한 기호가 들어가있다. 우리가 나타내고 싶은 주소는 단지 /blog/1 이나 /blog/2 인데 말이다. 자, 앞에서 /blog/1 이나 /blog/2 는 정규화 할 수 있다고 했다. 바로 /blog/숫자 라고 말이다. 여기서 /blog 과 숫자를 정규표현식으로 나타낸 것이 저 암호 같은 문자열이다.

정규표현식은 이 글에서 모두 다루기엔 만만치 않으니 저 규칙만 알아보자.

  • 우선 ^ 은 맨 앞이라는 뜻이다. 그러므로 ^blog 라고 하면 문자열에서 blog 로 시작한다는 말이 된다. blog/1 는 이 조건에 해당되지만 a_blog/1 은 해당되지 않는다.
  • (d+) 에서 정규표현식은 d+ 이다. d 는 숫자를 뜻한다. 다른 표현 방법으로는 [0-9]이다(0부터 9까지라는 뜻). 뒤에 있는 + 는 1개 이상이라는 뜻으로 d+ 라는 표현식은 숫자가 1개 이상인 경우를 나타낸다. /blog/1 이든 /blog/3333 이든 상관없이 숫자가 나오면 다 해당된다. 이 표현식을 괄호로 묶으면 이 표현식에 해당되는 부분을 변수처럼 따로 빼낸다(?).
  • 맨 마지막에 있는 $ 은 맨 끝이라는 뜻이다. /$ 라고 쓴 건 / 문자가 맨 끝이라는 뜻이다. /blog/1/ 은 이 조건에 해당되지만 /blog/1/aa 이러면 이 조건에 해당되지 않는다.
  • 마지막으로 (d+) 에서 괄호는 묶음(group)이다. 이 문자열 자체는 규칙(pattern) 뜻이 있는 건 아니고, 괄호로 묶인 부분을 묶음으로 인식해서 이 묶음을 변수처럼 따로 접근한다. 설명이 부족한데 실제 예는 곧 나온다.

정리하면 이용자가 접근한 주소 문자열을 분석했을 때, blog/ 로 시작하고 바로 뒤에 문자열 길이가 1자 이상인 숫자가 붙으며 /으로 끝나면 hannal.blog.views.index 를 실행하겠다는 뜻이다. hannal.blog.views.index 는 from 과 import 로 모듈을 가져오는 문장과 비슷하게 생겼는데, 실제로도 같은 뜻이다. hannal 에 있는 blog 에서 views 에 있는 index 를 실행하겠다는 뜻으로, hannal 은 바로 프로젝트, blog 는 어플리케이션, views 는 컨트롤러(뷰)인 views.py, index 는 views.py 안에 있는 index 라는 함수이다.

참 별 것 아닌 것 가지고 길게 다룬 것 같은데, 자주 만나고 다루는 놈이므로 글 공간을 길게 잡아봤다. ^^

django admin mode

요즘 잘 나가는 RoR (Ruby on Rails) 같은 웹 프레임워크들은 스카폴딩(scaffolding)이라는 편리하고 매력있는 기능을 제공한다. 스카폴딩은 쇠관(metal pipes)으로 틀을 지어 건물을 짓거나 보수할 때 유용한 임시 건축 틀이다. 적어도 건축 용어로는 그러한데, 다른 분야에서 이 낱말을 쓸 때도 알맹이를 만드는 데 유용한 임시 보완 틀이라는 점에서 거의 비슷한 개념이다. 이런 스카폴딩이 RoR 에서 어떻게 편리함을 제공하는지 궁금할텐데, 우리가 아직 글쓰는 화면과 기능을 정식으로 만들지 않았는데 일단 DB에 글을 써넣어야 화면에 글이 보이게 하고 싶을 때, 임시로 글을 써넣는 기능을 아주 짧은 시간과 노력을 들여 후다닥 만들어서 필요한 상황을 편하게 해소할 수 있다.

django 에는 이렇게 편리한 스카폴딩 기능을 제공하지 않는다. 대신 admin 기능(A dynamic admin interface)을 제공하는데, 스카폴딩에서 누리는 편리함을 대부분 제공한다. 어? 그럼 스카폴딩인데 이름만 바뀐 게 아닌가? 간단하게 생각하면 그렇다고 볼 수 있지만, django 는 임시 틀을 만들지도 않고 admin 이라는 미리 지어진 별도 공간에서 처리하므로 어떤 점에서 보면 잔손질을 좀 더 덜 하는 셈이다. 그래서 공식 문서에선 admin 기능을 임시 건축물 틀이 아니라 집 그 자체라는 재밌는 표현을 쓰고 있다.

이번 글에선 글 목록 기능을 만들텐데 아직 글쓰기 기능을 만들지 않았으니 admin 기능을 익힐 겸 admin 영역에서 글을 써보려 한다. 우선 settings.py 를 연 뒤 맨 아래에 있는 INSTALLED_APPS 가 있는 부분을 보자. 지난 글에서 우린 'hannal.blog', 라는 줄을 추가했다. 여기에 'django.contrib.admin', 줄을 추가한다.

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.admin',
    'hannal.blog',
)

이런 비슷한 모양새가 될 것이다.

이번엔 admin 기능에 필요한 DB 테이블을 생성해야 하는데 간단하게

manage.py syncdb

라고 명령어를 치면 된다(settings.py 에 치라는 말이 아니라 manage.py 파일을 저 부가명령어와 함께 실행하라는 뜻이다). 마치 blog 에 있는 models.py 에 모델을 디자인 하듯이 django.contrib.admin 에도 모델들이 정의되어 있어서 자동으로 테이블을 만드는 것이다 ( /django/contrib/admin/models.py 에 있다 ).

이제 웹에서 admin 기능으로 접근할 수 있게 주소 체계를 설정해야 한다. 바로 이 부분을 해야 해서 위에서 길게 urls.py 를 설명한 것이다. 우선 urls.py 를 열면

    # Uncomment this for admin:
    # (r'^admin/', include('django.contrib.admin.urls')),

이런 비슷한 줄이 있는데 (r'^admin/', include('django.contrib.admin.urls')), 이 줄을 주석 처리한 # 기호를 지운다. 주석을 해제하는 것이다.

2008년 9월에 발표될 예정인 django 1.0판에서는 다소 설정법이 바뀌었다. 먼저 admin 모듈을 가져와야 한다.

from django.contrib import admin

이번엔 그 아래에

admin.autodiscover()

코드를 추가하면 된다. 이 코드는 settings.py 에서 INSTALLED_APPS 에 admin.py를 자동으로 불러들이는 기능을 한다. 이제 주소 규칙을 새로 해야 하는데,

(r'^admin/', include('django.contrib.admin.urls')),

대신에

(r'^admin/(.*)', admin.site.root),

라고 해야 한다. 공식 문서에서 AdminSite Object 부분을 참고하길 바란다.

이번엔 django 내장 웹서버를 구동한다 ( manage.py runserver ). 이미 django 내장 웹서버를 구동했다면 스스로 재구동한다. 그런 뒤 웹브라우저에서 http://localhost:8000/admin 이라고 치면 Django administration 로그인 화면이 나타난다. ^^

아이디와 암호는 바로 이전 글에서 blog 모델을 DB 테이블로 만들 때 만들었던 그 내용으로 로그인 하면 된다.

저곳에서 당장 우리가 할 일은 없다. 저 관리자 화면에 우리가 관리할 모델을 나타나게 해야 한다.

각 모델에 admin 기능 활성화하기

글 정보 모델(Entries)

우선 blog 디렉토리(폴더)에 있는 models.py 파일을 연다. 그런 뒤 class Entries 라고 된 부분, 그러니까 글 정보 모델이 있는 부분에 다음 줄을 추가한다.

class Admin:
     pass

이런 모양새가 된다.

class Entries(models.Model):
    Title = models.CharField(max_length=80, null=False)
    Content = models.TextField(null=False)
    created = models.DateTimeField(auto_now_add=True, auto_now=True)
    Category = models.ForeignKey(Categories)
    Tags = models.ManyToManyField(TagModel)
    Comments = models.PositiveSmallIntegerField(default=0, null=True)

    class Admin:
    	pass

들여쓰기에 주의하자. Admin 이라는 클래스가 Entries 클래스(모델) 안에 있기 때문이다. 이제 파일을 저장하고(물론 django 내장 웹서버는 자동으로 재구동 된다) 아까 그 관리자 화면으로 가면 Entriess 라는 모델이 나타난다.

이제 admin 영역에서 Entries 모델을 다룰 수 있다. 가볍게 글을 하나 써보자.

Add entries 를 누르면 Entries 모델에 새 자료를 넣을 수 있는 화면이 나온다.

글 제목과 본문을 쓰고 Save 를 누르면 저장이 되지 않는다. 글 갈래(Category)와 꼬리표(Tags)를 지정하지 않았기 때문이다. 실제 글쓰기 기능은 이런 걸 자동으로 처리되게 할 테지만, admin 영역에선 그런 게 안되니 미리 글 갈래와 꼬리표를 써두고 골라야 한다.

admin 영역에서 글 갈래와 꼬리표 모델(Entries)에 자료 넣기

아까 Entries 모델에 그랬던 것처럼 Categories 모델과 TagModel 모델에도 class Admin: 을 추가하자.

class Categories(models.Model):
    Title = models.CharField(max_length=40, null=False)

    class Admin:
    	pass

class TagModel(models.Model):
    Title = models.CharField(max_length=20, null=False)

    class Admin:
    	pass

소스 코드는 이렇게 된다. 이제 admin 영역에서 첫 화면으로 간 뒤 Categoriess 모델을 누른 뒤 Add categories 를 누른다. Title 이라고 된 곳에 적당한 이름을 넣고 Save 를 누르면 저장된다(난 “주절 주절”이라고 했다). 같은 방법으로 TagModels 모델에도 적당한 꼬리표를 추가하자(난 “실험”, “한날”을 각 각 추가했다).

글 정보 모델에 글 써넣기

다시 한 번 글을 써보자. Entries 모델에 자료 추가 화면으로 간 뒤 Category 와 Tags 를 방금 추가한 것들을 고른 뒤 글을 쓰면 잘 저장된다. 우린 글쓰기 기능을 안만들었는데도 글 정보 모델에 글을 써넣은 것이다! 정말 편하고 쉽지 않은가? scaffolding 이든 django admin 영역이든 정말 편하고 강력한 기능이다.

근데 어째 방금 우리가 작업한 admin 영역에서 각 개체(자료) 모양새가 예쁘지 않다. 예를 들면, 꼬리표를 두 개 넣었는데 각 각이 우리가 쓴 꼬리표 이름(실험, 한날)으로 뜨지 않고 TagModel object 라고 뜨고, 글 정보 모델에 가보면 우리가 쓴 글이 글 제목으로 뜨지 않고 Entries object 라고 참 공돌이스럽게(?) 나타나는 것이다. 자료가 많아지면 뭐가 뭔지 어떻게 구분한담? 걱정마시라. 입맛에 맞게 바꿀 수 있다. 이 내용은 이 글 아래에서 다루기로 하고, 당장은 방금 쓴 글을 자랑스럽게 화면에 표시하는 영역부터 만들자.

글 목록 출력 영역

드디어! 블로그처럼 보이는 영역을 만들 차례이다. 그러나 여기서 잠시 다리를 걸자면, 주소 체계를 어떻게 해야 할 지부터 정해야 한다. 길게 고민할 것 없이 이렇게 하자.

  • /blog : 블로그 맨 첫 목록 화면 (1쪽)
  • /blog/page/숫자 : 쪽 번호 목록 주소
  • /blog/entry/숫자 : 글 낱장 주소
  • /blog/category/숫자 : 글 갈래에 속하는 목록 주소
  • /blog/tag/문자열 : 글 꼬리표에 속하는 글 목록 주소
  • /blog/tags : 글 꼬리표 목록

우선 만들 부분은 맨 위에 있는 화면과 두 번째 부분이다. 바로 쪽 번호로 글 목록을 나타내는 영역이다.

urls.py 에 주소 체계 추가

주소 규칙은 크게 두 가지이다. /blog 로만 접근했을 때는 (r'^blog/$', hannal.blog.views.index), 이렇게 하면 된다. /blog/page/숫자 는 (r'^blog/page/(?P<page>d+)/$', hannal.blog.views.index), 이렇게 하면 된다. 그래서

urlpatterns = patterns('',
    (r'^blog/$', 'hannal.blog.views.index'),
    (r'^blog/page/(?P<page>d+)/$', 'hannal.blog.views.index'),

     (r'^admin/', include('django.contrib.admin.urls')),
)

이런 비슷한 모양이 된다. 정규표현식에 대해서는 이미 간단히 설명을 했는데 ?P<page> 라는 문자열은 처음 보는 것이다. 이는 정규표현식에서 묶음(group)에 이름을 지어주는 것이다. 그냥 (d+) 라고 하면 이 규칙에 해당되는 저 묶음을 $1 이나 1 처럼 숫자로 된 이름으로 저 묶음을 변수처럼 다룬다. 그런데 이런 이름은 알아보기 쉽지 않다. 순서만 바뀌어도 헷갈리고. 그래서 우리가 좀 더 알아보기 쉬운 형태로 이름을 지정하는 것이다. ?P<page> 라고 하면 (d+) 이런 규칙에 해당되는 부분을 page 라는 이름으로 묶은 것이다. 즉, (d+) 에 해당되는 부분을 hannal.blog.views.index 로 넘겨줄 때 page 라는 이름으로 넘겨주는 것이다.

이번엔 hannal.blog.views.index 를 만들어 보자. blog 디렉토리에 있는 views.py 파일을 연 뒤 다음 내용을 써넣자.

# -*- coding: utf-8 -*-

def index(request):
    pass

이게 기본 형태이다. 맨 위에 utf-8 뭐시기는 예전 글에서 간단히 설명했으니 넘어가고, def 로 시작하는 줄은 함수를 선언하는 내용이다. 클래스는 class 이름 이렇게 선언을 하고, 함수(혹은 메소드)는 def 이름 이렇게 선언을 한다. 그리고 넘겨 받는 기본 인자가 request 이므로 request 를 써주어 저런 모양이 된다. 그런데 우리는 page 라는 변수도 받는다. 그러므로

def index(request, page):
    pass

이렇게 된다. 그런데 이렇게 하면 /blog 주소로 접근했을 때 오류가 난다. 왜냐하면 저 함수는 request 와 page 변수를 받는다고 했는데 /blog 로 접근을 하면 page 변수를 넘기지 않기 때문이다. 그래서 page 가 없는 경우엔 page 에 알아서 기본값이 들어가게 해야 한다.

def index(request, page=1):
    pass

간단하다. page 를 넘겨 받을 때 1 을 넣게 하면 된다. 물론 page 에 특정값을 넘겨 받으면 그 값이 들어오고, 넘겨주는 값이 없으면 1 이 들어간다.

기본값을 1로 한 이유는 글 목록 쪽 번호가 기본으로 1이기 때문이다. 쪽 번호가 -1023 쪽부터 시작한다면 그렇게 해도 무방하다만 꽤나 범상치 않은 쪽 번호이다. 무난하고 자연스럽게 page=1 이렇게 하면 page 를 지정하지 않은 경우 자동으로 쪽 번호는 1이 되는 것이다. 즉, /blog 로 접근하면 1쪽을 나타내므로 /blog/page/1 과 같은 화면이 뜬다.

만약 이렇게 했는데

TypeError: unsupported operand type(s) for -: ‘unicode’ and ‘int’

이런 오류가 난다면 바로 아래에 page = int(page) 라는 코드를 넣으면 된다.

def index(request, page=1):
    page = int(page)

이는 page 값이 숫자형이 아니라 unicode형이라서 빼기(-) 연산을 할 수 없다는 오류이다. int 라는 기본 내장 함수(built-in function)로 숫자형으로 바꾸는 것이다.

화면에 글자 찍어보기

화면에 아까 우리가 쓴 글을 뿌리기 전에 잠시 “안녕, 여러분” 문장부터 찍어보자. django 에서는 화면에(http response) 문자열을 뿌리기 편하게 해주는 기능을 제공하는데 그 기능들이 django.http 에 들어가 있고, 화면에 글자를 뿌리는 데엔 HttpResponse 함수를 쓰면 된다.

from django.http import HttpResponse

이걸 쓸 수 있게 소스 파일 안에서 불러 들이자(import).

그런 뒤 화면에 “안녕, 여러분”을 뿌리면 된다.

return HttpResponse('안녕, 여러분')

아주 간단하다. 소스 코드는

# -*- coding: utf-8 -*-
from django.http import HttpResponse

def index(request, page=1):
    return HttpResponse('안녕, 여러분')

이렇게 된다. 맨 위에 utf-8 줄이 없으면 저 한글 부분에서 오류가 나니 꼭 넣자. 그런 뒤 웹브라우저에서 http://localhost:8000/blog/ 에 접속하면

감격스럽게도 “안녕, 여러분”에 화면에 나타난다. 난 처음 저 화면을 봤을 때 주먹을 불끈 쥐었다. ^^;

이번엔 변수를 활용해보자. page_title 이라는 변수에 “블로그 글 목록 화면”을 담은 뒤 이 변수를 화면에 추가하는 것이다.

def index(request, page=1):
    page_title = '블로그 글 목록 화면'
    return HttpResponse('안녕, 여러분. 이곳은 [%s] 이야.' % page_title)

page_title 하는 줄은 별로 어려운 내용은 아니다. page_title 에 '블로그 글 목록 화면' 을 담은 것이다. 문제는(?) 그 다음 줄.

('안녕, 여러분. 이곳은 [%s] 이야.' % page_title)

에 보면 [%s]이 있고 % page_title 이 있다. [%s]에서 [ 과 ]는 화면에 출력되는 기호이니 별 의미는 없고, %s를 보면 되는데 %s 를 하면 % page_title 에서 page_title 변수 안 내용을 “문자열로” 대입해서 바꾼다는 뜻이다. 이런 역할을 하기 위해 % 를 넣은 것이다. 이런 걸 파이썬에서는 String Formatting Operations이라고 한다.

모델에서 글 목록 가져와서 출력하기

이번엔 모델을 이용하여 DB에 넣은 글을 가져와서 화면에 출력해보자. 모델을 이용하려면 먼저 모델을 불러와야 한다. 간단하다. python 으로 다른 모듈을 불러오는 그 방법,

from hannal.blog.models import Entries

이렇게 하면 된다. blog 디렉토리에 있는 models.py 에서 Entries 객체(여기서는 클래스)를 가져온(import) 것이다.

모델을 이용해서 DB에서 자료를 꺼내는 것도 간단하긴 한데, 방법이 몇 가지 있다.

  • 하나만 가져오기 : get
  • 여러 개 가져오기 : filter
  • 다 가져오기 : all

각 각은 쓰는 상황이 곧 닥치니까 쓸 때마다 설명을 할 것이다.

블로그 글 목록을 쪽 단위로 가져오려면 글을 가져오는 조건을 지정해야 한다. 무슨 말이냐 하면 글이 100개 있는데 한 쪽에 글을 10개씩 가져온다면 1쪽엔 1번~10번 글, 2쪽엔 11번~20번 글을 가져와야 한다. 이럴 때 쓰는 것이 all 메소드이다. 이름을 보면 왠지 모두 다 가져오는 것 같은데 실제로는 이러 이러한 조건에 해당되는 것만 가져오게 하는 데 쓰는 조건을 지정하지 않는 것 뿐이다.

모델.objects.all()

쓰는 법은 간단하다. 이렇게 하면 된다. django 로 모델을 만들면 그 모델엔 objects 라는 클래스가 있고, 이 objects 클래스엔 all 이라는 메소드가 포함된다. 우리는 Entries 라는 모델에서 자료를 꺼내올 것이니

Entries.objects.all()

라고 하면 되며, 가져온 자료를(결과를) 따로 변수에 담아야 하니

entries = Entries.objects.all()

이렇게 해서 entries 라는 변수에 가져온 결과를 담으면 된다. 그런데 이렇게 하면 정말로 그 모델에 연결된 DB에서 모든 자료를 가져온다. 글이 몇 천 개 있는데 고작 글 몇 개를 가져오려고 그 몇 천 개 글을 다 가져온 뒤 골라쓰면 낭비가 아닐 수 없다. 그러니 우리가 쓸 글만 가져오게 해야 한다.

entries = Entries.objects.all()[시작위치:끝날위치]

간단하다. 이렇게 하면 DB에서 자료를 가져올 때 가져올 범위를 지정한다(mysql 인 경우 LIMIT 시작위치, 가져올개수로 범위를 지정한다). 맨 처음부터 5개를 가져오고 싶다면

entries = Entries.objects.all()[0:5]

라고 하면 된다. 기왕 말이 나왔으니 이제부터 블로그 글 목록에서 글은 한 쪽에 글 다섯 개라고 한다.

per_page = 5

이렇게 변수로도 지정하자.

이제 쪽 번호에 따라 가져올 구간을 계산해야 한다. 1쪽은 맨 첫 번째 쪽이므로 글을 맨 처음부터 다섯 개 가져오면 된다. 이런 식으로 쭈욱 따져보면

  • 1쪽 : 0 ~ 5
  • 2쪽 : 5~10
  • 7쪽 : 30~35
  • 12쪽 : 55~60

이런 식이다. 잘 보면 쪽번호에서 1을 뺀 뒤 5를 곱한 값이 시작 위치이고, 이 시작 위치에 5를 더한 값이 끝날 위치라는 걸 알 수 있다. 이런 규칙을 공식으로 풀어쓰면

start_pos = (page-1) * 5
end_pos = start_pos + 5

인데, 여기서 숫자 5는 한 쪽당 가져올 글 개수이고 이 값은 per_page 라는 변수에 담아두고 있으니

start_pos = (page-1) * per_page
end_pos = start_pos + per_page

이렇게 된다. 그런 뒤 모델로 DB에서 글을 가져올 때는

entries = Entries.objects.all()[start_pos:end_pos]

라고 하면, 쪽 번호에 따라 글을 가져올 것이다. 정말 잘 가져올까? 화면에 뿌려보자. 새로 화면에 출력하는 부분을 만들기 귀찮으니 만들어 둔 부분을 살짝 바꾸려 한다.

return HttpResponse('안녕, 여러분. [%s] 글은 첫 번째 글이야.' % entries[0].Title.encode('utf-8'))

%s 부분은 위에서 이미 설명했다.

entries[0] 은 모델로 DB에서 글을 가져온 결과를 entries 로 받았는데, filter() 나 all() 로 자료를 가져오면 여러 개를 가져올 수 있게 list 자료형(인터넷 참조)으로 받아온다(엄밀히 말하면 list 자료형은 아니고 django 모델의 클래스나 메소드가 덧붙은 형태이다). 이 list 자료형은 배열과 비슷하게 생겼는데, 맨 처음 공간에 있는 방 번호가 0이다. 즉 entries[0] 이라는 말은 entries 자료에서 0번 방을 가리키는 것이다.

.Title 은 entries[0] 에 있는 Title라는 클래스 프로퍼티이다. 지난 글에서 Entries 라는 모델을 클래스로 짤 때 모델 구성 요소를 프로퍼티로 짠다고 설명한 걸 기억해냈으면 좋겠다. Title 은 글 제목으로 Title = models.CharField(max_length=80, null=False) 이렇게 정의했었다. 글 본문은 Content 였으니 저 부분을 entries[0].Content 라고 하면 글 제목 대신 글 본문이 출력된다.

뒤에 붙은 .encode('utf-8') 는 utf-8 문자열을 ascii 문자형으로 표현(변환이 아니다)하겠다는 뜻이다. 이 메소드는 python 문자형(string, unicode)에 기본으로 탑재되어 있는 built-in method 이다. 만약 .encode('utf-8') 를 하지 않으면 UnicodeDecodeError 가 발생할 수 있다.

이게 django 내부에서는 ascii 문자형으로 문자를 받아다 출력하기 때문에 그렇다(HttpResponse 함수 내부를 들여다보면 unicode 자료형은 string 자료형으로 변환해서 다룬다). django 공식 문서에서는 필요하면 인코딩 하는데( 공식 문서에서 content 항목 설명을 보면 Returns the content as a Python string, encoding it from a Unicode object if necessary. 라고 되어 있음) 잘 안될 때가 있다(자세한 설명은 넘어간다). 그래서 직접 .encode('utf-8') 라고 한 것이다.

이제 웹에서 확인하면 글 제목이 나타난다. 여러분의 소스 코드는

# -*- coding: utf-8 -*-
from django.http import HttpResponse
from hannal.blog.models import Entries

def index(request, page=1):
    per_page = 5
    start_pos = (page - 1) * per_page
    end_pos = start_pos + per_page

    page_title = '블로그 글 목록 화면'

    entries = Entries.objects.all()[start_pos:end_pos]

    return HttpResponse('안녕, 여러분. [%s] 글은 첫 번째 글이야.' % entries[0].Title.encode('utf-8'))

이것과 비슷하게 생겼을 것이다.

http://localhost:8000/blog 로 접속을 하면 화면이 잘 나온다. 물론 글 제목을 다르게 했다면 다른 제목이 뜬다.

블로그는 최근 글부터 나열한다. 즉 시간을 기준으로 역순이라는 말인데, 모델로 DB에서 글들을 가져올 때 가져오는 정렬 순서를 지정해야 한다. 이를 order_by() 메소드로 할 수 있다.

order_by 메소드는 정렬할 모델 요소(클래스 프로퍼티, DB 필드)을 넘겨주면 그 요소를 기준으로 정렬을 한다. 왼쪽 정렬, 가운데 정렬 이런 건 아니고, 정순, 역순을 하는데 원래 순서대로(정순) 정렬한다면 그냥 정렬할 요소 이름을 넘겨주면 되고, 거꾸로(역순) 정렬한다면 정렬할 요소 이름 앞에 - 문자를 써준다.

  • order_by('created') : created 순서대로 정렬
  • order_by('-created') : created 역순으로 정렬

우리는 작성 일시 역순으로 가져와야 하므로

entries = Entries.objects.all().order_by('-created')[start_pos:end_pos]

라고 하면 된다.

이제 글 목록은 1차로 끝났다. 정말? 정말이다. 2차, 3차, 4차, ... , 21차는 잠시 후에 하면 된다. 씨익.

모델에서 특정 글(낱장) 가져와서 출력하기

특정 글만 가져와서 나타내는 것 역시 글 목록 가져오는 것만큼 간단하다. 이 간단한 걸 구현하기에 앞서 먼저 주소 체계부터 만들어야 한다. 위에서 글을 낱장으로 접근할 주소를 /blog/entry/숫자로 하기로 했으니 urls.py 에

(r'^blog/entry/(?P<entry_id>d+)/$', 'hannal.blog.views.read'),

를 추가한다. 저 주소 규칙으로 접근을 하면 read 함수로 글 id 를 넘겨주는 것이다.

urlpatterns = patterns('',
(r'^blog/$', 'hannal.blog.views.index'),
(r'^blog/page/(?P<page>d+)/$', 'hannal.blog.views.index'),
(r'^blog/entry/(?P<entry_id>d+)/$', 'hannal.blog.views.read'),
)

특정 글 가져오기

특정 글을 가져오는 기능을 blog 디렉토리에 있는 views.py 에서 read 함수로 구현할 것이므로 위에서 index 함수를 만든 것처럼 read 함수부터 만들어야 한다.

다 아는 처지끼리(?) index 함수 설명하듯이 하나 하나 뜯어보며 소스를 짤 것까진 없으니 한 번에 소스를 턱 짜봤다.

def read(request, entry_id=None):
    page_title = '블로그 글 읽기 화면'

    return HttpResponse('안녕, 여러분. 여긴 [%s]이고 [%d]번 글이야.' % (page_title, int(entry_id)))

특이한 건 별로 없다. urls.py 에서 글 번호(id)를 entry_id 로 넘겨주므로 read 함수도 entry_id 로 받는다. 다만 entry_id 값을 넘겨주지 않으면 기본값으로 None 이라 지정하는 부분이 위에 있는 index 함수와 다르다.

새로 눈에 띄이는 부분은 %d 와 int 이다. %d 는 %s와 비슷한 녀석인데 %s는 문자열을 받는다면 %d는 숫자를 받는다.

int 함수는 파이썬에 기본으로 내장된 함수인데, 넘겨받는 값을 정수형으로 바꾼다. 넘겨받는 값이 숫자들로만 구성된 문자형('12'나 '1234' 같은 문자형)인 경우 숫자로 변환하여 12 나 1234 로 바꿔서 숫자 셈을 할 수 있게 만든다(c 언어로는 atoi() 함수랑 같다). 또 33.2처럼 실수(float)인 경우 .2 부분을 떼고 정수 부분만 남겨서 33으로 만들기도 한다. 주소에서 숫자(entry_id)를 가져오는 건 숫자인 문자형이므로 int 함수를 이용해 숫자로 변환한 것이다.

page_title 과 int(entry_id)를 괄호로 묶은 것은 %s 나 %d 같은 문자열 포매팅(formatting) 문자가 두 개 이상이기 때문이다.

웹브라우저에서 http://localhost:8000/blog/entry/1 에 방문하면 낱장 글 보는 화면이 잘 뜬다.

이번엔 지정한 글 번호를 가진 글을 가져와서 화면에 출력해보자. 방법은 간단하다. Entries 모델에서 이용자가 요청하는 글 번호(entry_id)를 갖는 글 하나를 가져오게 하면 된다.

이미 위에서 자료 하나를 가져올 때엔 get 함수를 쓴다고 아주 간단히 소개했다. all 함수로 글 목록에 나타낼 글 정보를 가져온 것과 비슷한 방법으로 글을 가져오면 된다.

current_entry = Entries.objects.get(id=1)

이런 식으로 쓰면 되는데 우리는 이용자가 요청한 주소에서 글 번호를 entry_id 로 가져오고 이 글을 화면에 출력해야 하므로

current_entry = Entries.objects.get(id=entry_id)

라고 해야 한다. 이렇게 해도 잘 되는데, 확실하게 하려면

current_entry = Entries.objects.get(id=int(entry_id))

이러는 게 낫다. 화면에 출력해보자.

return HttpResponse('안녕, 여러분. [%d]번 글은 [%s]이야.' % (current_entry.id, current_entry.Title.encode('utf-8')))

소스 코드는

def read(request, entry_id=None):
    page_title = '블로그 글 읽기 화면'

    current_entry = Entries.objects.get(id=int(entry_id))

    return HttpResponse('안녕, 여러분. [%d]번 글은 [%s]이야.' % (current_entry.id, current_entry.Title.encode('utf-8')))

이런 모양일테고 화면도 잘 뜬다.

정말 참 간단하다.

이전 글, 다음 글 가져오기

글 목록은 쪽 번호로 이리 저리 왔다 갔다 한다면, 특정 글을 읽고 있는 낱장 상태에선 지금 보고 있는 글을 기준으로 이전 글과 다음 글로 왔다 갔다 한다.

자, 단순하게 생각해보자. 우리는 글을 가져올 때 시간 역순으로 정렬한다. 근데 현재 특정 글만 달랑 가져왔다. 이 글의 이전 글은 어떻게 가져올까? 지금 보고 있는 글 id 가 1023이니까 id 가 1022 인 글을 가져와야 할까? 지금 우리가 만들 블로그는 그렇게 해도 상관 없지만, 엄밀히 말해서 옳은 방법은 아니다. 왜냐하면

  • 1023번 글 : 공개 상태
  • 1022번 글 : 비공개 상태
  • 1021번 글 : 공개 상태

처럼 글 공개/비공개 기능이 있는 경우엔 1022번이 이전 글이 아닐 수 있고, 혹은 글 작성일시를 이용자가 다시 설정하는 경우도 있기 때문이다. 1023번 글 작성 일시가 2008년 7월 5일 오후 1시인데 1022번 글의 작성일시를 7월 5일 오후 3시로 하면 글 순서는

  • 1022번 글
  • 1023번 글
  • 1021번 글

식으로 되는 것이다. 그러므로 지금 보고 있는(current) 글의 작성일시를 먼저 알아낸 뒤, 이 일시보다 이전에 작성된 글 중 가장 최근 글을 알아내야 한다. 1023번 글 보다 이전에 쓰여진 글이 1022, 1021, 1020 순이라면 1022를 가져와야 한다.

단순하게 생각하면 이런 흐름인데 이걸 소스 코드로 구현하면 영 귀찮고 지저분하다. 그러므로 우린 구현하지 않으려 한다. 농담이고(^^) django에선 이걸 아주 편하게 해주는 기능을 제공한다. 바로 get_next_by_뭐시기 라는 메소드와 get_previous_by_뭐시기 라는 메소드이다.

뭐시기는 뭘 기준으로 이전(previous)이고 다음(next)인지 정할 이름이다. 예를 들어, 작성일시(우린 created 라고 이름을 지었다)를 기준으로 한다면 get_next_by_created() 나 get_previous_by_created() 가 된다. 글 제목(Title 이라고 했다)이라면 get_next_by_Title() 이런 식이다. 정말 놀랍지 않은가?

그럼 실제로 구현해보자.

prev_entry = current_entry.get_previous_by_created()
next_entry = current_entry.get_next_by_created()

주의할 점은 Entries.objects.get_previous_by_created() 가 아니라 이미 Entries.objects.get 으로 글을 가져와서 담은 current_entry 객체에 붙인 점이다. 사람 논리로 접근해서 생각하면 당연하다. 이전이나 다음이란 무엇을 기준으로 이전이고 다음인지 나타내는 것이다. 그 기준이 바로 지금 우리가 보고 있는 글(current_entry)이므로 current_entry 에 있는 메소드인 get_previous_by_뭐시기 와 get_next_by_뭐시기 를 쓰는 것이다.

django는 이런 편리한 기능을 제공해서 귀찮거나 번거로운 기능 구현을 아주 쉽게 구현하도록 도와준다. 실제 사용은 좀 있다 할 것이다.

뷰(템플릿)을 이용해서 화면 나타내기

이제까지는 모델을 이용해 DB에서 글을 가져온 뒤, 잘 가져왔나 확인(실험)했는데, 이번엔 실제 화면 출력 부분을 그럴 듯 하게 만들 차례이다. 그럴 듯 하다고는 해도 실제로는 뼈대만 만들 것이지 실제로 정교하게 겉모습을 다듬진 않을 것이다.

django 에서 뷰(템플릿)는 내장된 템플릿 엔진(?)을 쓴다. 템플릿은 간단히 말해서 스킨 기능이다. 난 django에서 제공하는 템플릿 엔진의 문법을 그다지 좋아하지 않는다. 원한다면 다른 템플릿 엔진으로 바꿔 쓸 수도 있어서 난 치타 템플릿(Cheetah template) 엔진을 쓴다. 우리는 아직 django 를 익혀가는 과정이니 django 내장 템플릿 엔진을 쓰는 것이 낫다.

django 템플릿은 django.template (django/template 폴더)에 있으며, 주로 Context 와 loader 객체를 쓴다.

from django.template import Context, loader

Context 는 템플릿 파일 안에서 쓰는 템플릿 치환자를 선언하는 데, loader 는 템플릿 파일을 읽어들이는 데 쓴다.

템플릿 사용 설정

맨 먼저 템플릿 파일을 넣을 디렉토리(폴더)를 만든다. hannal 디렉토리, 그러니까 settings.py 이나 urls.py 파일이 있는 곳에 templates 이라는 디렉토리를 만든다.

다음엔 settings.py 를 연 뒤에 방금 만든 디렉토리 위치를 템플릿 위치 설정에 써넣는다. settings.py 를 연 뒤 맨 위에 os 라는 모듈을 읽어 들이게 한다.

import os

그런 뒤 그 아랫 줄에 다음 코드를 껴넣는다.

ROOT_PATH = os.path.dirname(__file__)

이건 os 클래스에 있는 path 에서 dirname 이라는 메소드로 현재 파일(__file__ 이라는 built-in 변수)이 있는 실제 파일 경로(file path)를 ROOT_PATH 에 담는 것이다. 윈도우라면 c:/어디어디/hannal 이런 식일테고 리눅스/유닉스 계열이라면 /home/hannal/web/hannal 뭐 이런 식일텐데 이런 경로를 일일이 바꾸지 말고 현재 파일(__file__ = settings.py)이 있는 실제 파일 경로를 알아내서 담으면 한결 편하다.

ROOT_PATH 변수는 settings.py 파일 아래 쪽에

TEMPLATE_DIRS = (

이 변수 안에서 쓴다. 변수 이름에서 알 수 있듯이 템플릿 경로“들”을 지정하는데, 튜플(tuple) 자료형으로 여러 개 담을 수 있다. 튜플 자료형은 쉼표를 꼭 찍는 습관을 들이자고 했으니

TEMPLATE_DIRS = (
    ROOT_PATH + '/templates',
)

이런 식으로 꾸미면 된다. 이제 템플릿 파일은 templates 안에다 넣으면 된다.

글 목록

글 목록을 템플릿 파일을 이용하여 나타내보자. views.py 파일을 연 뒤 파일 위쪽에 있는 from/import 부분에 템플릿 기능에 필요한 객체들을 불러 들이도록 한다.

# -*- coding: utf-8 -*-
from django.http import HttpResponse
from hannal.blog.models import Entries
from django.template import Context, loader

글 목록 기능을 맡고 있는 함수가 index 이니 def index 하는 부분을 찾아간 뒤, return HttpResponse 가 있는 줄 위에 아래 줄을 넣는다.

tpl = loader.get_template('list.html')

위에서 읽어온(import) loader 객체에 있는 get_template 메소드를 이용해서 list.html 이라는 파일을 읽어와서 tpl 이라는 변수에 담는 것이다. list.html 파일은 템플릿 파일로써 앞서 만든 templates 디렉토리에 만들면 된다.

그 아래엔

ctx = Context({
})

이런 코드를 넣는다. 이 내용은 Context 라는 놈으로 템플릿 파일 안에서 쓸 치환자 내용을 만들어 ctx 라는 변수에 담는 것이다. 아직은 따로 치환자를 만들지 않으므로 빈값({} 이라는 속이 텅 빈 딕셔너리 자료형)을 넣었다. 치환자를 여러 개 넣다보면 줄을 바꿔가는 게(개행) 낫기 때문에 저렇게 코드를 미리 써놓은 것이며, ctx = Context({}) 라고 한 줄로 써도 문제 없다.

이번엔

return HttpResponse('안녕, 여러분. [%s] 글은 첫 번째 글이야.' % entries[0].Title.encode('utf-8'))

부분을 지우고 읽어들인 템플릿 파일로 화면을 그리도록 바꿔야 하는데

return HttpResponse(tpl.render(ctx))

이렇게 하면 된다.

다음엔 list.html 파일을 만들 차례이다. templates 폴더 안에 list.html 파일을 만든 뒤

<p>안녕 여러분.</p> <p>이 내용은 list.html 에 있는 거야</p> <p>

이라고 쓰고 저장한다. 그런 뒤 http://localhost:8000/blog 로 접속을 하면 예전과는 달리 list.html 파일 내용대로 화면이 출력된다.

자, 이제 슬슬 좀 더 그럴 듯 하게 글 목록이 화면에 나오게 해볼까? 우선 글 목록과 화면 제목, 그리고 현재 쪽 번호를 담은 변수들을 치환자로 등록하자.

ctx = Context({
'page_title':page_title,
'entries':entries,
'current_page':page
})

아까 빈값을 넣었던 Context 에 내용을 추가했다. 앞에 ' 로 감싸있는 문자열이 템플릿 파일 안에서 쓸 치환자 이름이고, : (콜론) 뒤에 있는 문자열은 이 치환자에 연결할 컨트롤러(view) 파일에서 쓰고 있는 변수를 나타낸다. page_title 은

page_title = '블로그 글 목록 화면'

이렇게 했고, entries 는

entries = Entries.objects.all().order_by('-created')[start_pos:end_pos]

라고 해놨기에 이렇게 쓸 수 있는 것이다. 템플릿 파일 안에서 저런 이름과 다르게 하고 싶다면, 예를들면 page_title 이라는 이름이 마음에 들지 않아서 screen_title 이라고 하고 싶다면 'page_title' 이 부분을 고쳐서

'screen_title':page_title,
'entries':entries,

이라고 하면 된다. 다만 소스 코드 일관성을 위해 마음에 들건 들지 않건 page_title 이라고 하도록 하자.

이번엔 list.html 을 고쳐서 치환자에 연결된 변수들을 다루자. list.html 파일을 연 뒤 맨 위에

<h1><strong>{{page_title}}</strong></h1>

라고 쓴다. <h1> 과 </h1>은 <p> 와 </p> 이랑 마찬가지로 HTML 태그이니 여기서 당장 신경 쓸 필요는 없고, {{page_title}} 을 잘 눈여겨 보자. 제로보드 등으로 웹 게시판 스킨을 만들어 본 이라면 저게 왠지 템플릿(스킨) 파일에서 쓰는 치환자(변수)처럼 생겼다는 걸 느낄 것이다. 맞다. django 에 내장된 템플릿 엔진은 템플릿 파일에서 치환자를 {{ 과 }} 로 묶어서 나타낸다. {{page_title}} 이라고 하면 page_title 이라는 치환자를 {{page_title}} 과 바꿔 넣겠다는 뜻이 된다. page_title 은 “블로그 글 목록 화면”이라는 문자열이 있으므로 결국 <h1>{{page_title}}</h1> 은 <h1>블로그 글 목록 화면</h1> 이라는 문자열이 되는 것이다.

못믿겠다면 list.html 을 저렇게 고친 뒤 저장하고 나서 http://localhost:8000/blog 에 방문하시라. “안녕 여러분.” 위에 굵고 크게 “블로그 글 목록 화면”이라는 내용이 뜬다.

여기서 잠깐. 이런 치환자는 여러 종류가 있으므로 구분을 위해 방금 본 {{이름}} 이런 건 변수 치환자라고 부르겠다.

글 목록을 출력하는 건 조금 만만치 않다. 반복문을 쓰기 때문인데, DB 에서 글들을 가져와서 화면에 출력을 한 지금까지도 우리는 프로그래밍에서 반복문이라 부르는 문법들(for문, while문 등)을 단 하나도 쓰지 않았다. 즉, 이번에 처음 만나는 셈이다. 그러니 신경 써서 잘 보길 바란다.

프로그래밍 언어에 있는 반복문들은 대체로 비슷 비슷하다. 아주 자주 쓰이는 반복문은 바로 for문이다. 보통 이렇게 생겼다.

for ( 조건문 ) {
    반복할 내용
}

예를 들면 c 언어에서는

for ( i=0; i < 10; ++i ) {
    printf("number %d", i);
}

이렇게 생겼고, php 에선

for ( $i=0; $i < 10; ++$i ) {
    echo 'number '.$i;
}

이렇게 생겼으며(perl 도 거의 똑같다), python 에선

for i in range(0, 10):
    print 'number %d' % i

이렇게 생겼다. 그리고 django 템플릿 엔진에 있는 반복문 문법도 python 것과 조금 비슷하다.

{% for i in list_var %}
    number {{i}}
{% endfor %}

python 의 for문과 다른 점이라면 for문을 닫는 부분({% endfor %})이 있다는 점이며, for문이 변수 치환자와 다른 점이라면 {{ 과 }} 이 아닌 {% 과 %} 을 쓴다는 점이다. 이렇게 변수 치환자와는 달리 구문(statement)은 {% 과 %}으로 표현한다. 잠시 후 다룰 조건문 역시 이런 구문을 쓴다.

for문을 배웠으니 글들을 갖고 있는 entries 에서 글을 하나씩 뽑아내서 화면에 출력해보자. 글들을 갖고 있는 치환자는 entries 이다. entries 안에 글들을 반복해서 끄집어 내려 하므로

{% for entry in entries %}
{% endfor %}

라고 하면 된다. entries (글들) 안을 빙빙 돌며 매번 entry 라는 변수에 담는 것이다.

이렇게 각 글을 받아낸 entry 를 써서 각 글을 출력하면 된다.

{% for entry in entries %}
<div>
<h3><a href="/blog/entry/{{entry.id}}"  href="/blog/entry/{{entry.id}}">{{entry.Title}}</a></h3>
<ul>
<li></li>
</ul>
<div></div>
</div>

{% endfor %}

list.html 을 저장하고 다시 웹브라우저 화면을 갱신하면 화면 아래에 글도 출력될 것이다. 아직 글을 한 개 밖에 안썼으니 글은 하나만 출력될 것이다. django admin 에 접속해서 또 글을 쓰면 두 개가 뜬다. 참고로 내 화면은

이렇게 뜬다.

{{ 과 }} 으로 묶은 것은 변수 치환자라는 건 배웠는데, entry.Title, 그러니까 entry점Title 이라는 꼴은 좀 어색하다. 이건 django 가 모델로 가져온 각 자료 객체에서 모델 요소들이 클래스 프로퍼티로 존재하기 때문에 그렇다. 이미 앞선 글들에서 몇 번 설명을 했고, 글 낱장 읽기(def read)에서도 return HttpResponse 부분에서 글 제목을 current_entry.Title 이런 꼴로 출력하고 있다. 같은 내용이다.

흐음. 이걸로 끝? 아니다. 글 갈래(category)와 꼬리표를 출력하지 않았다. 글 갈래는 Category, 꼬리표는 Tags 라고 이름을 지었으니 와 라고 쓰면 되겠다. 날짜 표시하는 부분 위에 출력해보자.

<li></li>
<li></li>
<li></li>

어떤가. 정말 예상대로 잘 뜨는가? 아마

  • Categories object
  • <django .db.models.fields.related.ManyRelatedManager object at 0x1681af0>

이런 모양으로 뜰 것이다. 왜냐하면 Category 와 Tags 는 Title 이나 Content 와는 달리 값 자체를 담고 있지 않고 다른 곳에 있는 값을 가리키고 있기 때문이다. 혹, MANY-TO-ONE 이니 MANY-TO-MANY 어쩌고 하는 내용이 생각난다면 정말 착실히 공부했다는 반증이다.

Category는 간단하게 해결할 수 있다. 라고 쓴 부분을 라고 바꾸면 된다. 글Category 역시 Category 라는 모델이므로 Category 에 있는 Title 요소(프로퍼티)를 쓴 것이다.

이는 Category 가 MANY-TO-ONE 관계, 그러니까 각 글이 Category 를 달랑 하나와 이어질 수 있기 때문에 가능하다.

그에 반해 Tags 는 를 이라고 바꾸는 걸로는 꼬리표 이름을 출력할 수 없다. 왜냐하면 글 여러 개가 꼬리표 여러 개(글 하나가 꼬리표 여러 개를 갖는다는 말도 된다)와 이어지기 때문이다. 즉, 1번 글은 3번 꼬리표랑 이어져있지만, 2번 글은 1, 2, 5, 7, 12번 꼬리표랑 이어져 있을 수도 있다는 말이다.

잠깐. MANY-TO-MANY 니까 entry.Tags 도 entries 처럼 태그가 여러 개 들어가 있으니 for문을 써서 하나씩 가져오면 되지 않을까? 맞다. 혹 이렇게 생각을 한 사람이 있다면 엄지 손가락을 불끈 세워도 좋다.

<li></li>

이 부분을

<li>{% for tag in entry.Tags.all %}
<span></span>
{% endfor %}</li>

이렇게 바꾼 뒤 화면을 다시 불러오면 글 꼬리표가 잘 나온다. 원리는 간단하다. 앞서 자문자답 했듯이 entry.Tags 는 entries 처럼 꼬리표가 여러 개 들어갈 수 있는 자료형(tuple 비슷하게 생긴 자료형)이므로, for문으로 빙빙 돌며 안에 있는 자료를 하나씩 꺼낸 뒤 출력하면 되는 것이다. 이걸 for 문에서는 entry.Tags.all 로 한 뒤 tag 로 하나씩 받았다. 글 꼬리표 모델인 TagModel 에서 꼬리표 이름은 Title 이므로 하나씩 꺼낸 tag.Title 이라고 한 것이다.

그런데 위 방식은 성능이 다소 떨어지는 방식이다. 저런 코드가 나올 때마다 DB에서 관련 정보를 가져오기 때문이다. 좀 더 나은 방식은 이 글 아래에 따로 설명을 해놨다.

마지막으로 화면에 쪽 번호를 출력해서 쪽 이동을 할 수 있게 만들면 글 목록 출력 기능은 거의 끝나는 셈이다. 이 부분은 숙제로 내고 이 글에서는 다루지 않겠다. 다만, 사실상 소스 코드만 만들어주지 않았을 뿐 답을 거의 다 준 것이나 다름없는 귀띔을 해줄테니 부담은 조금만 갖고 스스로 만들어 보길 바란다.

  • 맨 마지막 쪽 번호는 전체 쪽 개수를 뜻한다.
  • 전체 쪽 개수는 전체 글 개수를 5(한 쪽에 출력할 글 개수(per_page))로 나누면 된다. 만약 5로 나눴는데 나머지 값이 있다면 5로 나눈 쪽 개수에 1을 더한다.
  • 모델로 전체 글 개수를 가져오는 방법은 count 라는 메소드를 쓰면 된다. Entries.objects.count() 라고 하면 해당 모델에 연결된 DB 테이블에 있는 모든 자료 개수를 가져온다.
  • python 에서 나누기 기호는 / 이다. res = 16 / 5 라고 하면 res 엔 3가 들어가 있는데, 소수점을 뺀 정수만 넣기 때문이다.
  • python 에서 나누기 이후 나오는 나머지 값만 구하는 기호는 % 이다. res = 16 % 5 라고 하면 16 에서 5를 나누고 남는 값(나머지 값)인 1이 들어간다.
  • 그러므로 res = (16/5) * 5 + (16%5) 라고 하면 16이 res 에 들어간다. 3 * 5 + 1 이 되기 때문이다.

쪽 번호 출력 형태는 [1] [2] [3] [4] [5] 이렇게 각 쪽 번호를 대괄호로 감싸서 나타내면 된다. 아마 아직은 글을 몇 개 안썼으므로 달랑 [1] 만 뜰 것이다.

특정 글 (낱장)

참으로 격렬하게 글 목록 출력을 배웠다. 덕분에 특정 글을 낱장으로 출력하는 부분은 새롭게 익힐 것이 사실상 없다. 빠르게 강좌를 진행하겠다.

views.py 에서 글 낱장 읽기 함수 부분, 그러니까 def read 로 시작하는 함수에서 read.html 템플릿 파일을 읽어오도록 한다.

tpl = loader.get_template(<read.html<)
ctx = Context({
    'page_title':page_title,
    'current_entry':current_entry,
    'prev_entry':prev_entry,
    'next_entry':next_entry
})
return HttpResponse(tpl.render(ctx))

list.html 이 read.html 로 바뀌었고, 현재 글, 이전 글, 다음 글을 치환자로 만드는 부분이 다르지만 틀은 같다.

read.html 은 일단 이렇게 만든다.

<div>
<h3><a href="/blog/entry/"  href="/blog/entry/"></a></h3>
<ul>
<li></li>
<li>{% for tag in current_entry.Tags.all %}
		<span></span>
		{% endfor %}</li>
<li></li>
<li></li>
</ul>
<div></div>
</div>

list.html 것과 거의 비슷하다. 이제 글 목록 화면에서 글 제목을 클릭해서 글 낱장 화면으로 가면 오류가 뜰 것이다.

오류 내용에서 알 수 있듯이 자료가 없기 때문에 그렇다. 무슨 말인고 하니, 1번 글로 접근을 할 경우 1번 글 보다 이전 글은 없다. 마찬가지로 가장 마지막 글로 접근을 하면 가장 마지막 글 보다 나중인(다음) 글은 없다. 물론 글이 3개가 있는데 가운데 있는 2번 글로 접근한다면 이전 글은 1번 글, 다음 글은 3번 글이 있으므로 오류가 나지 않는다. 즉, DB에 없는 자료를 요청하면 그런 자료가 없다는 예외 상황(Exception) 오류가 나버린다. 이용자가 어떤 글 번호로 접근할지 모르는데 글이 없을 때마다 저런 흉측한(?) 오류 화면을 그대로 놔둬야 할까? 물론 가만 냅두지 않을 수 있다. 자료가 없는 예외 상황이 발생하면 무작정 오류를 내지 말고, 그 예외 상황에 맞는 처리를 해주면 된다. 이런 예외 상황 처리를 예외 상황 구문으로 하며, 파이썬에서는 try except 문으로 표현할 수 있다. 이 구문의 꼴을 먼저 보자.

try:
    a = 1
except:
    a = 3

try 안쪽은 일단 실행할(시도할) 구문을 넣고, 이 구문에서 예외 상황이 터지면 except 안쪽에 있는 구문을 처리하는 것이다. 우선 a 에 1을 담아보는데 만약 어떤 문제로 인해 예외 상황이 터지면 a 에 3을 담는다. 물론 아까 본 그 흉측한(?) 예외 상황 오류는 나지 않는다. 이걸 문제가 된 이전 글/다음 글 부분에도 넣자.

try:
    prev_entry = current_entry.get_previous_by_created()
except:
    prev_entry = None

try:
    next_entry = current_entry.get_next_by_created()
except:
    next_entry = None

간단하다. get_previous_by_created() 로 이전 글을 가져와서 prev_entry에 넣되, 만약 이전 글이 없어서 예외 상황이 터지면 prev_entry 에 None 이라는 값을 넣는다. 이걸 다음 글 부분도 똑같이 한 것이다.

다시 화면을 열면 이번엔 오류가 안난다. 이전 글과 다음 글 가져오는 데서 발생할 수 있는 오류를 처리했으니 템플릿 파일에 관련 내용을 템플릿 파일 맨 아래에 넣었다.

<ul>
	{% if prev_entry %}
	<li><a href="/blog/entry/">이전 글 ()</a></li>
	{% endif %}

	{% if next_entry %}
	<li><a href="/blog/entry/">다음 글 ()</a></li>
	{% endif %}
</ul>

새로운 구문인 if문(조건문)을 빼면 나머지는 익숙한 내용들이다. if문 생김새 역시 for문과 비슷하다. {% 과 %}을 쓰며 if문을 닫을 때는 {% endif %} 를 쓴다. {% if prev_entry %} 라고 하면 prev_entry 가 있는가? 라고 확인을 한다. 있으면 참(true)이므로 if문 안쪽 내용을 처리하고, prev_entry가 없거나 None, 혹은 False 나 숫자 0인 경우는 거짓(false)이 되므로 if 문 안쪽을 처리하지 않는다.

if문은 for문에 비해 문법이 좀 더 복잡하다. 그게 아니면(else) 구문도 있고, 그것이 아닌가?( if a 가 1이 아니면) 같은 문법도 있다. 아직은 때가 아니니 이 정도만 알아두자. 저런 문법이 나올 때 마다 하나씩 다룰 것이다.


우와! 드디어 이번 글을 마칠 때가 됐다. 이번 글을 마치면서 전체 강좌에서 반 정도 마쳤다. 다음 주와 다다음 주까지가 강좌 분량도 많고 진행도 빨라서 정신이 없지만, 이 두 주만 넘기면 한결 가볍고 편안할 것이다.</p>

이번 글은 역대 최고 분량이었던 지난 글 보다도 거의 두 배나 돼서 참 힘들었다. 날도 무지 더웠고, 소스 코드 검증도 자주 해야 했으며 그림도 많았다. 약 31,000개 글자 이상, 6,500개 낱말 이상으로 쓰여질만큼 양이 많다. 날도 더워 집중도 잘 안되고 휴가철이라 마음도 이리 저리 흔들리겠지만 부디 차근 차근 잘 따라와서 다음 글도 부담없이 맞이하길 바란다. :)

다음 글에서는 글 입력과 수정, 삭제 기능을 만들 것이다.


각주

admin 영역에서 자료 좀 더 예쁘게 나타내기

django admin 영역에서 각 모델에 있는 자료들을 다루려 할 때 "뭐뭐뭐 object" 형태로 떠서 각 개체가 무엇인지 알아보기에 좋지 않다. Entries 모델을 보더라도 Entries object 라고 뜨지 말고 글 제목과 id, 그리고 작성일시가 뜬다면 한결 각 글(개체)을 구분하기 좋다.

우리가 admin 영역에서 모델을 다루고 싶을 때 class Admin: 이라는 부분을 추가했었다. 그 바로 아래 줄에는 pass 라고 써서 별도 설정을 하지 않았는데, 여기에 별도 설정을 하여 admin 영역에서 모델의 개체들을 좀 더 알아보기 좋게 나타낼 수 있다.

이는 class Admin: 에 list_display라는 프로퍼티를 이용하면 된다.

class Entries(models.Model):
    Title = models.CharField(max_length=80, null=False)
    Content = models.TextField(null=False)
    created = models.DateTimeField(auto_now_add=True, auto_now=True)
    Category = models.ForeignKey(Categories)
    Tags = models.ManyToManyField(TagModel)
    Comments = models.PositiveSmallIntegerField(default=0, null=True)

    class Admin:
    	list_display = ('id', 'created', 'Title')

위 내용은 글 정보 모델(Entries)에서 class Admin 부분을 고친 것이다. pass 를 저렇게 바꿨다. 이젠 굳이 자세한 설명을 하지 않아도 저것들이 각 각 무엇을 뜻하는지 잘 알 것이라 생각한다. 저장을 하고 admin 영역에서 Entries 모델을 눌러서 개체들을 보자. 뭔가 다른 화면이 나올 것이다. :)

주의해야 할 점은 list_display 변수에 넣을 값은 list 형이나 tuple 형이어야 한다는 점이다. 위 코드에서는 'id', 'created', 'Title' 이라고 넣었듯이 tuple 로 넣었다. 이 말은

list_display = ('id')

이렇게 값을 하나만 넣을 때 맨 끝에 쉼표를 넣지 않고 위와 같이 넣으면 내장 웹서버가 실행될 때 오류를 낸다. 그러므로 위와 같은 경우엔

list_display = ('id',)

이렇게 쉼표를 찍어 일반 문자열이 아닌 tuple 자료형으로 넣어줘야 한다.

이에 대한 보다 자세한 설명은 django 공식 문서에 있는 Customize the admin change list 부분에 있다.

쪽 번호 값 확인하고 예외 상황 처리하기

글 목록 기능에서 쪽 번호를 다룰 때 예외 상황을 고려해야 한다. 이용자가 순순하게 바로 된 쪽 번호를 써주지 않을 것이라는 생각을 해야 한다. 이를테면 쪽 번호로 -1 을 요청하거나 존재하는 총 쪽 개수보다 큰 값을 요청할 수 있고 숫자가 아닌 값을 넣을 수도 있다. 위에서는 이런 처리를 생략했는데, 생략할만 해서 생략한 건 아니다. 이런 예상치 못한 문제가 생겼을 때를 대비하는 방법을 알아보자.

우선 쪽 번호는 언제나 숫자여야 한다. 변수에 있는 값이 숫자인지 아닌지는 어떻게 가려낼까? 여러 방법이 있지만 여기선 isinstance 라는 파이썬 기본 내장 함수(built-in function)을 쓰려 한다.

isinstance(확인할 값, 자료형)

이렇게 하면 참(True)이나 거짓(False)를 반환해준다(자료형 말고도 클래스 등을 넣을 수도 있다). 파이썬 인터프리터 상태에서 확인해보자. python 이라고 치면 python 인터프리터 상태에 진입한다. 그 상태에서 다음과 같이 해보자.

Python 2.5.2 (r252:60911, Jul 1 2008, 17:26:00)
[GCC 4.1.2 (Gentoo 4.1.2 p1.1)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> isinstance(1, int)
True
>>> isinstance(
'a', int)
False
>>>

확인할 값을 1을 넣고 확인할 자료형으로 int 을 넣으면 두 자료형이 같으므로 True 를 반환한다. 그러나 'a' 를 넣으면 int 와 같지 않으므로 False 를 반환한다. 이걸 쪽 번호가 숫자인지 확인을 한다면

if isinstance(page, int) == False:
    page = 1

이렇게 하면 된다. 쪽 번호(page 변수)가 숫자가 아닌 경우 강제로 page 에 숫자 1을 넣는 것이다.

이 숫자가 1보다 작은지 확인하거나 총 쪽 개수 값 보다 큰지 확인은 스스로 해보자. 1보다 작으면 마찬가지로 강제로 page 에 1을 넣으면 된다. 총 쪽 개수 값 구하는 건 총 글 개수부터 구한 뒤 한 쪽에 출력할 글 개수로 어찌 저찌 셈을 하면 된다. 위에서 쪽 번호들 출력하는 걸 숙제로 내줬는데 소스 코드는 거의 동일하다. 이용자가 접근하려는 쪽 번호가 총 쪽 개수 값 보다 클 경우 총 쪽 개수 값(맨 마지막 쪽)을 강제로 넣으면 된다.

글 갈래와 꼬리표 더 성능 좋게 가져와 다루기

4번 글에 달린 댓글이 10개가 있을 때, 4번 글 정보 따로, 그리고 4번 글에 달린 댓글 10개 따로 가져오면 여러 모로 귀찮다. 간편하게 4번 글을 가져오면 알아서 이 글에 달린 댓글 10개도 함께 가져오는 게 낫다.

django 는 모델을 통해 DB에서 자료를 가져올 때, 해당 모델과 연관된 다른 모델에 있는 값을 손쉽게 다룰 수 있는 기능을 제공한다. 따로 기능을 만들거나 소스 코드에 반영하지 않아도 위에서 이미 그러했듯이 django 가 알아서 해준다. 이런 식이다.

  1. A모델에서 자료를 가져와
  2. A모델과 연결된 B모델에서 관련 자료 가져와
  3. A모델과 연결된 C모델에서 관련 자료 가져와
  4. A모델과 연결된 D모델에서 관련 자료 가져와

근데 이 방식엔 단점이 있으니 바로 성능이 다소 떨어지는 점이다. A모델과 연결된 B, C, D 모델에서 관련 자료를 가져오라고 시킬 때마다 DB에 접근해서 정보를 가져온다. 어차피 다 가져올 것이라면 A모델에서 자료 가져올 때 B, C, D 모델 자료도 다함께 한 번에 가져온다면 DB에 한 번 접근하고도 위 4과정을 처리할 수 있기에 더 효율이 좋다. 이럴 때 쓰는 메소드가 select_related() 이다. 사용법은 간단하다.

entries = Entries.objects.all().order_by('-created')[start_pos:end_pos]

이렇게 된 부분에

entries = Entries.objects.all().select_related().order_by('-created')[start_pos:end_pos]

이런 식으로 메소드를 껴넣으면 된다. 보다 자세한 설명과 예제는 django 공식 문서에서 select_related() 부분을 참조 바란다.

** 2009년 3월 27일 덧씀 : 이형욱님 댓글 내용을 확인한 뒤 select_related() 메서드 쓰는 방법을 제대로 고침.

주의할 점은 이 방식이 언제나 성능에서 더 나은 효율을 가져오는 건 아니라는 점이다. 대체로 DBMS에 자주 접속을 하는 것보다는 더 조금 접속을 하는 것이 성능에서 낫긴 한데, 너무 복잡한 요청을 DBMS에 하면 도리어 성능이 제대로 나지 않을 수 있다. 이건 쉽지 않은 영역이니 따로 더 깊게 공부를 하거나 DB 관리자 영역으로 넘기는 게 낫다.


이번 글에선 파이썬 기초 부문을 다루지 않으려 한다. 이미 이 글에서 다루고 있는 내용만으로도 여러분들을 바쁘게 할 것인데다 숙제도 있기 때문이다. :)</p>

아래는 이번 글에서 만든 소스 코드인데 템플릿을 쓰지 않고 모델로 DB에서 글을 가져와 출력하는 파일 묶음과 템플릿을 써서 출력하는 파일 묶음으로 나눴다.