년) 템플릿 작업과 Ajax 작업 (6편) - django 강좌

여태껏 django 와 파이썬을 공부해왔는데, 이런 쪽을 이용자 눈에 보이지 않는 서비스 뒤에서 작동하는 영역이라 해서 back-end (뒷단) 영역이라고 한다. 이번 글에서는 이용자 눈과 손에 바로 닿아 있는 front-end (앞단) 영역을 다룬다. 지난 글에서는 대충 뼈대 위주로 html 파일을 만들었는데, 이번엔 html 과 css, javascript 를 제대로 붙이는 것이다.

HTML, XHTML

요즘 많이 쓰이는 HTTP 문서 방식은 html 4.01 과 xhtml 1.0 이다. html 이 뭔지는 익히 알테니 xhtml 을 중심으로 간단히 알아보자.

xhtml 은 html 에 xml의 성격과 요소를 더한 언어이다. 좀 더 정확히는 xhtml 1.0 은 html 4.01 규격에 xml 요소를 더하였고, 문서 안에 있는 자료의 의미론으로는 두 규격 차이는 거의 없이 비슷하다.

xml 은 차세대 자료 규격으로 각광받고 있고, 이미 많은 곳에서 활발히 쓰이고 있는 언어이다. 사람이 알아보기 쉬운점(...이 장점이라고 여러 사람이 말하는데 난 전혀 동의하지 않는다), 확장성이 좋은 점, 서식/수식과 자료(data)가 깔끔하게 분리되어 있다는 점, 자료 구조가 표준화 되어있어 기종이나 환경을 타지 않고 쉽게 자료를 쓸 수 있다는 점이 특징이다.

이런 xml 기능을 웹 화면에 쓸 일은 사실 거의 없다. 이 말은 xhtml 기능을 써야만 하는 경우도 별로 없고 xhtml을 제대로 쓰는 경우도 많지 않다. 그런데도 왜 이 강좌에서는 초기에 html 4.01 대신 xhtml 1.0 쓰자고 했을까? 앞으로 xml 이 많이 쓰일 가능성이 크기도 하지만 좀 더 양식화/구조화 된 문서 구조이기도 하다. xhtml 이 html 보다 문법이 더 엄격하기 때문이 아니라 xhtml 이 xml 문법을 따르기 때문이다. 무엇보다 xhtml 1.0과 html 4.01 은 거의 차이가 없을 정도로 웹브라우저 호환성을 따를 수 있으니, 그러니까 html 4.01 대신 xhtml 1.0 을 써도 무방할 것이니 xhtml 1.0을 쓰기로 한 것이다.

더 많은 내용은 wystan님께서 쓰신 xhtml 과 html 글에서 얻을 수 있으며, 많은 도움을 받을 수 있다.

이외 css 와 javascript 는 작성한 html 를 꾸밀 때 마다 설명을 할 것이다.

글 목록 화면 html 제대로 꾸미기

미리 말하지만 여기서는 공부를 위해 겉모양은 별 신경을 안쓸 것이다. 결코 본인의 디자인 감각이 떨어져서 그런 것이 아니다. ^^

아무튼 우선 list.html 을 다음과 같이 바꿨다.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
	<title></title>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<link rel="stylesheet" href="/media/css/style.css" type="text/css" media="all" />
</head>

<body>

<h1></h1>

<div id="content">
	{% for entry in entries %}

	<div class="post_entry">
		<h3 id="post_" class="post_title"><a href="/blog/entry/"></a></h3>

		<p class="post_info">글 갈래 : [  ] / <a href="/blog/entry/"></a></p>

		<div class="content_box"></div>

		<ul class="post_meta">
			<li>꼬리표 : {% for tag in entry.Tags.all %}
			<span></span>
			{% endfor %}</li>
			<li><a href="/blog/get_comments/">댓글 ()</a></li>
		</ul>

	</div>

	{% endfor %}
</div>



<div id="sidebar"></div>


</body>

</html>

맨 위에 <!DOCTYPE 로 시작하는 부분은 DOCTYPE 을 선언하는 것이다. 지금 열고 있는 문서가 xhtml 라는 걸 선언하고 이 문서의 DTD 선언도 하고 있다. 흔히 사람들은 이 부분을 가볍게 여겨서 무시하고 선언하지 않는 사람도 있는데 많이 중요하다. html/xhtml 문서를 가장 작게 만들 때 꼭 들어가야 하는 요소이기도 하다. 자세한 건 후니님께서 쓰신 HTML 에서 문서 형식(Doctype) 지정의 중요성 글을 읽자. 꼭 읽자.

다음엔 <html 로 시작하는 부분이 있는데 보통 <html> 이라고 쓰는 부분이다. 뒤에 붙은 xmlns="http://www.w3.org/1999/xhtml" 은 xml 문서의 네임 스페이스(name space)를 쓴 것이다. 자세한 건 XML namespace 문서를 참조하자.

이젠 head 태그를 열어서 안에다 이 문서가 UTF-8 라는 걸 메타 태그로 선언하고, css 파일을 문서에 포함시키고 있다. 여기서 잠깐. 이 바깥 css 파일을 html 문서 안에 적용하는 방법은 또 하나 더 있다.

<style type="text/css">
@import url(/media/css/style.css);
</style>

이렇게 하는 것인데 뭐가 다를까? 다른 점이 많지만 가장 크게 중요한 다른 점은 link 는 html 요소(element)이므로 html 문서단에서 처리되고, import 는 style sheet 요소(기능)이다. link 태그는 css 파일을 연결하는 것 말고도 다른 쓰임새가 더 있지만 import 는 오직 바깥 css 파일을 포함시키는 쓰임새만 있다. 이런 차이 탓에 웹브라우저에 따라 처리 우선순위가 다를 수 있다. 예를 들면, 인터넷 익스플로러에서는 link 태그 처리 우선순위가 이미지 파일 처리 우선순위보다 높고, style sheet import 기능은 이미지 파일 처리 우선순위 보다 낮다고 한다. 몰라도 별 지장은 없지만, 혹 앞단(front-end)을 깊게 파고들어 공부하고 싶은 이라면 <link> 태그로 바깥 css 파일을 가져와 적용하는 것과 위와 같이 하는 것이 어떻게 다른지 더 알아보길 권한다. :)

다음으로 볼 부분은 /media/css 이다. django 에서 어떤 url 을 쓰려면 urls.py 에 정의해야 하는데 우리는 urls.py 에 이런 주소를 넣은 적이 없다. 이미 urls.py 에 주소 체계 넣는 법을 익혔으니 간단하게 처리할 수 있을 것이다. 과연? 그렇지 않다. 이 부분은 조금 다른 설정이 필요하다.

urls.py 에 주소 체계를 넣을 때 다음과 같은 구조이다.

(주소규칙, 연결할 함수)

그럼 /media 는 어떻게 해야할까? /media 뒤에 붙는 내용이 뭐든 상관 없이 /media 아래에 css 나 javascript 파일을 둔다고 하면

r'^media/(?P<path>.*)$'

이런 식으로 정규표현식을 써야 한다. 그럼 이걸 어디다 넘긴다? print_media 라는 함수를 만들고 거기서 저 path 로 넘어온 파일을 가져와서 출력해야 할까. 그것도 일일이 css 인지 javascript 인지 이미지 파일인지 구분을 해서? 그렇다면

r'^media/(?P<path>.*)$', 'hannal.blog.views.print_media'

이런 비슷한 모양새가 될 것이다. 맞다. 그래도 된다. 하지만, 이런 걸 굳이 우리가 만들지 않고 django 에서 제공하는 기능을 쓰면 간편하다.

(r'^media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': ROOT_PATH+'/media'}),

바로 django.views.static.serve 라는 부분으로써, django.views.static 에 있는 serve 라는 함수이다. django.views.static.serve 는 위와 같이 고정된 파일을 출력할 때 쓴다. 해당 파일이 서버에 있는지 확인해서 있으면 출력하고 없으면 urls.py를 참조해서 처리하게 하면 되지, 뭐하러 이렇게 고정된 파일에 접근할 곳을 지정할까 싶겠지만 django 에선 이런 처리를 기본으로 해주지 않는다. 왜냐하면 이런 설정은 웹서버 설정을 따르면 그만이기 때문이다. 예를 들면, 아파치 웹서버라면 rewrite rules 에서 지정하면 된다. 그래서 django 에서는 이를 기본 기능처럼 자동으로 처리하지 않고 이용자가 선택 하게 별도 기능으로 빼놨다.

settings.py 에도 관련 설정을 해야 한다. 실은 안해도 출력이 되긴 된다. settings.py 안에 보면 MEDIA_ROOT 변수와 MEDIA_URL 변수, 그리고 ADMIN_MEDIA_PREFIX 변수가 있다.

MEDIA_ROOT 는 매체 파일이 있는 절대 경로이다.

MEDIA_ROOT = ROOT_PATH + '/media/'

이렇게 지정하면 된다. ROOT_PATH 는 settings.py 파일 맨 위에 저번에 지정했다.

MEDIA_URL 은 웹 경로(url)을 뜻하는데

MEDIA_URL = '/media/'

라고 해두자. http://localhost:8000/media/ 이렇게 실제 웹 주소까지 쓰는 것이 좋다.

ADMIN_MEDIA_PREFIX 가 중요한데, 절대로 MEDIA_URL 과 같아서는 안된다. 이는 django admin 영역에서 사용할 각종 매체 파일이 있는 웹 경로에 쓰이는데, 이 경로가 MEDIA_URL 과 같으면, 웹에서 해당 파일이 존재하지 않는다는 오류가 발생한다. MEDIA_URL 과 똑같지만 않다면 무어라 쓰든 상관없다.

ADMIN_MEDIA_PREFIX = '/media/admin/'

적절하게 위와 같이 하자. 그러면 urls.py 상관없이 admin 영역 매체 파일은 위 경로에서 접근해서 가져온다. 이게 가능한 이유는 /django/core/servers/basehttp.py 안에 있는 AdminMediaHandler 클래스가 이런 일을 대신 해주기 때문이다.

앞으로 /media 안에 css, javascript 파일 등을 둘 것이니, urls.py 과 settings.py 파일이 있는 디렉토리(폴더)에 media 라는 디렉토리를 만들자. 그리고 그 안에 css 라는 디렉토리를 만들고, css 디렉토리에 style.css 파일을 만들어 다음 내용을 넣자.

body {
	background-color: #fff;
	font-size: 0.9em;
}
a {
	text-decoration: none;
	color: #449;
}
	a:hover {
		color: #944;
	}

#content {
	width: 700px;
}

.post_entry {
	border: 1px solid #000;
	margin: 10px 0 10px;
	color: #666;
	padding: 10px;
}


.post_title {
	margin: 0 0 5px 0;
	background-color: #eaeaea;
	padding: 2px 3px;
}
	.post_title a {
		color: #558;
		text-decoration: none;
	}
	.post_title a:hover {
		color: #855;
	}

.post_info {
	margin: 0;
	border-bottom: 2px solid #999;
	font-size: 0.8em;
}

.content_box {
	margin: 20px 0;
	padding: 0 2em;
}

.post_meta {
	font-size: 0.8em;
	color: #757575;
	margin: 10px 0 0 10px;
	padding: 0;
}
	.post_meta li {
		display: inline;
		margin-right: 30px;
	}

위 내용들을 저장한 뒤 블로그 글 목록 화면을 보자.

html 공통 부분 뽑아내기

<html> 이나 <head> 같은 윗쪽 부분과 나 </head></html> 같은 아랫쪽 부분은 list.html 뿐 아니라 read.html 에도 쓰고 write.html 에도 쓴다. 그리고 이러한 윗쪽 부분이나 아랫쪽 부분을 바꿀 경우, 이 세 파일에도 똑같이 반영되어야 할 것이다.

이렇게 일일이 파일 세 곳을 고치면 귀찮다. 글 낱장 읽기 기능을 만들 때 댓글 영역을 comments.html 로 빼낸 것처럼 html 윗쪽과 아랫쪽을 header.html 과 footer.html 로 빼낸 뒤, list.html, read.html, write.html 에서 포함시키게 해보자.

우선 header.html 파일과 footer.html 파일은 템플릿 파일이 있는 폴더(/templates) 안에 layout 이라는 폴더를 하나 더 만든 뒤 그곳에 넣자.

먼저 header.html 는 다음과 같이 넣는다.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
	<title></title>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<link rel="stylesheet" href="/media/css/style.css" type="text/css" media="all" />
</head>

<body>

그리고 footer.html 은

</body>

</html>

이라고 넣는다. 그리고 list.html 에서 위 html 코드를 지운다. 다음엔 list.html 파일 안 맨 위에

{% include 'layout/header.html' %}

를 넣고, 맨 아래엔

{% include 'layout/footer.html' %}

를 넣는다. 이런 코드를 read.html 와 write.html 에도 넣으면 된다.

댓글을 ajax 로 가져오기

댓글 가져와 출력하기

이번엔 댓글을 ajax 로 가져와 보자. html 코드 중간에 보면 댓글 출력하는 부분에 주소 연결(link)가 되어 있다.

<li><a href="/blog/get_comments/">댓글 ()</a></li>

이제 척하면 척이다. “/blog/get_comments/글번호” 주소 체계를 urls.py 에 넣자.

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

그리고 views.py 에 get_comments 함수를 만들면 되는데

def get_comments(request, entry_id=None):
    comments = Comments.objects.filter(Entry=entry_id).order_by('created')
    pass

일단 이렇게 간단하게 기본 틀을 짰다. 여기서 잠깐 고민. 가져온 댓글을 어떻게 보내줘야 할까. 가져온 댓글 자료형 그대로 보내주고 javascript 로 화면에 맞게 html 을 만드는 게 좋을까, 아니면 서버에서 댓글 목록 화면을 다 만든 뒤에 이걸 통채로 화면에 끼워 넣는 게 좋을까. 사람 취향이겠지만 경험상 후자 방식이 더 편하더라.

후자 방식으로 하면 아주 간단하게 마무리 할 수 있다.

tpl = loader.get_template('comments.html')
ctx = Context({
    'comments':comments
})
return HttpResponse(tpl.render(ctx))

을 추가하면 된다. comments.html 은 글 낱장 읽기(read.html)를 만들 때 만들었다. 그래서

def get_comments(request, entry_id=None):
    comments = Comments.objects.filter(Entry=entry_id).order_by('created')

    tpl = loader.get_template('comments.html')
    ctx = Context({
        'comments':comments
    })
    return HttpResponse(tpl.render(ctx))

이렇게 된다.

ajax 방식 구분에 따라 출력 화면 다르게 하기

이번엔 comments.html 을 어떤 방식으로 가져왔는지 구분하는 기능을 넣어보자.

comments.html 을 가져와 출력하는 접근 방식은 크게 세 가지이다.

  • read.html 처럼 다른 템플릿 파일에서 포함시키기
  • ajax 로 get_comments 함수를 통해 comments.html 만 가져오기
  • get_comments 함수를 통해 comments.html 를 가져오되 ajax 가 아닌 보통 웹 접근(get method) 방식으로 접근하기

문제가 되는 부분은 세 번째이다. 예상 밖 문제가 생겨서 javascript 가 작동하지 않을 경우 적어도 서비스가 아주 응답이 없거나 오류가 발생하는 상황은 피해야 한다. 단지 잠깐 네트워크에 문제가 생겨서 javascript 파일을 제대로 가져오지 못했을 뿐인데, 이 때문에 이용자가 아무 일도 할 수 없어 서비스 신뢰도가 떨어지면 얼마나 억울한가. 이외에도 이용자가 순순히 ajax(javascript)로 접근하지 않을 가능성도 감안하면 위 세 가지 방식 중 세 번째 방식도 신경 써야 한다.

무슨 말이 하고 싶어 이렇게 길게 뜸을 들이는 것이냐면 request 객체에 있는 is_ajax() 라는 메소드를 설명하기 위해서이다. 이용자가 서버에 접속할 때 접속 방식이 ajax 인지 아닌지를 구분해서 그에 맞는 대응을 하는데 필요한 메소드가 is_ajax 이다. 잠깐. request 객체는 어디서 튀어 나온 녀석일까? 우리가 views.py 에서 함수를 만들 때 request 를 인자로 받은 걸 기억할 것이다. 바로 그 request 이다.

사용법은 간단하다. request.is_ajax() 로 메소드를 호출하면 이용자 접속/접근 방식이 ajax 이면 true (참), 아니면 false (거짓)을 반환한다.

그런데 문제가 하나 있다. 이 메소드는 django 0.97 이상부터 쓸 수 있다. 0.96 엔 아직 추가되지 않은 메소드이므로 별도 처리를 해야 한다. 우선 is_ajax 라는 함수를 views.py 안에 만들자.

<blockquote <pre>def is_ajax(request): if dir(request).count('is_ajax') > 0: return request.is_ajax() else: return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'</blockquote>

request 객체를 인자로 넘겨 받은 뒤, 이 객체에 is_ajax 라는 요소가 있는지 확인한다. 있으면 1이 반환되므로 request 의 is_ajax 메소드를 실행시켜 그 결과값을 반환한다. 만약, 없으면(0이면) request 객체에서 http meta 정보 중 HTTP_X_REQUESTED_WITH 를 가져온 뒤 이 값이 XMLHttpRequest 인지 확인한다. 동일하면 ajax 로 요청을 한 것이다. 실제로 request.is_ajax 메소드는 작동이 request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' 이것과 동일하다. 그러므로 if 조건문을 걸 필요 없이

<blockquote <pre>def is_ajax(request): return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'</blockquote>

이렇게 하는 것이 낫다. 하지만, 공부 차원에서 위와 같이 if 조건문으로 상황을 구분했다.

따로 is_ajax 함수를 만들지 않고 request 객체에 is_ajax 라는 메소드를 추가해서 django 0.96판에서도 request.is_ajax() 로 쓰는 방법도 있다.

import new
request.is_ajax = new.instancemethod(lambda request: request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest', request, request.__class__)

이렇게 하면 기존에 is_ajax 메소드가 없던 request 에 is_ajax 메소드가 추가된다. 하지만 그다지 권장하고 싶진 않으니 위와 같이 별도 is_ajax 함수를 만든 뒤 request.is_ajax 라고 쓰는 대신 is_ajax(request) 라고 쓰자.

if is_ajax(request):
    # ajax 요청입니다.
else:
    # ajax 요청이 아닙니다.

이런 식이다.

ajax 방식으로 “/blog/get_comments/숫자” 주소로 접근하면 댓글 목록만 반환하고, ajax 가 아닌 방식으로 접근하면 완성된 html, 그러니까 header.html 과 footer.html 을 포함시켜서 댓글 목록을 출력하도록 하자.

if is_ajax(request):
    with_layout = False
else:
    with_layout = True

먼저 get_comments 함수 (def get_comments)에 위와 같이 ajax 상황을 구분한다. with_layout 변수는 comments.html 템플릿 파일에서 쓸 치환자인데, 이 값이 True 이면 header.html 와 footer.html 을 가져오고, False 이면 가져오지 않게 하는 데 쓴다.

다음엔 comments.html 파일 맨 위에

{% if with_layout %}{% include 'layout/header.html' %}{% endif %}

이렇게 해서 with_layout 변수(치환자)가 True 이면 layout/header.html 을 가져오게 하고, 맨 아래엔

{% if with_layout %}
	{% include 'layout/footer.html' %}
{% endif %}

같은 작동을 하되 layout/footer.html 을 가져오게 한다. header.html 가져오는 부분은 if조건 치환문과 include 치환문을 공백없이 붙여 쓴 이유는 header.html 파일 맨 위에 있는 DOCTYPE 선언 부분을 html 문서 맨 위와 앞에 넣어야 하기 때문이다. 만약

{% if with_layout %}
	{% include 'layout/header.html' %}
{% endif %}

라고 하면 html 문은


	<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

이렇게 출력되기 때문이다. 앞에 공백이 들어가면 안되므로 신경 쓰자.

이번엔 댓글 작성 폼(form)을 comments.html 안에 넣자. 지난 글에서는 read.html 에서 comments.html 과 comment_form.html 을 각 각 include 치환문으로 가져왔지만, 이번 글부터는 comment_form.html 을 comments.html 안에 넣는다. 여러분을 귀찮게 하기 위함보다는 include 치환문을 설명하려다 보니 그렇게 됐다. ^^; 어쨌든

{% include 'comment_form.html' %}

이 내용을 comments.html 에서 footer.html 를 include 할 지 말 지 판단하는 if조건 치환문 위에 넣는다. 그러면 comments.html 문은 이렇게 생겼을 것이다.

{% if with_layout %}{% include 'layout/header.html' %}{% endif %}

	{% if comments|length %}
	<ul>
	{% for comment in comments %}
		<li>님이 에 남긴 댓글
		<p></p></li>
	{% endfor %}
	</ul>
	{% else %}
	댓글이 없습니다.
	{% endif %}

	{% include 'comment_form.html' %}

{% if with_layout %}
	{% include 'layout/footer.html' %}
{% endif %}

그런 뒤 read.html 에서

{% include 'comment_form.html' %}

이 부분을 지우자. comments.html 에 포함됐기 때문이다.

이제 상황에 따라(ajax 접근인지 아닌지) comments.html 파일은 다르게 출력된다.

ajax 작동을 위한 html 기반 작업

javascript 코드를 작성하기 전에 javascript 기능을 위한 html 기반 마무리를 하자. list.html 파일을 살짝 고치면 된다.

<li><a href="/blog/get_comments//">댓글 ()</a></li>

이렇게 된 부분을

<li><a href="/blog/get_comments//" onclick="toggle_comment_box(this.href, ''); return false;">댓글 ()</a></li>

이렇게 고친다. onclick 어쩌고 저쩌고 내용을 추가한 것이다. 이번엔

<div id="comment_box_" style="display: none;"></div>

이 내용을 아래에 추가한다. 아마 댓글 수를 출력하는 근처 html 은 이렇게 생겼을 것이다.

	<li><a href="/blog/get_comments//" onclick="toggle_comment_box(this.href, ''); return false;">댓글 ()</a></li>
</ul>

<div id="comment_box_" style="display: none;"></div>

하나씩 살펴보자. onclick 부분은 댓글 (숫자) 이 부분을 클릭했을 때 실행할 javascript 내용을 써넣은 것이다. on click 을 뜻하는 말로 click 이 일어난 상황을 뜻하며 이런 “상황”을 이벤트(event)라고 부른다. 이런 이벤트는 여러 종류가 있는데 일단 onclick 만 알아두자.

onclick 이벤트 부분을

onclick="alert('hello world'); return false;"

이렇게 바꾼 뒤 마우스로 클릭하면 hello world 라는 문자열을 담은 돌출창(pop-up window)이 뜬다.

return false; 는 return false; 앞부분까지만 일을 처리하고 여기서 작동을 중단시키는 역할을 한다. 만약 return false; 를 지우거나 return true; 라고 하면, return false; 앞에 있는 javascript 내용을 실행한 뒤에 a 태그(앵커)로 연결(link)한 주소로 화면 이동을 할 것이다. javascript 내용만 실행한 뒤 굳이 a 태그로 지정한 주소로 이동할 필요가 없으니 return false; 로 작동을 끝내자.

toggle_comment_box 이라는 javascript 함수는 우리가 top.js 에서 만들 것이다. 인자 두 개를 넘기면 이 인자 두 개로 적절한 일을 할 것이다. 첫 번째 인자는 댓글을 가져올 주소(/blog/get_comments/글일련번호(숫자))이다. 이 주소는 a 태그에서 href 에 있다.

<a href="/blog/get_comments//" onclick="...">

이렇게 말이다. 그래서 this.href 라고 값을 넘겼다.

toggle_comment_box(this.href, ...);

this 는 객체 자기 자신을 가리키는 javascript 표현(keyword)이다. a 태그에 있는 href 라는 속성은 javascript 에서는 a 태그라는 객체에 있는 href 라는 프로퍼티처럼 다룬다.

  • a 태그 = a 태그로 만들어진 객체(element)
  • href 속성 = 이 객체에 있는 href 라는 프로퍼티
  • this = a 태그 객체 안에서는 자기 자신을 가리킴

두 번째 인자는 어떤 글인지를 구분하기 위한 글 번호이다.

<div id="comment_box_" style="display: none;"></div>

이런 내용을 추가했는데, 이 상자는 ajax 로 가져온 댓글 목록 내용을 넣을 공간이다. 글 목록 화면에는 글이 여러 개 출력되므로 이런 상자도 여러 개이다. 이 상자를 각 각 구분해서 댓글 목록 내용을 넣으려고 글 일련번호(id)로 각 상자를 구분하고 있다. 10번 글이라면 comment_box_10 이라는 div(division) 상자의 id 가 되고, 1023번 글이라면 comment_box_1023 이 된다. 그리고 우리는 javascript 에서 10이나 1023 같은 글 일련번호를 넘겨줘서 저러한 상자를 구분하는 것이다.

prototype.js 를 이용하여 ajax 로 댓글 가져오기

이제 javascript 작업을 할 차례이다. 우리는 javascript framework 로 prototype 을 쓰기로 했으니 이 라이브러리를 가져다 쓰고, 우리가 직접 만든 javascript 도 가져와야 한다.

먼저 header.html 부터 고치자.

<script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.2/prototype.js"></script>
<script type="text/javascript" src="/media/js/top.js"></script>

prototype.js 는 prototype 공식 누리집에서 직접 받아다 포함시켜도 되지만, 여기서는 편의상 google AJAX Libraries API 에서 제공하는 prototype.js 를 바로 접근해서 가져왔다. 이에 대한 내용은 likejazz님이 쓰신 “구글 AJAX Libraries API” 글을 참조 바란다.

우리가 직접 작성할 top.js 는 media 디렉토리 안에 있는 js 디렉토리 안에 있다. 지금까지는 media 디렉토리에 css 디렉토리가 있었는데 이번에 js 디렉토리를 만들고 그 안에 top.js 파일을 만들자.

그런 뒤 아래 내용을 넣는다.

var toggle_comment_box = function(url, entry_id) {
	var el = $('comment_box_'+entry_id);

	if ( el.visible() == true ) {
		el.hide();
	}
	else {
		var ajax = new Ajax.Updater(el, url);
		el.show();
	}
}

toggle_comment_box 는 list.html 에서 onclick 이벤트가 일어났을 때 실행시킬 함수로 지정했었다. 그 함수를 만든 것이다.

우선 var toggle_comment_box 는 toggle_comment_box 라는 객체(변수)를 선언하는 것이다. 이 객체에 넣을 값을 function 이라는 자료형으로 넣으려고

function(url, entry_id) { ... }

이렇게 한 것이다.

javascript 에는 자료형이 총 7개가 있다.

  • Number (숫자)
  • String (문자, 문자열)
  • Boolean (부울린. 참과 거짓)
  • Function (함수)
  • Object (객체)
  • Null (없음)
  • Undefined (선언되지 않음)

정말이다. Array 자료형 같은 것도 있는 것 같은데 이런 것이 위 목록에 없어서 믿기지 않는다면 javascript 에서

alert(typeof Array);

라고 하면 function 라고 뜬다. javascript 를 앞으로 자주 쓸 것이라면 위 7가지는 외워두자.

주의해야 할 점은 우리가 흔히 표현하는 객체라는 표현과 위에 나온 Objects (객체)는 구분을 해야 하는 점이다. javascript 에서는 (거의) 모든 것을 객체로 취급하는데 이 객체의 자료형 중엔 Number 나 String 형이 있듯이 Object 라는 자료형도 있기 때문이다(이 글에선 구분을 위해서 Object 자료형을 뜻할 땐 Object 라고 쓰겠음). 어쨌든 Function 이라는 자료형을 toggle_comment_box 라는 객체에 넣는 것이

var toggle_comment_box = function( ... ) { ... }

이 코드이다. 이제 함수 안을 하나씩 뜯어보자.

var el = $('comment_box_'+entry_id);

이것은 우리가 쓰기로 한 prototype.js 에 있는 $ 라는 함수를 쓴 모습이다. $ 함수는 html 문서 안에 있는 요소(element)를 골라서 javascript 객체에 담을 수 있게 해준다. javascript 에는 getElementById 라는 함수가 이 역할을 한다. document 라는 객체에 있는 메소드로써

var el = document.getElementById('comment_box_'+entry_id);

이렇게 해도 된다. 그런데 굳이 prototype.js 에 있는 $ 함수를 쓴 이유는 prototype.js 에 있는 Element 객체가 아주 편하고 강력한데 이 Element 객체 기능들을 $ 함수가 포함시키기, 즉 상속 받아 놓기 때문이다. Element 객체 뿐 아니라 Form 객체 등 자주 쓰는 prototype.js 의 객체들에 있는 메소드들을 함께 상속 받는다.

잠시 파이썬과 django 를 떠올려보자. models.py 안에서 모델을 만들 때 django 에서 제공하는 모델 각종 기능들을 상속 받아서 이를 편하게 활용했다. 예를 들면, Entries 라는 모델 클래스를 만들면 django 에 있는 모델 기능들이 Entries 에 달라붙어(상속되어)

new_entry = Entries(...)
new_entry.save()

위와 같이 save 같은 메소드를 쓸 수 있었다. prototype.js 의 $ 함수도 마찬가지이다. $ 함수로 html 요소(element)를 선택하면, 이 객체에 prototype.js 에 있는 Element 객체도 상속시킨다. 그래서 javascript 에 기본 내장된 getElementById 대신 써서 좀 더 편리함을 누리는 것이다. 그럼 prototype.js 에 있는 편리한 Element 객체 기능은 무엇이 있을까? 바로 다음 줄에 나온다. 어쨌든

var el = $('comment_box_'+entry_id);

이건 저 이름을 가진 html element 를 가져와서 el 라는 변수(객체)에 담은 것이다. el 은 element 를 줄인 이름으로 여러분 마음에 들지 않으면 적당한 걸로 바꿔도 된다. 만약 10번 글에 있는 댓글 (숫자)를 클릭했다면

var el = $('comment_box_'+10);

과 같은 코드가 된다. 'comment_box_'+10 는 comment_box_ 문자열에 10 을 덧붙인 것으로 comment_box_10 과 같다. 이후부터는 10번 글의 댓글을 가져오는 상황으로 가정한다.

if ( el.visible() == true ) {
	el.hide();
}
else {
	var ajax = new Ajax.Updater(el, url);
	el.show();
}

위 코드에서 visible 이라는 메소드가 바로 prototype.js 에 있는 Element 객체의 메소드이다. visible 메소드는 해당 element 가 현재 출력을 한 상태인지 아닌지 확인해서 출력 상태이면 true 를, 그렇지 않으면 false 를 반환한다. el.visible() 이란 el 이라는 element 가 현재 출력 상태인지 확인하는 것으로

if ( Element.visible('comment_box_10') == true )

라고 한 것과 동일하다. Element 객체에 있는 visible 메소드를 실행한 것인데, 위에서 설명했다시피 $ 함수로 html element 를 선택하면 Element 객체도 상속 받기 때문에 el.visible 이라고 쓸 수 있는 것이다. 즉,

  • el.visible()
  • $('comment_box_10').visible()
  • Element.visible('comment_box_10')

모두 동일한 작동을 한다. (여담인데, 난 visible 이라는 이름을 참 싫어한다. is_visible 이라는 이름이 visible 이라는 이름 보다 좀 더 직관성이 높기 때문이다)

우리는 list.html 에서 댓글을 가져와서 출력할 상자( <div id="comment_box_" style="display: none;"></div> )를 출력하지 않았다. style="display: none;" 이라고 했기 때문이다. 그러므로 위 if 조건문은 false 가 된다. 그래서 else 블럭 안에 있는 내용을 실행한다. 바로

else {
	var ajax = new Ajax.Updater(el, url);
	el.show();
}

이 부분 말이다. 우선 show 메소드부터 보자면, show 메소드는 해당 element 를 출력 상태로 바꿔준다. comment_box_10 이런 이름(id)을 가진 element(<div id='comment_box_10' ...>)이 출력하지 않은 상태(display: none)이므로 이를 show 메소드로 상태를 바꿔주면 출력 상태가 된다. 반대 기능을 하는 메소드는 위 javascript 바로 위에 있고 이름에서도 추측할 수 있듯이 hide 메소드이다.

이번엔

var ajax = new Ajax.Updater(el, url);

이 부분을 보자. Ajax 는 prototype.js 에 있는 ajax를 처리하는 객체이다. 근데 앞에 new 는 뭘까? new 연산자(operator)는 Function 자료형으로 객체의 인스턴스(instance)를 생성할 때 쓴다. 자세한 설명은 MDC에 있는 new Operator 문서를 참조 바란다.

위 코드에서 new 를 하지 않고 Ajax.Updater 를 바로 실행하려고 하면 Ajax.Updater 가 아닌 Ajax.Updater 에 담겨있는 내용 자체를 실행하려 한다. 그래서 new 를 통해 Ajax.Updater 인스턴스를 생성하여 실행하는 것이다. 자세한 내용을 많이 생략해서 설명이 막연한데 prototype.js 에 보면 prototype.js의 Class 라는 객체에 있는 create 메소드로 만든 객체들(Ajax, Hash, Template, PeriodicalExecuter 등)은 new 로 인스턴스를 할당한다고 보면 된다. 이 강좌를 쓰는 본인 능력이 부족해서 쉬운 설명을 하지 못해 결국 자세한 설명을 포기하여 참 미안스럽다.

어쨌든 Ajax.Updater 는 new 연산자를 써야 하므로

var ajax = Ajax.Updater(el, url);

이렇게 하면 작동하지 않는다(오류 남). prototype.js 에서 어떤 건 new 를 쓰고 어떤 건 안써도 되는지 모르겠다면 prototype.js 에 있는 공식 문서를 참고하면 된다. 물론, javascript 의 new 연산자에 대한 이해도를 먼저 높이는 것이 중요하다.

Ajax.Updater 객체는 지정한 html element 에 지정한 url 로 접근하여 받은 내용을 반영하는 역할을 한다. el 은 comment_box_10 이름을 가진 html element 이고, url 은 list.html 에서 댓글(숫자)를 누를 때 넘겨 받은 주소인 /blog/get_comments/10 이다. 이 주소를 ajax 방식으로 접근하여 받은 내용은 header.html 와 footer.html 를 포함하지 않은 채 댓글 목록을 html 내용으로 갖는 comments.html 이고, 이 내용을 comment_box_10 이름을 가진 html element 안에 반영한다. 추가가 아니라 반영이다. 기존에 이미 문자열이 있으면 이 내용은 무시하고 새로 받은 내용으로 덮어씌운다.

이 기능은 Ajax.Request 라는 객체로도 구현할 수 있다.

var ajax = new Ajax.Request(url, {
			onSuccess: function(req) {
				el.update(req.responseText);
			}
	});

이렇게 말이다. 작동은 동일하지만 훨씬 복잡하다. 그래서 단지 서버로부터 ajax 로 html 내용을 받아다 특정 html element 에 반영만 할 것이라면 Ajax.Updater 객체를 권한다. 훨씬 간결하니 말이다.

이제 서버로부터 댓글 목록을 가져와서 댓글 목록 상자에 반영하고 출력 상태로(el.show()) 바꿨다. 이 상태에서 “댓글 (숫자)”를 클릭하면 당연히(?) toggle_comment_box 함수가 실행된다. 그런데 현재 이 element 는(el) 출력 상태이므로

if ( el.visible() == true ) {

이 조건문이 참이 되므로

el.hide();

만 실행하고 끝난다. 댓글 상자를 닫는데 굳이 서버에 접속해서 댓글 목록을 가져올 필요가 없기 때문이다.

이제 글 목록에서 댓글을 ajax 방식으로 가져오는 기능까지 만들었다. 물론 꽤 효율이 떨어지는 방식이다. 이용자가 “댓글 (숫자)” 부분을 계속 눌러대면 1/2 만큼 서버로에 접속해서 댓글 목록을 가져오기 때문이다. (댓글 상자 열 때만 접속하므로 1/2) 좀 더 효율성 있는 방법은 여러분이 스스로 고민해서 만들어 보길 바란다. :) 이는 기획 관점도 필요하므로 어떻게 하는 것이 나을지 고민을 하면 개발 감각이나 기획 감각 늘리는 데 많은 도움이 된다.

글 읽기 화면에서 ajax 방식으로 댓글 달기

read.html 보완

드디어 이 글에서 만들 마지막 기능이다. read.html 파일을 열어서 {% include 'layout/header.html' %}{% include 'layout/footer.html' %} 를 추가하자. 방법은 list.html 에 한 것과 같다. 물론, 위에서 이미 말했듯이 read.html 안에서 {% include 'comment_form.html' %} 은 빼야 한다. 이것 뿐만 아니라 html 내용도 list.html 을 참조해서 read.html 도 바꾸자. 난 다음과 같이 read.html 를 만들었다.

{% include 'layout/header.html' %}

<h1></h1>

<div id="content">

	<div class="post_entry">
		<h3 id="post_" class="post_title"><a href="/blog/entry/"></a></h3>

		<p class="post_info">글 갈래 : [  ] / <a href="/blog/entry/"></a></p>

		<div class="content_box"></div>

		<ul class="post_meta">
			<li>꼬리표 : {% for tag in current_entry.Tags.all %}
			<span></span>
			{% endfor %}</li>
			<li>댓글 ()</li>
		</ul>

		<div id="comment_box_" style="display: block;">
		{% include 'comments.html' %}
		</div>

	</div>


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

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

</div>

{% include 'layout/footer.html' %}

list.html 과 조금씩 다른데, 우리가 신경 쓸 부분은 댓글 목록 상자 html 태그이다.

<div id="comment_box_" style="display: block;">
{% include 'comments.html' %}
</div>

list.html 에서는 display: none 으로 했다면 이젠 block 으로 해서 출력 상태로 바꾸었고, 그 내용도 ajax 방식이 아닌 comments.html 을 가져와서 html 내용으로 붙박아 넣었다.

comment_form.html 에 ajax 기능 추가

comment_form.html 에 보면 form html 태그로 댓글 작성 폼을 짰다. 그 중 form 태그 부분에도 onclick 과 같은 이벤트를 설정해야 한다.

<form method="post" action="/blog/add/comment/" onsubmit="add_comment(this); return false;">

onsubmit 라는 이벤트가 추가 됐는데 이는 폼 영역에서 submit 이 일어날 경우 실행할 javascript 내용을 썼다. 물론 javascript 내용만 실행하고 html form 실행은 하지 않을 것이므로 return false; 도 써놨다. add_comment 함수는 form html 요소(element) 자체를(this) 인자값으로 받는다. 댓글 상자 펼칠 때는 this.href 라 하여 a 태그 자기 자신(this) 중 href 프로퍼티를 인자로 넘겼다면, 이번엔 form 태그 자기 자신(this)을 통채로 넘긴 것이다.

add_comment 함수는 이렇게 생겼다.

var add_comment = function(form_el) {
	var form_el = $(form_el);

	var ajax = new Ajax.Request(form_el.action, {
				method: form_el.method,
				parameters: form_el.serialize(),
				onSuccess: function(req) {
					if ( req.responseText.isJSON() == true ) {
						var _result = req.responseText.evalJSON(true);
						$('comment_box_'+_result['entry_id']).update(_result['msg']);
					}
					else {
						alert(req.responseText);
					}
				},
				onFailure: function(req) {
				}
	});
}

좀 더 복잡한데 차근 차근 살펴보자.

var form_el = $(form_el);

이건 form html 요소를 인자로 넘겨 받을 때 prototype.js 의 $ 함수를 이용해 prototype.js 의 Element 객체 등을 상속시켜 편리한 기능들(메소드)을 쓰려는 것이다. 파이어폭스(Firefox)나 사파리(Safari) 같은 웹브라우저에선 prototype.js 가 자동으로 필요한 객체의 메소드들을 form html 태그 등에 덧붙여 확장시켜 주므로 위와 같은 코드가 필요없지만, 인터넷 익스플로러(Internet Explorer)에선 자동으로 확장시켜주지 못해서 위와 같이 별도 코드를 썼다. 즉 인터넷 익스플로러를 위한 코드이다.

다음은 Ajax.Request 를 볼 차례인데 이것의 꼴을 먼저 보자.

Ajax.Request(주소, 옵션);

참 간단하다. 위 코드가 복잡해보이지만 하나 하나 뜯어보면 간단하다.

우선 주소는 form html 태그에 보면 action 이라는 속성으로 줬다.

<form method="post" action="/blog/add/comment/" ...>

이 중 바로 action="/blog/add/comment/" 이 부분이다. 위에서 this.href 를 썼듯이 this.action 하면 /blog/add/comment/ 이 내용을 갖고 있는데, 우리는 this 를 인자로 넘긴 뒤 form_el 로 받았으므로 form_el.action 으로 써서 댓글을 입력할 주소를 넣었다.

var ajax = new Ajax.Request(form_el.action

이렇게 말이다. 아참, Ajax.Request 역시 Ajax.Updater 와 마찬가지로 new 를 이용해야 한다.

다음엔 옵션을 하나 하나 살펴보자. 내용이 긴데 기본 모양새는

var ajax = new Ajax.Request(form_el.action, {});

와 같다. 이 {} 내용이 길어지므로 개행을 한 것이다. 옵션의 첫 번째 내용은 method 이다. 값을 get method 로 보낼 것인지 post method 로 보낼 것인지 정하는 것인데 이것 역시 form action 값을 따오듯이 form 태그에 있는 method 값을 따르면 된다.

{
	method: form_el.method
}

물론 이런 방식이 마음에 들지 않아 직접 방식을 get 이나 post 라 지정하고 싶다면

method: 'post'

이런 식으로 짜면 된다.

이번엔 parameters 이다. parameters는 서버로 보낼 값을 URL이나(get method) http 요청 본문(request body)에(post method) 붙여 보낼 때 쓴다. 우리는 form 태그 안에 있는 모든 내용을 담아 보낼 것인데, 편리하게 form_el.serialize() 로 값을 만들어 담았다.

parameters: form_el.serialize()

serialize 메소드는 prototype.js 에 있는 Form 객체에 있는 메소드로써 form 자료를 문자열로 주욱 풀어쓰는 데 쓴다. 예를 들어, 글쓴이는 “hannal”, 비밀번호는 “1234”, 댓글 본문은 “hello django” 라고 쓴 폼 내용을 위와 같이 serialize 하면

entry_id=1&name=hannal&password=1234&content=hello%20django%20

이런 문자열이 나온다. hello%20 에서 %20은 공백 문자를 뜻한다. 이런 실험은 prototype.js 공식 문서 중 serialize 부분에 있는 실험기로 편하게 확인해볼 수 있다.

serialize 메소드는 prototype.js 의 Form 객체에 있는 메소드이며 prototype.js 의 Element 객체에는 없다. 그러나 $ 함수는 Element 객체 뿐 아니라 Form 객체의 메소드들도 상속시키기 때문에 $ 함수로 html 요소(element)를 가져오면 위와 같이 serialize 함수를 쓸 수 있다.

{
	method: form_el.method,
	parameters: form_el.serialize()
}

이제 ajax 로 값을 서버로 보냈을 때 문제 없이 잘 보내고 응답을 받았을 때, 즉 성공한 상황을 처리할 행동을 지정할 차례이다. 이런 상황은 onSuccess 로 지정하며 위 method, parameters 와는 달리 함수 자료형이어야 한다.

onSuccess: function() { }

그리고 prototype.js 은 자동으로 서버로부터 받은 결과물을 서버 인자로 넘겨주므로, 그 인자를 넘겨 받을 수 있는 인자 이름을 써넣는다.

onSuccess: function(req) { }

난 request 라는 이름을 줄여 쓴 req 를 즐겨 쓰며, 어떤 이는 transport 를 줄여 쓴 tran 을 쓰기도 한다. 편한 이름을 쓰면 된다.

{
	method: form_el.method,
	parameters: form_el.serialize()
	onSuccess: function(req) { }
}

이렇게 하면 댓글 폼 내용을 서버에(/blog/add_comment/) ajax 방식으로 보낸다. 아직 서버는 ajax 방식으로 접근한 상황을 처리하지 않으니 javascript 작업은 여기서 잠깐 멈추고 위에서 한 것처럼 ajax 상황을 추가해 보완하자.

views.py 의 add_comment 함수에 ajax 방식용 응답 추가

views.py 에서 add_comment 함수 끝부분을 보면

new_cmt.save()
entry.Comments += 1
entry.save()

return HttpResponse('댓글 잘 매달았다, 얼쑤.')

이런 부분이 있다. 여기에 ajax 방식을 구분하는 코드를 넣으면 된다. 우선 댓글이 잘 저장된 이후에 구분을 하면 되므로 entry.save() 아래에서 해당 코드를 넣으면 된다.

if is_ajax(request):

우선 ajax 방식으로 접속 요청을 한 것인지 구분을 하고,

return HttpResponse(...)

그에 맞는 결과 내용을 반환한다. 우리가 웹브라우저로 반환할 정보는 두 가지이다. 댓글이 달린 글 번호와 댓글 목록 html 내용이다. 글 번호는 entry_id, 댓글 목록 html 내용은 msg 라는 이름으로 반환하자. 어떻게 해야 이 값을 구분해서 javascript 로 넘겨줄 수 있을까. 파이썬 변수를 javascript 로 그대로 넘겨준다고 해서 javascript 가 받아들일 수는 없는데 말이다.

이런 상황을 해결하는 방법은 다양하다. 예를 들면,

entry_id=10//////msg=내용

이렇게 문자열로 serialize 하여 보낸 뒤, javascript 에서는 ////// 로 문자열을 쪼개서

entry_id=10
msg=내용

으로 나누고, 이를 다시 = 로 쪼개서 entry_id 가 10이고, msg 는 “내용”이라는 문자열로 구분하는 것이다. 마치 get method 방식으로 주소(URL)에 ?who=hannal&msg=hello 이렇게 보내면 & 과 = 으로 문자열을 쪼개듯이 말이다.

이렇게 서버가 클라이언트(웹서버)로 보내는 값을 문자열로 serializing 하고, 클라이언트는 미리 약속한 방법으로 이걸 해제(unserializing)하는 방법 중 javascript 를 위한 방법으로 각광 받고 인기 있는 방식이 json 방식이다.

서버 변수를 json 방식으로 serializing 하면 javascript 가 쓸 수 있는 문자열로 만들어 준다. 즉, entry_id=10//////msg=내용 이런 문자열을 우리가 직접 만드는 대신 json 방식으로 만들면

{"entry_id": 10, "msg":"안녕"}

이렇게 javascript 가 이해할 수 있는 javascript code 로 만들어 준다. 이 javascript code 를 받아다 실행하면(evaluate) 마치 javascript 에서 위 코드를 실행한 것과 같은 효과가 일어난다. 굳이 문자열을 쪼개고 담는 귀찮은 작업을 할 필요가 없으니 참 편하다.

django는 json 으로 값을 만들어주는 기능을 제공한다. simplejson 이라는 파이썬용 외부 모듈을(파이썬에서 기본 제공하는 모듈 아님) 이용한다. django가 모델을 통해 DB에서 가져온 값을 serialize 하려면 django.core 에 있는 serializers 모듈을 이용한다. 그런 뒤 이 객체(모듈)에 있는 serialize 메소드를 통해 xml 이나 json 으로 serialize 할 수 있으며

serializers.serialize('json', comments)

이런 모양새이다. 하지만, 우리는 모델을 통해 DB에서 넘겨 받은 객체를 풀어내는 것이 아니라 우리가 entry_id 와 msg 라는 이름으로 값을 만들어 serialize 해야 하며, 위와 같은 방식으로는 할 수 없다. 위 객체와 메소드는 django 모델용이라고 보면 된다. 그래서 django가 이용하는 simplejson 모듈을 직접 가져와서(import) serialize 해야 한다.

simplejson 은 django.utils (django/utils/)에 있으니

from django.utils import simplejson

라고 읽어오면 되며, 위 코드를 views.py 맨 위에 넣자. 다른 곳에선 simplejson 을 쓰지 않고 오직 댓글 입력 후 결과값 반환할 때만 쓸 것이라면 if is_ajax(request): 안에다 넣어도 된다.

return_data = {
    'entry_id':entry.id,
    'msg':'hello world'
}

우선 파이썬의 dictionary 자료형으로 값을 만들었다. entry.id 는 예전에 add_comment 함수에서 댓글 입력할 때 생성된 객체이다. entry 는 댓글을 입력할 글이고, entry.id 는 그 글의 id 이다. 즉 entry_id 에 글 번호를 넣었고 msg 엔 hello world 라는 문자열을 넣었다.

이걸 simplejson 을 이용해서 json 으로 serializing 하려면

simplejson.dumps(return_data)

이렇게 하면 된다. simplejson 객체에 있는 dumps 메소드로 return_data 를 serializing 한 것이다. 이는

{"msg": "hello world", "entry_id": 1}

이런 식으로 json serializing 된다.

get_comments 보완

자, 그럼 댓글 목록을 html 로 가져와서 msg 에 담아보자. 이건 get_comments 함수로 이미 구현을 했다. 이걸 add_comment 함수 안에서도 쓰면 get_comments 함수와 같은 기능을 하는 코드를 또 작성할 필요가 없다.

그러려면 get_comments 함수를 조금 고쳐야 한다. 왜냐하면 get_comments 는 ajax 방식이나 직접 웹에서 접근했을 때 이를 HttpResponse 으로 결과 화면을 반환하는데, add_comment 함수에서 우리가 필요한 반환값은 HttpResponse 로 가공된 내용이 아니라 템플릿까지만 입힌 내용이기 때문이다. 즉

return HttpResponse(tpl.render(ctx))

여기서 tpl.render(ctx) 이 부분만 필요하다. 그러므로 get_comments 함수를 실행하는 방식에 따라 반환값을 tpl.render(ctx) 를 할 지 HttpResponse 에 담아서 할 지 결정하면 된다. 이를 is_inner 라는 함수 인자로 구분하고, 이 값이 True 이면 다른 함수 안에서 호출된 것이므로 tpl.render(ctx) 만 반환하고, 그렇지 않으면 함수를 직접 실행한 것이므로 HttpResponse 에 담아 반환하자.

def get_comments(request, entry_id=None, is_inner=False):

우선 is_inner 라는 인자를 넣고 따로 값이 없을 경우 기본값으로는 False 를 넣는다. 그런 뒤 맨 마지막 return HttpResponse(...) 부분을

if is_inner == True:
    return tpl.render(ctx)
else:
    return HttpResponse(tpl.render(ctx))

이렇게 바꿔준다. 이제 다시 add_comment 함수 안에서 return_data 변수 부분으로 돌아가자.

거기에서 'msg':'hello world' 부분을

'msg':get_comments(request, entry.id, True)

이렇게 바꾸면 된다. get_comments 함수를 실행하되 is_inner 인자를 True 로 넘긴 것이다. 그러면 get_comments 함수에선 tpl.render(ctx) 만 넘긴다.

댓글 입력 후 결과 내용 반환

다 됐다. 반환할 결과물은 return_data 에 있고 이는 simplejson.dumps 로 json 형태로 만들었다. 이걸 HttpResponse 로 반환하기만 하면 된다.

return HttpResponse(simplejson.dumps(return_data))

add_comment 함수에서 entry.save() 아래는 이런 비슷한 모양일 것이다.

if is_ajax(request):
    return_data = {
        'entry_id':entry.id,
        'msg':get_comments(request, entry.id, True)
    }
    return HttpResponse(simplejson.dumps(return_data))
else:
    return HttpResponse('댓글 잘 매달았다, 얼쑤.')

이제 다시 javascript 부분으로 돌아가자. 댓글 입력 요청이 제대로 이뤄지면 onSuccess 에 연결한 함수가 실행된다. 아직까지는 function(req) { } 이렇게 해서 아무 일도 안했다. 이젠 서버로부터 json 방식으로 결과를 받으니 이를 처리하면 된다.

prototype.js 의 Ajax 객체는 서버로부터 넘겨 받는 정보를 req 로 받았고(여러분이 req가 아닌 tran 이나 req_server 같은 이름으로 함수 인자를 받았다면 그 이름일 것이다), 여기엔 다양한 정보가 들어가 있는데 서버로부터 받은 문자열은 “responseText” 라는 프로퍼티에 들어가 있다. 이외 어떤 프로퍼티가 있는지는 공식 문서에서 Ajax.Response 부분에 잘 나와있다. 아직 우리는 responseText 만 만질 것이다.

서버로부터 넘겨 받은 문자열(req.responseText)이 json 데이터인지 확인해야 하는데, prototype.js은 자동으로 문자열 자료형(String type)에 isJSON 메소드를 매달아놨다. 만약 json 이면 true 를 반환하고 아니면 false 를 반환한다.

코드로 표현하면

if ( req.responseText.isJSON() == true ) {
}
else {
}

라고 할 수 있다. 우리가 views.py 에서 add_comment 함수의 출력물 반환을 할 때 댓글이 제대로 입력되고 그 요청이 ajax 일 때만 json 으로 값을 반환하고, 그 외엔 일반 문자열을 반환한다. 그러므로 서버에서 댓글이 제대로 안달린 경우엔 서버에서 받은 문자열이 일반 문자열이므로 이걸 그대로 출력하면 댓글이 제대로 저장되지 않았다는 뜻이기도 하다. 그래서 req.responseText 가 json 값이 아니면 따로 할 일 없이 서버로부터 문자열을 그대로 출력하면 된다.

if ( req.responseText.isJSON() == true ) {
}
else {
	alert(req.responseText);
}

이번엔 넘겨받은 값이 json 인 경우, 즉 ajax 방식으로 댓글을 제대로 남겼을 경우를 처리하면 된다. 가장 먼저 해야 할 일은 넘겨받은 json 문자열을 javascript 로 실행해서 javascript 객체나 값으로 변환해야 한다. 이는 javascript 에 있는 eval 이라는 함수를 쓰면 되는데, prototype.js 에서 제공하는 evalJSON 메소드를 쓰는 것이 낫다. 이 메소드는 isJSON 문자열과 마찬가지로 prototype.js 이 String 자료형에 자동으로 매달아놓은 메소드인데, 문자열을 javascript 로 실행한다. javascript 에 있는 eval 함수와 다른 점은 문자열이 javascript 로 올바른지 검수한다는(sanitize) 점이다. 즉 eval 함수보다는 한결 안전하다.

넘겨받은 json 문자열을 evalJSON 메소드로 실행할 때 만들어지는 값은 _result 라는 변수에 담자. 그러면

var _result = req.responseText.evalJSON(true);

이렇게 하면 된다. 서버로부터 {"msg": "...", "entry_id": 숫자} 이렇게 받았으므로, 위 코드 이후부터는 _result['entry_id']_result['msg'] 로 다룰 수 있다. 우리가 화면에 출력할 내용은 _result['msg'] 이므로 이걸 html 에서 comment_box_숫자 를 id 로 갖는 html element 에 _result['msg'] 를 반영하면 된다. 이런 일은 prototype.js 의 Element 객체에서 update 메소드가 한다. 이건 이미 ajax방식으로 댓글 목록 가져와 출력할 때 써봤다. 코드로 구현을 해보면

$('comment_box_'+_result['entry_id']).update(_result['msg']);

이렇게 하면 된다. 이제는 댓글을 쓰면 서버로부터 댓글 목록 html을 받아서 댓글 출력할 상자 안에 반영한다.

한 번 댓글 쓰면 다음부터는 댓글이 안달린다

위 코드엔 오류(bug)가 하나 있다. 댓글을 한 번 쓰고 나면 그 다음부터는 “댓글 달 글을 지정해야 한다우.” 라는 경고창이 뜨며 댓글이 달리지 않는다. ajax 방식으로 댓글을 가져온 뒤에 댓글을 달아도 마찬가지이다.

이건 여러분들이 해결해야 할 숙제이다. 물론 이번에도 문제를 해결할 도움말은 있다.

  • 댓글이 달릴 글은 comment_form.html 에서 entry_id 라는 input 에 값으로 넣어서 알아낸다.
  • comment_form.html 은 comments.html 에서 가져와 포함시킨다.
  • ajax 방식으로 댓글을 가져오거나 ajax 방식으로 댓글을 달면, 댓글 목록 html 을 위한 comments.html 파일은 views.py 에서 get_comments 함수에서 가져와 처리한다.
  • get_comments 함수에선 댓글을 속한 글 정보를 가져오지 않는다. read 함수에선 글 정보를 가져와서 current_entry 에 담는데 말이다.

이 정도면 충분하다고 본다. 이 강좌에서 바꾼 소스에는 위 문제를 해결한 코드가 반영되어 있다. 즉 답이 들어가 있다. 위 도움말을 잘 참조하여 문제를 해결해보고, 답과 비교해보자.


이번 글을 통해 여러분은 xhtml 과 css, 그리고 prototype.js 을 간단하게나마 익혔다. 더 깊게 파고들고 능숙해지려면 더 많이 다루고 문서도 봐야 한다. 여기서는 매우 조금만, 그 조금도 아주 부실하게 설명을 했기 때문이다.

또, 위 코드는 효율성이 다소 떨어진다. 다소 경직된 구조라서 확장성이 부족하며, 다소 서버나 클라이언트의 자원을 낭비하는 경향도 있다. 효율 보다는 개념 이해를 위한 설명에 맞추어 그러하니 여러분들이 이리 저리 고민하며 좀 더 최적화 하길 권한다. 이 구조에 익숙해지거나 지향한다면 앞으로 두고 두고 고생할 것이다. ^^


javascript, html, css 작업 편하게 하는 데 아주 좋은 도구, firebug

javascript 작업이나 html 에 css 를 입히는 작업을 하다 보면 답답할 때가 많다. 이 javascript code 가 현재 어떤 값을 갖고 있는지 확인하기도 불편하고, 특정 부분의 style 정보가 어떠한지 확인하기도 까다롭다. 이런 작업을 하는 데 아주 큰 도움을 주는 도구가 바로 firebug 이다.

firebug 는 웹브라우저인 firefox (파이어폭스) 전용 부가기능이다. 이를 설치하면 웹브라우저 오른쪽 아래 끝을 보면 바퀴벌레처럼 생긴 아이콘이 있다. 그걸 누르면 firebug 공간이 나타난다.

위 그림은 글 목록 화면이다. 맨 윗 글의 “댓글 (2)”을 누르면 서버로부터 ajax 방식으로 댓글 목록을 가져온 뒤 글 상자가 열어서 댓글 목록을 나타낸다. 이때 ajax 방식으로 서버에 요청을 보내는 내역을 firebug 에서 볼 수 있다.

저 보낸 내역을 클릭하면 서버로부터 보낸 내용이나 받은 내용을 확인할 수 있다.

이외에도 Inspect (검사)를 누른 뒤 웹브라우저 화면에 마우스로 이곳 저곳을 다니며 클릭해보자. 클릭한 그 부분을 firebug 화면에서 html 코드를 볼 수 있으며, 그 부분에 css 의 style 정보가 어떻게 적용되었는지, 그 html element 가 어떤 코드의 하위에 있는지 볼 수 있다. 정말 끝내준다!

이외에도 우리가 javascript 변수에 어떤 값이 들었는지 확인할 때 종종 alert 함수를 띄우는데, 이것 대신 console.log 함수를 권한다. 간단하다.

var hannal = 'hannal is a good man';
console.log(hannal);

이렇게 화면 firebug 콘솔 창에 hannal 자료형이 무엇인지, 그 안에 어떤 값인지 알 수 있다.