Curriculum/Wanted Free Onboarding Backend

[17~21일차] 5번째 개인과제 SNS

작은코딩 2022. 7. 27. 21:25

마지막 프로젝트는 개인과제로 진행되었다. 

 

간단하게 요구사항을 설명하면

JWT 토큰을 사용하여 로그인, 로그아웃, 인증, 인가하고,

게시글을 작성할 수 있고, (CRRUD)

좋아요를 할 수 있는 SNS을 만드는 프로젝트이다. 

 

요구사항을 확인하고 가장 먼저 API Document를 작성했다. 

가능한 RESTful하게 설계하려고 고민했으며 주로 인스타, 페이스북 같은 sns 서비스를 참고하여 작성했다.

LIke 기능의 경우 좋아요 기능과 취소 기능을 한 API에 설계하기 보단 like, unlike 두 개의 API로 나눠 설계했으며 이는 인스타에서 확인할 수 있었다. 

 

하나의 API로도 충분히 구현은 가능하지만 인스타의 경우 좋아요할 경우와 취소할 경우 로직이 다르고 알림과 같은 추가적인 로직이 필요하기에 구분 지었다고 생각했고 내가 진행하는 프로젝트 역시 확장성을 고려한다면 나눠서 설계하는 게 좋다고 생각했다. 

 

[개인프로젝트 노션 링크]

https://wool-cobalt-585.notion.site/SNS-new_create-com-84e9627ace194465ada3639f8c25d9bb

 

SNS new_create.com

✅ 서비스 개요

wool-cobalt-585.notion.site


🍁 JWT 학습

API를 설계한 다음 진행한 Task는 JWT 토큰에 대한 학습이었다. 

 

팀 프로젝트에서 다른 팀원들이 사용한 경험은 있어서 대략적인 내용은 알고 있었지만 직접 구현하긴 처음이라 학습이 필요했다. 

 

Why JWT 토큰

JWT 토큰을 왜 사용할까?

사용자 인증과 인가에 대해 주로 사용되는 기술은 세션과 토큰이다.

 

간단히 설명하면

세션은 서버 DB에 사용자의 정보를 저장하고 확인하는 방법이고, 

토큰은 사용자의 로컬 스토리지에 정보를 저장하고 확인하는 방법이다.

 

세션의 경우 서버 리소스를 사용하기에 서버 부담이 커지는 단점이 있지만 보안 수준이 높다는 장점이 있으며,

토큰의 경우는 서버 부담은 덜하지만 보안 수준이 비교적 낮다는 단점이 있다. 

 

이러한 토큰의 단점을 보완하기 위해 access 토큰과 refresh 토큰을 별도로 부여하여 access 토큰의 경우 단기간에 만료되게 설정하고 refresh 토큰을 가지고 재발급받는 구조로 만들어 access 토큰이 탈취당하더라도 큰 피해로 이어지지 않는 구조로 되어있으며 refresh 토큰의 경우 사용자 접속 기기, 위치 등을 기반으로 특정 조건을 벗어난다면 이메일, 휴대폰 인증 등을 하도록 로직이 구성되어 보안 수준이 높아졌다. 

 

나는 이번 프로젝트에서 JWT 토큰을 사용해서 로그인 시 access, refresh 토큰을 발행하고 access 만료 시 refesh 토큰으로 access토큰을 재발급받는 부분을 구현했다. 

 

좀 더 커스텀을 해가면서 여러 시도를 해보고 싶었지만 정해진 시간 내에 필요 기능을 구현해야 해서 다음 과정으로 넘어갔다.


🍂 Post 기능 구현

게시글에 대한 CRRUD 기능을 만드는 건 자주 해왔던 부분이라 큰 어려움은 없었다. 

 

C : 게시글 생성

R : 게시글 목록 조회 (정렬, 검색, 필터, 페이지 네이션)

R : 게시글 상세(단일) 조회 (조회수)

U : 게시글 수정 (부분 필드만 수정)

D : 게시글 삭제(소프트 삭제)

 

이번에 새롭게 시도한 건 게시글 목록 조회를 할 때 검색, 정렬, 필터, 페이지 네이션 기능을 한 번에 구현하는 부분이었다. 

 

django ORM을 사용해서 구현하는 데는 큰 어려움이 없었는데 효율 좋은 코드인지는 고민을 계속해봐야겠다. 

# url : /api/posts
class PostView(APIView):
    """
    Assignee : 고희석
    Date : 2022.07.21
    게시글 목록 조회, 생성을 위한 view입니다.
    GET : 게시글 목록 조회
    POST : 게시글 생성
    """

    permission_classes = [permissions.IsAuthenticated]

    def get(self, request):
        """
        게시글 목록 조회
        :param page:        페이지네이션을 위한 파라미터입니다.             default = 1
        :param limit:       페이지네이션 개수를 정하기 위한 파라미터입니다. default = 10
        :param sorting:     정렬 방법을 정하는 파라미터입니다.              default = -created_at
        :param searching:   검색을 위한 파라미터입니다.                     default = ""
        :param hashtags:    필터를 위한 파라미터입니다.                     default = ""
        :return Response:   게시글 목록 data, 상태코드
        """

        # 페이지네이션 설정
        page = int(request.GET.get("page", 1) or 1)
        limit = int(request.GET.get("limit", 10) or 10)
        offset = limit * (page - 1)

        # 정렬 설정
        sorting = request.GET.get("sorting", "-created_at") or "-created_at"

        # 검색 설정
        search = request.GET.get("searching", "") or ""
        search_list = search.split(" ")

        search_posts = PostModel.objects.filter(title__icontains=search_list[0])

        for word in search_list[1:]:
            search_posts = search_posts | PostModel.objects.filter(
                title__icontains=word
            )

        # 필터 설정
        hashtag = request.GET.get("hashtags", "") or ""
        if hashtag == "":
            pass
        else:
            hashtag_list = hashtag.split(",")

            for word in hashtag_list:
                search_posts = search_posts.filter(hashtags_text__icontains=f"#{word},")

        # 조건에 맞는 게시글 가져오기
        posts = search_posts.order_by(sorting).filter(status__status="public")[
            offset : offset + limit
        ]
        return Response(
            PostListSerializer(posts, many=True).data, status=status.HTTP_200_OK
        )

🌹 좋아요 기능 구현

좋아요 기능은 이전에 구현한 경험이 있어 비교적 쉽게 구현했다. 

전에는 좋아요 데이터를 생성 하고 취소할 경우 데이터를 삭제하는 방법으로 로직을 구현했지만 생성과 삭제를 반복하는 로직은 별로 효육적이지 못하다고 생각해서 이번엔 is_like 필드를 만들어 True False로 구분할 수 있도록 구성을 했다. 

 

좋아요와 좋아요 취소 로직이 발생하는 경우 Post 테이블의 like_count가 증감하며 이 로직은 중간에 취소되면 안 되는 부분이기에 트랜젝션 아토믹 데코레이터를 사용해서 한 트랜젝션으로 묶어줬다. 

 

# 좋아요 시리얼라이저
	@transaction.atomic
    def create(self, validated_data):
        """
        :param validated_data["user"]: 사용자 객체
        :param validated_data["post"]: 게시글 객체
        """
        PostModel.objects.filter(id=validated_data["post"].id).update(
            like_count=F("like_count") + 1
        )
        instance = PostLikeModel(**validated_data)
        instance.save()
        return instance

    @transaction.atomic
    def update(self, instance, validated_data):
        """
        :param validated_data["user"]:      사용자 객체
        :param validated_data["post"]:      게시글 객체
        :param validated_data["is_like"]:   좋아요 여부
        모든 필드를 수정하는게 아니라 입력된 필드 + updated_at 필드만 수정
        """
        if instance.is_like == True:
            raise serializers.ValidationError({"error": "이미 좋아요 한 게시글입니다."})

        PostModel.objects.filter(id=validated_data["post"].id).update(
            like_count=F("like_count") + 1
        )

        validated_data.pop("user")
        validated_data.pop("post")

        field_list = ["updated_at"]
        for key, value in validated_data.items():
            setattr(instance, key, value)
            field_list.append(key)
        instance.save(update_fields=field_list)
        return instance
# 좋아요 취소 시리얼라이저
	@transaction.atomic
    def update(self, instance, validated_data):
        """
        :param validated_data["user"]:      사용자 객체
        :param validated_data["post"]:      게시글 객체
        :param validated_data["is_like"]:   좋아요 여부
        모든 필드를 수정하는게 아니라 입력된 필드 + updated_at 필드만 수정
        """
        if instance.is_like == False:
            raise serializers.ValidationError({"error": "이미 좋아요 취소가 된 게시글입니다."})

        PostModel.objects.filter(id=validated_data["post"].id).update(
            like_count=F("like_count") - 1
        )

        validated_data.pop("user")
        validated_data.pop("post")

        field_list = ["updated_at"]
        for key, value in validated_data.items():
            setattr(instance, key, value)
            field_list.append(key)
        instance.save(update_fields=field_list)
        return instance

좋아요 기능의 create, update 판단하는 부분은 view 로직에 구현되어 있다. 

 


🥕 프로젝트 마무리

일단 요구 상항은 전부 구현했긴 한데 전반적인 검토와 리팩토링이 필요하다.

 

좋아요의 경우 로직은 구성되어있지만 실제 사용자가 좋아요를 했는지 여부는 게시글을 가져올 때 전달을 해줘야 되는 로직이 아직 구현되진 않았다.

이전에 prefetched related를 이용해 구현한 내역이 있어 적용하는 것은 크게 어렵지 않을 거라 예상된다. 

 

개인 프로젝트는 꾸준히 발전시켜 나가며 나만의 스니펫으로 활용할 예정이기에 고도화, 업데이트 기능을 고민해봐야겠다. 

 


🍖 프로젝트 자료

[노션]

https://wool-cobalt-585.notion.site/New_Create-SNS-84e9627ace194465ada3639f8c25d9bb

 

New_Create SNS 노션페이지

✅ 서비스 개요

wool-cobalt-585.notion.site

 

[깃허브]

https://github.com/GoHeeSeok00/NewCreate_SNS

 

GitHub - GoHeeSeok00/NewCreate_SNS: 🎨예술가(藝術家), 🎸아티스트(Artist)의 혼을 가진 모든 사람이 자신

🎨예술가(藝術家), 🎸아티스트(Artist)의 혼을 가진 모든 사람이 자신이 new create 한 창작물, 발견 등을 공유하고 소통하는 SNS(Social Networking Service) 서비스입니다. - GitHub - GoHeeSeok00/NewCreate_SNS: 🎨

github.com