Wecode 2차 프로젝트 후기

짱구석·2020년 12월 13일
3
post-thumbnail

위코드에서 2차 프로젝트가 막이 났다. 이번에도 까먹기 전에 기가막히게 한번 적어보겠다.

마이리틀트립

이번 클론은 myrealtrip 사이트 클론인 MyLittleTrip이다.

myrealtrip은 항공권 및 숙박예약 서비스 사이트이다. 우리는 이중 항공권예약 페이지로 범위를
축소하고 업무를 분배하였다.

저번에는 너무 많은 내용을 소화하려는 탓이였을까 이번에는 좀더 양보다는 질적으로 프로젝트를 진행하고 싶었다. PM으로써 Trello로 정보를 공유하는 장으로 많이 쓰려고 노력했다.

drf : Django를 더 Django스럽게

이번 프로젝트에서 백엔드는 단순 기능 구현을 넘어서 Django를 더 Django스럽게 사용해보고 싶었다.
Django의 장점 중의 하나인 높은 생산성을 경험해보고 싶어서 django restframework를 도입하였다.

팀원들과 나눴던부분 중에 하나가 '이거 drf 왜 써야하는 거죠'였다. 나도 처음에는 '그냥 다들 이걸로 개발하니까..'라고만 생각했었고, https://www.django-rest-framework.org/ 공식문서에 어떻게 써야하는지는 알았지만 왜 써야하야는지 확실히 와닿는 것은 없었다.

다음 블로그를 보면서 '와 이렇게 하면 진짜 빨리 만들겠다'라고 생각하게 되었다.
https://ssungkang.tistory.com/entry/Django-APIView-Mixins-generics-APIView-ViewSet%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

간단히 인용하자면

블로그 포스팅 엔드포인트를 만든다고 했을 때

#views.py
PostListVlew(GET, POST)
PostDetailVlew(GET, PATCH, DELETE)

#urls.py
posts -> PostListVlew
posts/<int:pk> -> PostDetailView

이런 형태로 구성이 될 것이다.
이것이 같이 엮이게될 댓글, 좋아요, 대댓글 등에서도 틀은 크게 바뀌지 않는다. 사실 이제 해당기능들은 어떻게 짜야되는 지 알고 있고 반복적인 작업으로하면 큰 틀은 어려움 없이 작업할 수 있다.

class PostListView(View):
    @login_decorator
    def post(self, request):
        NECESSERY_KEYS = ('content', 'image_url')
        data = json.loads(request.body)

        if list(filter(lambda x: x not in data,NECESSERY_KEYS)):
            return JsonResponse({'message' : 'KEY_ERROR'}, status=400)

        Post.objects.create(content   = data['content'],
                            image_url = data['image_url'],
                            user_id   = request.user_id,
                            )
        return JsonResponse({'message' : 'SUCCESS'}, status=201)

#@login_decorator
    def get(self, request):
        return JsonResponse({'data' : list(Post.objects.values())}, status=200)

위에는 내가 westagram에서 작성했던 예제이다 keyerror와 같이 기본적으로 중복되어져 작성해야하는 부분들이 있다.

serilalizer 는 이부분에 대해서 개발자들의 노가다?를 조금 줄여준다.

# views.py 

from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post
from .serializers import PostSerializer

# 포스팅 목록 및 새 포스팅 작성
class PostListAPIView(APIView):
    def get(self, request):
        serializer = PostSerializer(Post.objects.all(), many=True)
    def post(self, request):
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
          	serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)  
      
from django.shortcuts import get_object_or_404

# 포스팅 내용, 수정, 삭제
class PostDetailAPIView(APIView):
    def get_object(self, pk):
        return get_object_or_404(Post, pk=pk)
      
    def get(self, request, pk, format=None):
        post = self.get_object(pk)
        serializer = PostSerializer(post)
        return Response(serializer.data)
    
    def put(self, request, pk):
      	post = self.get_object(pk)
        serializer = PostSerializer(post, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
      
    def delete(self, request, pk):
        post = self.get_object(pk)
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

내가 참조한 블로그의 serializer를 적용한 예제이다. 우선 serializer에서 model을 가지고 json으로 직렬화를 알아서 만들어준다. 기존에는 우리가 직접 json으로 typing 해주었다면 model의 colunm이름을 직접 json으로 변환하여 반환해준다.

이 serializer는 나중에 nested 구조를 사용할 때 편하게 사용할 수 있다.
이것은 뒤에서 이번 프로젝트 내용과 같이 자세히 다루겠다.

어쨋든 위같은 내용도 기존의 짰던 방식 보다 편해졌지만 계속 반복해서 들어간다.

여기서 mixin과 generic등을 지나 viewset을 이용하게되면 다음과 같이 변경된다.

# views.py

from .models import Post
from .serializers import PostSerializer
from rest_framework import viewsets

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
#. urls.py

from django.urls import path, include
from . import views
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('viewset',views.PostViewSet)

urlpatterns = [
    path('',include(router.urls)),
]

매번 기능들을 구현할 때마다 고민해야했던 에러핸들링, json 작성에 대한 고민이 사라졌다 물론 여기에 쿼리, 필터 등이 들어가면 당연히 조금 더 복잡해지는 것은 사실이지만 우리가 scrum방식을 통해 배운 스케이트보드 -> 자전거 -> 자동차를 만들어가면서 스케이트보드는 참 빨리 만들 수 있겠다는 생각이 들었다.

사실 이번 프로젝트에서는 viewset까지 활용하지 못했다 2주라는 시간동안 당차게 drf를 사용해보겠다고 하였지만 serializer만으로도 버거웠던 것같다.

drf serializer in 마이리틀트립

# serializer.py
from rest_framework import serializers

from products.models import Airline, Airplane, Product, Region, SeatType

class SeatTypeSerializer(serializers.ModelSerializer):
    class Meta:
        model  = SeatType
        fields = ('name','image_url')

class AirlineSerializer(serializers.ModelSerializer):
    class Meta:
        model  = Airline
        fields = ('name','image_url')

class RegionSerializer(serializers.ModelSerializer):
    class Meta:
        model  = Region
        fields = ('name','region_code')

class AirplaneSerializer(serializers.ModelSerializer):
    from_region = RegionSerializer()
    to_region   = RegionSerializer()
    airline     = AirlineSerializer()

    class Meta:
        model  =  Airplane
        fields = '__all__'

class ProductSerializer(serializers.ModelSerializer):
    airplane = AirplaneSerializer()

    class Meta:
        model  = Product
        fields = '__all__'

항공권티켓한장에는 티켓 -> 항공기 -> 지역 이렇듯 model끼리 서로 물려있다.
serialzer에서 서로의 foreign Key field를 modelserializer로 물려 주었다.
Djanog만큼 급한 성격인 나는 얼른 결과를 보고 싶다.

# url:http://52.79.55.158:8000/products?departure_region=김포&arrival_region=제주&departure_date=2020-12-24&offset=0&limit=1
{
  "data": [
    {
      "id": 9808,
      "airplane": {
        "id": 16121,
        "from_region": {
          "name": "김포",
          "region_code": "GMP"
        },
        "to_region": {
          "name": "제주",
          "region_code": "CJU"
        },
        "airline": {
          "name": "현지 항공",
          "image_url": "https://i.ibb.co/YDVZ8t8/air3.png"
        },
        "from_date": "2020-12-24T11:37:33",
        "to_date": "2020-12-24T22:22:33",
        "airplane_numbers": "8072"
      },
      "seat_type": {
        "name": "비지니스석"
      },
      "price": "100.00",
      "remaining_seat": 20,
      "total_seats": 20
    }
  ]
}

Json을 어떻게 작성할지 고민할게 없다 모델의 내용을 그대로 반영해주니 이것만해도 큰 수확이었다고 생각한다.

get을 해봤으니 post까지는 해봐야지 않겠는가?

항공권를 예약하면 항공권의 정보가 예약정보와 같이 보여진다. 우리는 이것을 many to many로 연결하였고 생성과 동시에 기존에 생성된 티켓과 예약정보가 연결되길 바랬다.

내가 원하는 결과를 내기 위해서 고민을 많이 했던것같다.

class ReservationCreateSerializer(serializers.ModelSerializer):
    product = serializers.PrimaryKeyRelatedField(
        many=True, queryset=Product.objects.all())

    class Meta:
        model = Reservation
        fields = ('id', 'head_count', 'product', 'user')

다음과 같이 티켓정보가되는 product를 PrimaryKeyRelatedField로 선언함으로써 답을 얻을 수 있었다.
many=True는 한번에 여러개의 id를 받을 수 있고, Client는 Body에 product : [1,2]의 형태로 넣어서 등록할 수 있다.

drf-yasg

사실 drf는 이것을 하기위한 작업이었다고 해도 과언이 아니다.
하나의 완성된 API를 만들기 위해서는 실제 구동 하는 기능과 , 테스트, 문서 이 세가지가 있어야한다고 느끼고 있었고 귀찮은 일, 반복적인 작업을 지독하게 싫어하는 나에게 코드에서 자동화된 문서는 어떤 framework을 결정하든 반드시 필요하다고 생각한 부분이 었다.

가장 custom하기 쉬우며, 문서에서 test가 가능하고 자동화할 수 있는 방법을 찾다가 drf-yasg를 알게 되었으며 우리 프로젝트에서의 결과는 다음과같다.

어떤 요소가 어떤 형태로 나오는 지 명시가 잘 되어있고 쿼리스트링, request 조건도 추가적이 serialzier를 생성하여 명시해 줄 수 있었다.
이렇게 client와 server간의 소통시 주관적인 의견을 줄일수있고 조금 더 신뢰있게 작업 할 수 있었다고 느꼈다.

느낀점

이번 프로젝트는 내가 chellge하고 싶은 영역이 사실 백엔드가 기본으로 갖춰야할 부분인 성능이나 웹에 대한 이해에서는 조금 멀어져서 팀원들에게는 미안한 감정이 컸다. 또한 PM이라는 위치가 물론 실제로는 더 어렵겠지만 모든 인원의 역량을 생각해서 항상 정리를 해줘야한다는 것이 얼마나 어려운지 느끼게 되었고 아쉬움이 컸다. 아쉬움이 컸던 만큼 다음에는 더 잘하고 싶다는 욕심도 났다. 항상 팀원의 입장에서 팀을 위해 무엇을 해야될까만 생각했다면 PM의 입장에서 팀을 위해 무엇을 해야하는지 생각해볼 수 있게된 경험이었다.

이번에도 너무나 소중한 팀원들과 프로젝트를 진행할 수 있게 되어서 영광이었다고 생각한다.
이제 2차 프로젝트가 끝나고 기업협업을 나간다. 각자 자신의 위치에서 최선을 다해서 언젠가 인정받는 개발자로써 웃으면서 봤으면 좋겠다. 그날을 위해서 나부터 더 열심히 살아야겠다.

3개의 댓글

comment-user-thumbnail
2020년 12월 13일

역시는 역시 규님 기가맥혔다.

1개의 답글
comment-user-thumbnail
2021년 1월 27일

와 규석님 ... 이거 저 drf 공부할 때 쓰겠습니다

답글 달기