개발기록장

[Django] 인스타그램 클론 코딩(6) - 팔로우(follow) 기능 구현 본문

TIL/Django

[Django] 인스타그램 클론 코딩(6) - 팔로우(follow) 기능 구현

yangahh 2021. 2. 10. 03:35

 

[이전 글]

[Django] 인스타그램 클론 코딩(5) - 좋아요(Like) 기능 구현

 

[Django] 인스타그램 클론 코딩(5) - 좋아요(Like) 기능 구현

[이전 글] [Django] 인스타그램 클론 코딩(4) - 게시물과 댓글 C.R.U.D (+ RESTful API에서 Update에는 무슨 메소드를 써야하는가...) [Django] 인스타그램 클론 코딩(4) - 게시물과 댓글 C.R.U.D (+ RESTful API..

devvvyang.tistory.com

[다음 글]

[Django] 인스타그램 클론 코딩(7) - 대댓글 기능 구현

 

 

다른 계정을 follow 하는 기능은 'Like' 기능을 구현한 것과 비슷하게 M:N 관계이다.

다만 다른 점이 있다면,  User 테이블과 User 테이블이 M:N인 관계, 즉 셀프 참조로 M:N인 형태가 되어야 한다.

 

Django에는 셀프 참조하는 경우에 아래 예시처럼 ManyToManyField에 상대 테이블 이름 대신 'self'라고 적어서 셀프 참조를 구현할 수 있다.

예)

class User(models.Model):
    (생략..)
    relations = models.ManyToManyField('self', through=  (생략...) )
    (생략..)

 

하지만 follow 기능 구현에는 이 방법이 더 복잡한 것 같아서 이 방법을 이용하지 않고, 그냥 class를 통해 중간 테이블을 만들어서 ForeignKey로 관계를 설정하였다. 나도 공부하는 입장이라 이게 괜찮은 코드인건지 모르겠다....

 


1. user/models.py

from django.db   import models


class User(models.Model):
    email      = models.EmailField(max_length=50, unique=True) 
    name       = models.CharField(max_length=20, unique=True) 
    phone      = models.CharField(max_length=15, unique=True)
    password   = models.CharField(max_length=300)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'users'


class Follow(models.Model):
    from_user = models.ForeignKey('User', on_delete=models.CASCADE, related_name='to_user')
    to_user   = models.ForeignKey('User', on_delete=models.CASCADE, related_name='from_user')
    
    class Meta:
        db_table = 'follow'
  • 'follow' 기능은 user와 관련이 있으므로 user app에 구현하였다.

  • Follow class는 User와 User 사이의 중간 테이블로 from_user, to_user 두 attribute 모두 User class를 ForeignKey를 갖는다.

  • 이때 related_name 옵션이 반드시 설정되어 있어야 한다.(없으면 makemigration도 안됨)

 

** 셀프 참조 관계에서 related_name이 필수인 이유는 무엇일까?

아래 테스트를 통해 알아보자

In : user = User.objects.get(id=1)

In : user
Out: <User: User object (1)>

In : follow = Follow.objects.filter(from_user=user)

In : follow
Out: <QuerySet [<Follow: Follow object (2)>, <Follow: Follow object (5)>]>

In : follow.user
  • 여기서 셀프참조가 아니라면 이 follow.user_set는 역참조(또는 정참조)로 해당 객체를 가지고 있어야 한다.

  • 하지만 이 경우에는 user_set이라는 속성만으로는 자신을 바라보고 있는 두 User 객체 가운데 어떤 속성에 접근해야 할지 알 수가 없는 상황이 발생한다.

  • 그래서 셀프 참조의 경우에는 반드시 related_name을 설정해야 한다.

 

2. user/views.py

import json

from django.views           import View
from django.http            import JsonResponse

from .models                import User, Follow
from .utils                 import login_decorator

(생략..)


class FollowView(View):
    @login_decorator 
    def post(self, request):
        data           = json.loads(request.body)
        follow_user_id = data.get('follow_user', None)

        # KEY_ERROR check
        if not follow_user_id:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)

        from_user = request.user
        to_user   = User.objects.get(id=follow_user_id)
        follow    = Follow.objects.filter(from_user=from_user, to_user=to_user)
        
        if follow:
            follow[0].delete()
            message = 'Unfollowing'
        else:
            Follow.objects.create(
                from_user = from_user,
                to_user   = to_user, 
            )
            message = 'Following'

        return JsonResponse({'message': message}, status=200)
    
  • follow 기능도 로그인이 필수인 기능이므로 login_decorator를 통해 로그인 체크를 한다.

  • from_user는 follow를 요청한 사람을 의미하는데, 이 view는 나를 기준으로 중간 테이블의 데이터를 추가/삭제하는 것이기 때문에 from_user는 지금 접속한 user. 쉽게 말해 '나'를 의미한다.

  • to_user는 follow를 당한 사람. 이 view에서는 내가 follow를 건 사람을 의미한다. follow_user_id는 request body로 전달된다고 가정하고 코드를 작성하였다.

  • 'Like' 기능과 마찬가지로 이미 follow를 하고 있는 다른 계정에 또 한 번 follow 버튼을 누른다면 unfollow 되어야 한다. unfollow가 된 경우에는 중간 테이블에서 from_user가 '나'이고 to_user가 내가 언팔한 계정인 데이터를 삭제한다.

  • 내가 아직 follow 하고 있지 않은 상태라면 중간 테이블에 from_user, to_user를 매치시켜서 데이터를 insert 한다.

 

+ 추가 구현 - 특정 계정의 following user name, followers user name 가져오기

ForeignKey로 구현한 이 상태에서 related_name을 이용하여 특정 계정의 following user 정보와 follower user 정보를 조회해오는 것을 연습 삼아서 구현해보았다.

user/views.py

(생략..)

class FollowingView(View):
    def get(self, request, user_name):
        # valid user check
        if not User.objects.filter(name=user_name).exists():
            return JsonResponse({"message": "USER_DOES_NOT_EXIST"}, status=404)
        
        user       = User.objects.get(name=user_name)
        followings = Follow.objects.filter(from_user=user) 
        
        if not followings:
            return JsonResponse({'message': 'follow 하고 있는 계정이 없습니다.'}, status=204)
        
        following_list = [following.to_user.name for following in followings]
        return JsonResponse({'data': following_list}, status=200)
        
        
class FollowerView(View):
    def get(self, request, user_name):
        # valid user check
        if not User.objects.filter(name=user_name).exists():
            return JsonResponse({"message": "USER_DOES_NOT_EXIST"}, status=404)
        
        user      = User.objects.get(name=user_name)
        followers = Follow.objects.filter(to_user=user)

        if not followers:
            return JsonResponse({'message': 'follower가 없습니다.'}, status=204)

        follower_list = [follower.from_user.name for follower in followers]
        return JsonResponse({'data': follower_list}, status=200)
        

 

3. user/urls.py

follow 기능과 following 유저, follower를 가져오는 기능의 엔드포인트 추가

from django.urls import path

from .views import (
                (생략..)
                FollowView,
                FollowingView,
                FollowerView,
            )

urlpatterns = [
    (생략..)
    path('/follow', FollowView.as_view()),
    path('/<str:user_name>/following', FollowingView.as_view()),
    path('/<str:user_name>/follower', FollowerView.as_view()),
]

** url 경로를 설정하면서 follow에 관련된 기능이 추가가 된다면 이것과 관련된 기능은 user app에서 분리시켜서 따로 app을 만드는 게 나을 것 같다는 생각이 들었다.