Backend/Python

[인스타그램 클론] Vanilla JS - AJAX

비비빅B 2020. 6. 8. 00:14
  • 인스타그램 클론 코딩 중 댓글 기능 구현을 위해 AJAX를 써야 했음
  • 요즘 제이쿼리는 지양하는 추세라고 해서 바닐라 JS로 작성해봄

DB모델

class Photo(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE,
                             related_name="photos")
    image = models.ImageField(upload_to="media/")
    filtered_image = ImageSpecField(source='image', processors=[
                                    ResizeToFill(293, 293)], format="JPEG", options={'quality': 60})
    content = models.TextField(max_length=500, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    like = models.IntegerField(default=0)

    class Meta:
        ordering = ['-created_at']
        
    def get_absolute_url(self):
        return resolve_url("photo:detail", self.id)
        
 
 class Comment(models.Model):
    photo = models.ForeignKey(
        Photo, on_delete=models.CASCADE, related_name="comments")
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE,
                             related_name="comments")
    text = models.TextField(max_length=500)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    like = models.IntegerField(default=0)

    def get_absolute_url(self):
        if self.photo:
            return resolve_url("photo:list")
  • photo 앱 안에 Photo 모델과 Comment모델을 만듦
  • Comment 모델은 photo와 user를 외래키로 지정하고 날짜는 자동으로 받음
  • 좋아요 수는 0을 기본 값으로 설정
  • 따라서 Comment 모델은 댓글 내용 text와 photo, user만 입력하면 DB에 저장 가능
  • text는 HTML 댓글 폼에서 받고 photo와 user는 view에서 지정할 것임

URL 설정

app_name = "photo"

urlpatterns = [
    path('comment/create/<int:pk>/',	# HTML에서 넘겨주는 photo.pk를 받기 위해 <int:pk>지정
         views.CommentCreateAjaxView.as_view(), name='comment_create'),]
  • 클래스 뷰 사용
  • photo를 기준으로 댓글이 달리기 때문에 photo.pk를 url로 받아줄 것임

AJAX로 렌더링할 HTML파일

<!-- photo_list.html-->

<h4 class="photo_content">
	<span class="user_nickname">{{ photo.user.profile.nickname }}</span>
    {{ photo.content }}
</h4>
										<!-- with: 단순히 문자열를 바꿈
                                        ex) "/detail/{{pk}}"    ->  "/detail/{{photo.id}}"  -->
<div class="comment_box">
	{% include 'comment/comment_container.html' with comments=photo.comments pk=photo.id%}
</div>
<small class="timezone">{{ photo.timedelta_string }}</small>	<!-- 업로드 날짜 -->
<div class="comment_add">		<!-- 댓글 입력 폼 -->
	<form class="comment_form" action="{% url 'photo:comment_create' photo.pk %}" method="post">
		{% csrf_token %}
		{{ form.as_p }}
		<input type="hidden" name="next" value="{{ request.path }}">
		<input type="submit">
	</form>
</div>
  • AJAX로 렌더링할 박스 하나 만듦 comment_box
  • 그 안에 따로 렌더링할 코드를 적어논 comment_container를 include함
  • include할 때 with를 같이 쓰면 변수처럼 지정 할 수 있음
  • include한 html파일을 가져올 때 "comments"는 "photo.comments"로 "pk"는 "photo.id"로 바꿈
  • photo_list.html은 모델이 Photo이므로 with로 변수를 조정하지 않으면 오류남
<!-- comment_container.html -->

{% if comments.count %}		<!-- comments가 1개 이상이면~ -->
    <a href="/detail/{{pk}}" class="comment_count"><small>댓글 {{ comments.count }}개 모두 보기</small></a>
    <div class="comment">
        <h5 class="comment_text">
        	<span class="user_nickname">{{ comments.last.user.profile.nickname }}</span>
        	{{ comments.last.text }}
        </h5>
        <i class="far fa-heart"></i>
    </div>
{% endif %}
  • 제일 최근에 적은 comment 하나만 보여주고 나머지 댓글을 detail 페이지에서 보여주는 방식
  • 이 html파일은 AJAX로 계속해서 렌더링 될 파일임
  • 디테일 페이지로 가는 앵커요소에 {% url 'photo:photo_list' photo.id %} 를 하면 AJAX로 댓글을 쓰면 오류남
  • 처음 list페이지를 렌더링할 때는 괜찮지만 AJAX로 댓글을 생성할 때마다
  • Comment 모델로 만들기 때문에 photo.id를 찾을 수 없음
  • include with로 처음 Photo 모델에서 페이지를 렌더링할 때와 나중에 Comment 모델로 렌더링 할 때 변수를 잘 조정해서 오류없도록 맞춤

VIEW 수정

class CommentCreateAjaxView(LoginRequiredMixin, CreateView):
    model = Comment
    fields = ['text']
    template_name = "comment/comment_container.html"

    def form_valid(self, form):
        comment = form.save(commit=False)

        comment.user = self.request.user	# 로그인한 유저

        comment.photo = get_object_or_404(
            Photo, pk=self.kwargs.get("pk"))	# url에서 photo.pk 가져와서 일치하는 photo 찾음

        comment.save()		# DB에 저장

        context = {		# 다른 페이지로 넘겨줄 데이터 딕셔너리
            "comments": Comment.objects.select_related("user").filter(photo=comment.photo).order_by("created_at"),
            "pk": Photo.objects.filter(id=comment.photo.id)[0].id
        }   # 템플릿에서 쓰려면 {{}}안에 key 써야함
        # ex) "/detail/{{pk}}" --> "/detail/16"  즉, 중괄호 사라짐

        return render(self.request, "comment/comment_container.html", context)
  • text는 폼으로 받고 user는 request에서 받음
  • photo는 html에서 photo.pk로 넘겨주고 url에서 <int:pk>로 넘겨준 값을 받아서 가져옴
  • context에 commet_container.html로 넘겨줄 데이터를 넣음
  • comments와 pk라는 key로 해당 포토의 id 상수와 댓글쿼리를 넘겨줌

AJAX JavaScript 작성 - XMLHTTPRequest

    <script>
        const comment_inputs = document.querySelectorAll(".comment_input");		// 댓글 폼들 지정

        for (input of comment_inputs) {		// 댓글 폼들에게 이벤트리스너 설정
            input.addEventListener("keypress", (event)=> {
                if (event.keyCode === 13 || event.which === 13) {	// 엔터키가 입력됐을 때
                    event.preventDefault();		// 엔터키 -> submit 막는 함수

                    const text = event.target.value;	// 댓글 폼에 입력한 값
                    event.target.value = "";		// 댓글 input value 비우기

                    if (text === "") return		// 아무것도 입력안하고 엔터키 입력했을 경우 그냥 리턴
                    const url = event.target.closest("form").getAttribute("action");	// event 지점에서 가장 가까운 form의 action을 url로 가져옴 
                    const request = new XMLHttpRequest();   //  Request 객체 생성
                    const data = new FormData();        // Form 객체 생성

                    data.append("text", text);      // Form text에 데이터 삽입

                    request.open("POST", url, true);        // Request를 POST 메소드, url에 async로 설정
                    request.setRequestHeader('X-CSRFToken', cookies['csrftoken']);      // POST에 {{ csrf_token }}
                     request.send(data);           // Send
                    
                    request.onload = () => {
                        if (request.status === 200) {	// send 정상적으로 완료했을 경우
                            const result = request.responseText;	//	comment_container.html 코드
                            event.target.closest(".comment_wrap").querySelector(".comment_box").innerHTML = result;		// 박스 안 HTML코드 교체
                        } else {
                            console.error(request.responseText);
                        }
                    }
                }
            })
        }		///////////// post csrf_token 설정
        function parse_cookies() {
            const cookies = {};
            if (document.cookie && document.cookie !== '') {
                document.cookie.split(';').forEach(function (c) {
                    const m = c.trim().match(/(\w+)=(.*)/);
                    if(m !== undefined) {
                        cookies[m[1]] = decodeURIComponent(m[2]);
                    }
                });
            }
            return cookies;
        }
        const cookies = parse_cookies();

                                   
    </script>
  • 폼으로 데이터를 보낼 경우 header를 지정할 필요가 없음
  • 여기서는 csrf_token header만 지정함
  • 자동으로 Content-type이 multipart-formdata로 됨
  • 괜히 header를 지정하면 send 정상적으로 작동 안함

AJAX JavaScript Fetch API

 const comment_inputs = document.querySelectorAll(".comment_input");

        for (input of comment_inputs) {
            input.addEventListener("keypress", (event)=> {
                if (event.keyCode === 13 || event.which === 13) {
                    event.preventDefault();

                    const text = event.target.value;
                    event.target.value = "";

                    if (text === "") return
                    const url = event.target.closest("form").getAttribute("action");
                    const data = new FormData();        // Form 객체 생성

                    data.append("text", text);      // Form text에 데이터 삽입

                    fetch(url, {
                        method: "POST",
                        body: data,
                        headers: {'X-CSRFToken': cookies['csrftoken']}
                    }).then(res => {
                        if (res.status === 200) {
                            res.text().then(text=> {
                                event.target.closest(".comment_wrap").querySelector(".comment_box").innerHTML = text;
                            }).catch(err => console.log(err))
                        }
                    })
                }
            })
        }
                  
  • fetch api로 좀 더 간단하게 표현
  • fetch는 promise 객체를 반환


https://www.zerocho.com/category/HTML&DOM/post/594bc4e9991b0e0018fff5ed

 

(HTML&DOM) AJAX 요청 - XMLHttpRequest

안녕하세요. 이번 시간에는 AJAX 요청을 보내는 대표적인 방법인 XMLHttpRequest(XHR) 사용 방법에 대해서 알아보겠습니다. 다른 웹사이트나 블로그에서 AJAX 코드를 많이 보셨을텐데요. 제가 자세히 설

www.zerocho.com

https://nearkim.coffee/posts/django-instagram-clone-tutorial-5-4

 

Django를 이용한 Instagram clone 만들기 Part.5 (4)

AJAX를 이용한 기본적인 댓글 시스템 구현

nearkim.coffee

https://new93helloworld.tistory.com/310

 

[Django] vanilla JS로 장고 CSRF Ajax 요청

 vanilla JS로 장고 CSRF Ajax 요청 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function parse_cookies() {     var cookies = {};     if (document.cookie && document.co..

new93helloworld.tistory.com