여기서 ajax는 네덜란드의 축구팀 아약스를 말하는 것이 아니라, 프로그래밍 할때 쓰는 애잭스를 말하는 것이다.
인스타그램에서 게시물에 좋아요를 누르거나, 유투브에서 구독 버튼을 누를때를 잘 생각해 보면 누르는 즉시 페이지 업데이트 없이 내가 수정한 내용이 반영된다. 만약 이런 동작을 할 때마다 페이지가 업데이트 된다면 사용자 입장에서는 데이터 소모도 많아질 뿐더러, 동작을 수행하기 매우 귀찮아질 것이다. 이런 불편을 해결하기 위한 방법이 ajax이다. ajax를 사용하면 내가 수행하는 동작에 대한 변화가 새로고침 없이 바로 페이지에 반영이 되고(좋아요, 구독 누르기 등), 이 내역은 페이지를 수동으로 새로고침한 뒤에도 남아있게 된다.
어떻게 하는 것일까? 생각해보면 해당 메카니즘은 간단하다.
우리는 이전 세션들에서 자바스크립트를 이용해서 페이지의 html DOM구조를 바꾸는 방법을 확인했다. 이번에도 이 방식을 사용해서 페이지에 내 동작의 결과를 즉시(페이지 리프레시 없이) 반영하고, 이벤트와 서버를 연결해서 서버에도 해당 동작이 반영되게 함으로써 새로고침한 뒤에 페이지에 해당 정보가 남아있게 할 수 있다.
위 내용을 다시 정리해보면, 다음과 같다.
여기서 중요한 내용은, html에서 이벤트가 발생하면 js에서 그 이벤트를 일차적으로 받고 이벤트의 변수(id)등을 json파일에 담아서 백엔드에 보내주는 역할까지를 수행한다는 점이다. 사실상 프론트와 백을 연결하는 작업을 수행하는 것이다. 그런 뒤에는, 서버에서 받은 request를 바탕으로 DB를 변경한 다음 response를 보내고 그걸 js가 받아서 HTML DOM에 반영하는 순차적인 작업을 진행한다.
이 내용을 간단하게나마 정리하지 못했을 때는 함수와 코드의 이해가 어려웠지만, 기본 흐름을 이해한 뒤에는 더 수월할 것이다.
개인적으로, 과제를 수행하면서 배운 내용이 전체 흐름과 코드를 이해하는데에 큰 도움이 됐다고 생각이 된다. 그래서 피로그래밍 세션의 과제로 나온 위의 인스타그램의 기능을 비슷하게 재현한 게시물 만들기를 통해서 ajax를 설명하려고 한다. 여기서 구현해야 하는 기능은 크게 4가지이다. 게시물 만들기, 좋아요 누르기, 댓글 생성하기, 댓글 삭제하기. 이중 좋아요와 댓글의 기능은 비동기로 처리해야한다.
\models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=20, verbose_name='제목')
like = models.BooleanField(default=False, verbose_name='좋아요')
image = models.ImageField(
upload_to='idea_image/%Y/%m/%d', help_text='이미지 업로드', blank=True)
def __str__(self):
return self.title
class Comment(models.Model):
content = models.CharField(max_length=100, verbose_name='내용')
post = models.ForeignKey(
Post, related_name='comments', on_delete=models.CASCADE)
프로젝트를 진행하는데에 있어서 가장 중요한 부분은 정확한 모델을 구성하는 것이라고 생각한다. 모델을 어떻게 구성하느냐에 따라서 전반적인 기능의 구현이 크게 달라지기 때문이다. 현재 구현해야 하는 주요 기능인 좋아요의 on/off와 댓글의 생성과 삭제를 다시 생각해보자. 좋아요는 post에 귀속된 존재이고, 한 개의 좋아요가 like/unlike의 두 가지 종류로만 존재한다. 반면, 댓글은 post에 귀속되어있기는 하지만 여러 개가 다양한 모습으로 존재한다.
- 따라서, like는 post의 속성중 하나로, BooleanField를 사용해서 나타낸다. 처음부터 좋아요가 눌러져있으면 안되기 때문에 default값은 False로 설정한다.
- 반면, comment는 댓글의 내용이 필요하고, 한 개의 post에 여러 개의 댓글이 생성될 수 있기 때문에 ForeignKey를 사용해서 구현한다. 나는 항상 related_name에서 헷갈리곤 하는데, 나중에 comment를 불러올 때 master.related_name 의 형태로 slave를 불러온다고 생각하면 조금 편하다.
추가로, ajax와는 크게 관련이 없는 여러 설정들을 해준다.
이미지에 관한 관리를 해줘야 하기 때문에 config\urls.py에 아래 코드를 입력해준다.
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
settings.py에도 이미지를 처리하기 위해서 아래 코드를 추가해준다.
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/main_image/%Y/%m/%d/"
선택사항이기는 하지만, superuser를 만들어서 admin페이지에서 작업하기 위해서 admin.py에 model에 관한 정보를 적어주었다.
from django.contrib import admin
from insta.models import Post, Comment
@admin.register(Post)
class CustomPostAdmin(admin.ModelAdmin):
list_display = (
'title',
'like',
)
@admin.register(Comment)
class CustomCommentAdmin(admin.ModelAdmin):
list_display = (
'content',
'post',
)
우리는 form을 이용해서 def기반의 view를 작성할 계획이다. 따라서 forms.py를 다음과 같이 만들어준다.
from django import forms
from .models import Post, Comment
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = '__all__'
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = '__all__'
이제 실질적으로 ajax에 대한 코드를 구현할 차례이다. 위의 흐름도를 다시 살펴보자. 모든 비동기 방식의 구현은 html에서 이벤트를 발생시킴으로부터 시작한다. html의 전체 코드를 layout과 main을 이용해서 다음과 같이 작성했다. layout.html은 거의 css에 대한 내용이기 때문에 크게 볼 것은 없다고 판단해서 생략한다.
\main.html
{% extends "insta/layout.html" %} {% block content %}
<div class="box">
{% for post in posts %}
<div id="post-{{post.id}}" class="postbox">
<div class="imgbox">
<img src="{{post.image.url}}" alt="" width="300px" />
</div>
<div class="contentbox">
<div class="title">
<h4>{{post.title}}</h4>
<hr />
</div>
<div class="combox">
{% for comment in post.comments.all%}
<div id="comment-{{comment.id}}" class="comments">
<p>{{comment.content}}</p>
<button type="submit" onclick="onClickDel({{ post.id}}, {{comment.id}})">
{% csrf_token %}삭제
</button>
</div>
{% endfor %}
</div>
<div class="likebox">
<button type="submit" onclick="onClickLike({{ post.id }}, 'like' )" name="type">
{% csrf_token %} {% if post.like %}
<i class="fas fa-heart"></i>
{%else%}
<i class="far fa-heart"></i>
{%endif%}
</button>
{% if post.like %}
<span class="span">이 게시물을 좋아합니다!</span>
{%else%}
<span class="span">이 게시물에 좋아요를 눌러보세요!</span>
{%endif%}
</div>
<div class="commentbox">
<input type="text" size="40" placeholder="댓글달기..." />
<button type="submit" onclick="onClickCom({{post.id}})">{%csrf_token%} 게시</button>
</div>
</div>
</div>
{% endfor %}
</div>
main의 html 구성은 다음과 같이 만들었다. 별도의 detail페이지가 없기 때문에 main페이지의 기본을 post들의 list로 구성했다.
이중에서 이벤트의 발생에 관해 우리가 봐야하는 코드는 아래와 같다.
<button type="submit" onclick="onClickLike({{ post.id }} )" name="type">
24번 째 줄에 나오는 코드인데, 이 코드의 의미는 다음과 같다.
1. 버튼을 정의하고, 버튼의 type은 submit으로 하겠다.
2. 버튼이 클릭되면(이벤트의 발생) onClickLike라는 함수를 발생시키고, post.id 를 변수로 지정해 이 내용을 js로 보낸다.
여기까지가 흐름도에서 나온 1번의 내용이다.
다음은 2번 내용인 js에서 이벤트를 받고, 백엔드로 json을 보내는 방법에 대한 코드이다.
const request = new XMLHttpRequest();
가장 위에서 request라는 변수를 new XMLHttpRequest로 선언해준 이유는 서버와 통신하기 위해 XMLHttpRequest 객체를 생성하고 사용하는 방법을 의미한다. 사실상 이 한 줄의 코드가 ajax의 시작이라고 볼 수 있다.
const onClickLike = (id) => {
const url = "ajax/";
request.open("POST", url, true);
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.send(JSON.stringify({ id }));
};
이어지는 이 코드는 아까 html에서 button이 클릭되면 생성되는 함수인 onClickLike를 정의하는 방식이다.
- 이 함수는 id라는 변수를 갖는데, 이 id라는 변수가 post.id에 대응된다. 만약 2개 이상의 변수가 필요한 함수의 경우에는 html의 이벤트를 발생시킬때 선언했던 변수가 그 순서대로 js에서 정의해주는 변수에 대응된다. 예를 들어서,
onclick = "onClickFunc({{post.id}}, {{comment.id}})" 라고 html에서 정의한 뒤에
js에서 함수를 받을 때 onClickFunc = (post_id, comment_id) => { ... }라고 받았다면, 순서대로 post_id가 post.id에 대응되고, comment_id가 comment.id에 대응되는 것이다.
- 다시 코드로 돌아가면, url을 ajax/ 로 지정하고, request.open('POST', url, true)를 통해서 request라는 요청을 POST의 방식으로 url에 보낼 준비를 한다. form이랑 비슷한 역할을 한다고 볼 수 있다. method로 post인지 get인지를 확인하고 action으로 이동하려고 하는url주소를 명시해주면 된다.
- request.setRequestHeader는 request의 요청에 대한 타입과, 변환 방식을 지정해주는데 사실 여기 코드는 손댈 필요가 없다. 어떤 작업을 하든지 그냥 넣어주면 된다.
- 그런 뒤에, json파일을 서버로 보내주는데, 이 과정이 request.send(JSON.stringify({ id })이다. stringify는 id를 string의 형식으로 변환한다는 것인데, json파일은 전체가 다 텍스트형식이기 때문에 stringify작업이 필요하다. 참고로 json은 자바스크립트 객체처럼 생겼지만 여러 프로그램 언어에서 읽고 쓸 수 잇는 데이터 형식이다.
여기까지의 작업으로 2번 내용의 과정이 끝났다.
3번과 4번 내용은 백엔드에서 json을 해석하고, 서버에서 이벤트를 listen한 뒤에 서버에서 db를 변경하는 과정이다.
\app\urls.py
urlpatterns = [
path('ajax/', views.press_like, name='ajax'),
]
- 우선 onClickLike라는 함수가 정의된 다음에 ajax/라는 url으로 요청을 보낸다고 했으니, ajax/라는 url을 정의해줘야 한다.
- urls.py에서 ajax/라는 url요청이 들어오면 views에서 press_like 함수를 실행한다.
\views.py
@csrf_exempt
def press_like(request):
if request.method == 'GET':
post_list = Post.objects.all()
ctx = {"posts": post_list}
return render(request, 'insta/main.html', ctx)
elif request.method == 'POST':
req = json.loads(request.body)
# load는 파이썬이 이해할 수 있는 형식으로 저장함
post_id = req['id']
post = Post.objects.get(id=post_id)
if post.like == True:
post.like = False
elif post.like == False:
post.like = True
post.save()
return JsonResponse({'id': post_id, 'type': post.like})
- 여기에서 press_like함수를 정의해준다. @csrf_exempt를 통해서 post 요청에서 필요한 csrf방식을 정리해준다. 그런 뒤에, get방식으로 요청이 들어오면 별다른 행동을 할 필요가 없기 때문에 main페이지를 보여주고, post방식으로 요청이 들어오면 실질적인 행동을 정의해준다.
- req라는 새로운 변수를 json파일으로 들어온 request의 body부분으로 정의한다.
- req중에서 'id'라고 된 변수를 views의 함수에서 사용할 수 있도록 post_id로 새롭게 받아준다.
- 그 다음에는 post를 Post중에서 id가 post_id와 일치하는 객체로 정의해서 '좋아요'가 눌린 게시물이 무엇인지를 확인한다. 만약 좋아요가 눌린 게시물이 이미 좋아요가 눌린 상태였다면(True) 눌리지 않은 상태(False)로 바꿔주고, False였다면 True로 바꿔준다.
- 마지막으로는 post에 대한 내용을 저장하고 다시 Json파일을 response(응답)해준다. 이때 'id'라는 변수로 post_id를, 'type'이라는 변수로 post.like (True인지 False인지)를 넣어서 같이 보내준다.
여기까지의 작업으로 3번 내용(백엔드에서 json해석, 이벤트 listen)과 4번 내용(서버에서 db변경)을 모두 해줬다.
마지막으로 5번 내용인 js에서 html DOM을 조작하는 방법이다.
const handleResponse = () => {
if (request.status <= 400) {
const { id, type } = JSON.parse(request.response);
// response = {id:number, type: "like" | "dislike"}
const element = document.querySelector(`#post-${id} .contentbox`);
const i = element.querySelector(".likebox button i");
i.classList.toggle("fas");
i.classList.toggle("far");
const span = element.querySelector(".likebox .span");
if (type === true) {
span.innerText = "이 게시물을 좋아합니다!";
} else if (type === false) {
span.innerText = "이 게시물에 좋아요를 눌러보세요!";
}
}
};
request.onreadystatechange = () => {
if (request.readyState === XMLHttpRequest.DONE) {
handleResponse();
}
};
- 우선 handleResponse라는 함수를 새롭게 정의한다. request.status로 request의 현재 상태가 어떤지(에러가 나지는 않았는지)를 확인한다. 만약 리턴값이 400보다 크다면, 심각한 에러를 동반하고 있을 가능성이 높기 때문에 아래 작업을 진행하지 않는다.
- 만약 별일이 없다면, 이 함수에서 사용할 id와 type이라는 변수를 다시 정의한다. 3번에서 json파일에 id와 type을 변수로 넣어서 보내준 response로 보내준 것을 다시 기억해보자. 이 변수들은 js에서도 다시 필요하기 때문에 id와 type을 다시 정의해주는 것이다. request의 response로 온 JSON파일을 parse해서 변수들을 id와 type이라고 정의한다. 순서가 중요함에 다시 유의하자.
- 그 다음은 querySelector로 like의 이모티콘이 있는 위치까지 접속을 해준다.
- 이모티콘을 바꾸는 과정은 fontawesome을 참고했는데, 내가 사용하는 하트 이모티콘이 빈 이모티콘은 fas로 시작하고 색 이모티콘은 far으로 시작하기 때문에 toggle이라는 함수를 사용했다. 만약 이모티콘이 있는 i 태그에 fas가 있으면 없애고, 없다면 만들라는 요청을 보내는 함수가 toggle함수이다. 따라서 fas와 far에 대한 toggle함수를 연속으로 사용하면 빈 이모티콘과 색 이모티콘이 번갈아 나타날 것이다.
- 마지막으로는 좋아요 버튼을 눌러보라는 요청글을 상황에 맞게 바꿔야 한다. 만약 json파일에서부터 받은 type이 true라면 좋아요가 눌려있기 때문에 '이 게시물을 좋아합니다!'라는 글을, false라면 좋아요를 누르도록 유도해야하기 때문에 '이 게시물에 좋아요를 눌러보세요!'라는 글을 적어준다.
- onreadystatechange 함수는 request에서 변화가 생길 때 마다 호출되는 함수이다. 다시 말하자면, 버튼을 누르는 작업은 request의 변화를 만들기 때문에 이 함수 아래의 내용이 호출될 것이다.
- 그리고 request의 readystate가 XMLHttpRequest에서 DONE 값과 같다면 handleResponse를 실행시킨다. 여기서 DONE이 무엇을 의미하는지는 아래 내용을 참고하면 이해가 쉬울 것이다.
∀ 다시 정리를 해 보자면, ajax를 이용한 비동기 방식이란 json데이터 형식을 이용해 이벤트가 발생했을때 데이터를 비동기(페이지 새로고침 없이)로 보낸 후, api 서버가 보내준 응답값을 가지고 html dom을 페이지 이동 없이 수정할 수 있는 방식이다.
∀ 추가로, 만약 ajax를 검색했을때 JQuery에 관한 내용이 나온다면 지금은 스킵하고 배우자. 공부를 하는데에 있어서는 큰 도움이 되지 않는 방식이라고 한다.
∀ api란, 정확히 어떻게 함수가 작동하는지는 모르지만 우리한테 필요한 기능을 제공해주는 것으로 이해할 수 있다.
예를 들어서 (1+2)를 실행하면 어떤 방식으로 1+2를 구현하는지는 모르지만, 3이라는 결과가 나온다는 것은 안다는 것을 들 수 있다.
∀ 여기 사이트에 들어가면 유용한 api들을 많이 불러올 수 있다. 내가 필요로 하는 지도 기능을 불러올수도 있을 것 같다.
∀ ajax에는 내가 진행한 것 처럼 vanilla javascript를 사용하는 것 말고도 fetch 방법과 axios 방법등 여러 다양한 방법들이 있다.
∀ asynchronous는 '비동기적인' 이라는 뜻이다.
∀ status는 커뮤니케이션의 결과를 알려준다. ==200 이면 성공을 의미한다.
처음에는 이해가 굉장히 어려웠는데, 차근차근 이해하면서 하나씩 따라하다보니 어느 정도는 감을 잡은 것 같다. 내 공부를 본인들의 일처럼 도와준 피로그래밍 스터디 친구들 민과 양파에게 큰 감사를 전한다.
위에서는 좋아요를 누르는 방법에 대해서만 자세하게 설명했는데, 댓글을 달고 삭제하는 방법에 대해서는 전체 코드를 올려놓은 깃헙 주소를 올릴테니, 참고하면 좋을 것 같다.
피로그래밍14기: day11 <JAVASCRIPT 응용, 이벤트 구현> (0) | 2021.01.23 |
---|---|
피로그래밍 14기: day5 오전 <JS - DOM, 끝말잇기, 숫자야구> (0) | 2021.01.13 |
피로그래밍 14기: day4 오후 <JAVASCRIPT 기본 문법> (0) | 2021.01.12 |
댓글 영역