DRF

DRF 개발 일기 - 1

좋아요 만들기

by HOON


Last updated on March 31, 2024, 5:42 p.m.


random_image

블로그 좋아요 기능 추가

오늘은, drf 공부 이후, 각 블로그에 좋아요 기능을 추가하는 과정을 작성해보겠습니다.

우선 drf를 공부하면서 계속 강조된 내용은 파이썬과 복잡한 데이터간에 이해할 수 있는 언어로 번역하기위해 json format을 사용한다 입니다.
따라서 해당 내용을 참고하여 코드를 작성해보겠습니다.

우선 views.py부터 작성해보겠습니다.


views.py
@api_view(['POST'])
def like_post(request,pk):
    if request.user.is_authenticated:
        post = get_object_or_404(Post, id=request.data.get('id'))
        if request.user in post.likes.all():
            post.likes.remove(request.user)
        else:
            post.likes.add(request.user)
        serializer = BlogPostSerializer(post)
        return Response(serializer.data)
    else:
        return Response({'detail': 'Authentication credentials were not provided.'}, status=401)

저는 블로그의 like기능을 추가 할거라, CBV 방식보다, FBV방식으로 작성해봤습니다. (이후에는 FBV->CBV로 변경할 예정입니다.)
코드 중 설명드릴부분은 아무래도 @api_view 데코레이터와, serializer 부분 일 것 같은데요
우선 @api_view 데코레이터는 간단히 말하자면, POST요청이 발생 시 해당 함수를 실행할 수 있는 기능입니다.
작성된 코드로 보면, 해당 뷰에 연결된 url이 작동 시 like_post를 실행하도록 유도하는 설정입니다.

또한 serializer는 drf에서는 빼놓을 수 없는 필수 기능입니다. 파이썬 형태와 복잡한 데이터 Json, XML 등의 형태로 변환 할 수 있는 역할을 합니다.


serializers.py
from rest_framework import serializers
from .models import Post

class BlogPostSerializer(serializers.ModelSerializer):
    likes = serializers.SerializerMethodField() #커스텀 메서드를 사용하여 필드값생성 메서드는 get_<field_name>형태여야함. 아래 참고
    class Meta:
        model = Post
        fields = '__all__'

    def get_likes(self,obj):
        return obj.number_of_likes()

drf에서 api를 사용하기위한 serializers.py도 작성해줍니다.
model은 Post모델을 참조하며, fields는 모든 field를 가져옵니다. ["title","author"] 처럼 따로 가져오는것도 가능합니다.

get_likes함수를 통해 실제 "좋아요"기능의 카운트를 표시할 수 있습니다.


detail_view.html
<a id="like-btn" class="btn btn-primary btn-sm float-right" href="#" role="button"><i class="fas fa-thumbs-up"></i>  <span id="like-count">{{ post.likes.count }}</span></a>

우선 버튼을 하나 만들어줍니다. 해당 버튼은 like 버튼이며, 아래에 jsp를 이용하여 버튼 클릭시 action을 실제 count를 증가시키고, 증가된 숫자를 표시 할 수 있는 버튼입니다.

<script>
document.getElementById('like-btn').addEventListener('click', function(event) {
    // 기본 동작을 취소합니다(페이지 이동 방지).
    event.preventDefault();

    fetch('/blog/like/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            // 인증 토큰을 추가합니다.
            'Authorization': '{{ request.user.auth_token.key }}'
        },
        body: JSON.stringify({id: '{{ post.id }}'})
    })
    .then(response =>  return response.json();
    .then(data => {
        // '좋아요'의 수를 업데이트합니다.
        document.getElementById('like-count').textContent = data.likes;
    })
});
</script>

코드는 간단합니다. 위에 정의한 like-btn 버튼이 클릭 될 경우, 아래 fetch 를 실행하게 됩니다.
method는 POST방식이며, header에는 필요한 내용을 추가해줍니다.
body에도 내용이 필요합니다. 현재 저희는 blog/like 경로로 접속 시 해당 api를 실행하도록 코드를 작성중이라. 정확히 어떤 post.id가 필요한지 정의되어있지 않습니다.
따라서 body에 id값을 매핑해주도록 합니다.


urls.py
path('like/', like_post, name='like_post'),

url은 blog/like로 접속 시 like_post 함수를 실행하도록 합니다.

결과적으로 3개의 코드를 종합해보면
blog/like로 api를 전송하게되면 views.py에 작성 된 like_post가 실행되고
해당 함수를 detail_view.html에 like-btn에 연결시켜주었습니다. (파란색 엄지척)


문제 발생

언제나 그렇듯, 한번에 되는건 없습니다...
현재 이 코드는 4개정도의 문제가 있습니다.

  1. 실제로 버튼을 클릭하게되면 반응이 없으며, 숫자는 사라지는 현상
  2. 로그인이 안된 user의 경우에도 버튼을 클릭 할 수 있으며, 1번이 해결 된 상태라면, 카운팅이 되는 상태
  3. Like 상태를 취소 할 수 없습니다.
  4. URI 주소에서 접근 시 어떤 게시물의 Like를 증가시키는지 구분이 명확히 되지 않습니다.

한가지씩 해결 후 한번에 코드를 보여드리겠습니다.

1. 숫자가 사라진다.

먼저 실제로 버튼 클릭 시 어떤 오류가 발생하는지 보겠습니다.
Forbidden: /blog/like/ [31/Mar/2024 15:06:40] "POST /blog/like/ HTTP/1.1" 403 45
해당 오류는 서버는 이해하였으나, 요청된 처리를 할 수 없다는 403에러입니다.
GPT에게 물어보니 세가지 에러 발생 가능성을 제시해주었습니다.

  1. 인증 문제: 로그인하지 않았거나, 인증토큰의 에러
  2. 권한 문제: 로그인 사용자에대한 권한이 없을 수 있습니다.
  3. CSRF 문제: Django에서는 POST 요청에 대하여 CSRF를 포함해야합니다.

1,2번의 가능성을 보자면, 인증은 제대로 전달중이며, 권한은 따로 지정하여 like를 하지 않기때문에 상관없습니다.
문제는 3번일 가능성이 높습니다.
실제로 jsp에서 post 요청을 보내지만, CSRF에 대한 처리가 없습니다.
따라서 코드를 아래와 같이 수정하겠습니다.

var csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
fetch('/blog/{{ post.id }}/like/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            // 인증 토큰을 추가합니다.
            'X-CSRFToken': csrftoken,
    ''' 
2. 사용자인증 상태 검증

현재 코드로는 사용자의 인증상태에 상관없이 버튼이 클릭되면 POST 요청을 보낼것입니다.
따라서 코드는 사용자 인증을 구분하도록 변경하겠습니다.
또한 인증되지 않는 사용자가 버튼을 클릭해도 아무런 반응이 없도록 표현하겠습니다.

document.getElementById('like-btn').addEventListener('click', function(event) {
    // 기본 동작을 취소합니다(페이지 이동 방지).
    event.preventDefault();

    //로그인 여부 확인
    if (!'{{ request.user.is_authenticated }}'){
        return;
    }

   '''

   .then(response => {
        if (!response.ok){
            throw new Error('Network response was not ok');
        }
        return response.json();
    })
    .then(data => {
        // '좋아요'의 수를 업데이트합니다.
        document.getElementById('like-count').textContent = data.likes;
    })
    .catch(error => {
        console.error('There has been a problem with your fetch operation:',error);
    });
3. Like 상태를 취소 할 수 없습니다.

Like를 다시 한번 클릭하면 토글을 사용하여 like 상태를 취소 할 수 있도록 하겠습니다.

@api_view(['POST'])
def like_post(request,pk):
    if request.user.is_authenticated:
        post = get_object_or_404(Post, id=request.data.get('id'))
        if request.user in post.likes.all():
            post.likes.remove(request.user)
        else:
            post.likes.add(request.user)
        serializer = BlogPostSerializer(post)
        return Response(serializer.data)
    else:
        return Response({'detail': 'Authentication credentials were not provided.'}, status=401)

like_post를 구현해뒀기때문에, 해당 함수를 사용하여 한번 더 호출 시 취소될 수 있도록 remove와 add를 나누어 구현하였습니다.

4. URI 주소의 정확한 기능 확인

현재 저는 api를 blog/like로의 접속으로 구현하였습니다.
하지만 이는 블로그의 몇번(pk)의 데이터를 변경하는지 정확하지 않습니다.
따라서 서버 입장에서 endpoint를 정확하게 안내 할 필요가 있다 생각하여, 변경하고자합니다.
모든 like url을 /like 로 변경하겠습니다.
변경이 필요한 코드는 views.py, urls,py, detail_view.html입니다.

최종적으로 변경된 코드를 첨부하겠습니다.

views.py

@api_view(['POST'])
def like_post(request,pk):
    if request.user.is_authenticated:
        # post = get_object_or_404(Post, id=request.data.get('id'))
        post = get_object_or_404(Post, pk=pk)
        if request.user in post.likes.all():
            post.likes.remove(request.user)
        else:
            post.likes.add(request.user)
        serializer = BlogPostSerializer(post)
        return Response(serializer.data)
    else:
        return Response({'detail': 'Authentication credentials were not provided.'}, status=401)

urls.py

    path('<int:pk>/like/', like_post, name='like_post'),

detail_view.html

<script>
document.getElementById('like-btn').addEventListener('click', function(event) {
    // 기본 동작을 취소합니다(페이지 이동 방지).
    event.preventDefault();

    //로그인 여부 확인
    if (!'{{ request.user.is_authenticated }}'){
        return;
    }

    var csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;

    fetch('/blog/{{ post.id }}/like/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            // 인증 토큰을 추가합니다.
            'X-CSRFToken': csrftoken,
            'Authorization': '{{ request.user.auth_token.key }}'
        },
        body: JSON.stringify({id: '{{ post.id }}'})
    })
    .then(response => {
        if (!response.ok){
            throw new Error('Network response was not ok');
        }
        return response.json();
    })
    .then(data => {
        // '좋아요'의 수를 업데이트합니다.
        document.getElementById('like-count').textContent = data.likes;
    })
    .catch(error => {
        console.error('There has been a problem with your fetch operation:',error);
    });
});
</script>

이렇게 코드를 수정하면서, csrf를 추가하여, Django의 POST 방식에 맞추어 작성하였고,
인증된 사용자일경우에만 like버튼이 작동하도록 수정하였으며, Like를 한번 더 클릭 할 경우 db상에서 like를 실행한 username이 제거되어 count되도록 하였습니다.
또한 실제 uri의 endpoint를 를 넣어주면서 명확하게 표기해주었습니다.

아직 DRF가 익숙하지않아, 이것저것 테스트중이며
게시글이 번잡할 수 있습니다.
양해 부탁드립니다.


Leave a Comment:

🤖 AI Chat 🤖