차담화 프로젝트 회고
1. 프로젝트 소개
- 술담화 사이트를 모티브로 한, 차를 판매하는 차담화란 사이트를 구현하는 것이 목표
- 2주란 시간적 제약으로 인하여, 많은 기능을 목표로 하지 않았고, 해당 기능 구현에 집중
- 로그인/회원가입
- 테마별 정렬이 들어간 메인페이지
- 필터링과 정렬 기능이 들어간 전체 상품 리스트 페이지
- 상품 상세 페이지
- 장바구니 페이지
- 애자일한 프로젝트 진행을 위해 스크럼 방식으로 프로젝트를 진행(하려 했습니다만...)
- 2주간의 프로젝트 과정을 1,2차 스프린트로 나눠서 진행
- 각 스프린트의 시작마다 플래닝 미팅 진행
- 데일리 스탠드업 미팅을 통해 각자 진행과정과 블록커등을 공유
- 진행과정을 시각화하기 위하여 칸반 툴을 활용
2. 사용된 기술
공용 툴
- Trello: 칸반 생산성 툴인 트렐로를 활용했습니다. Backlog, To Do, In practice, Reviewing, Done으로 카드의 카테고리를 분류하여 각 카드에 해야 할 업무를 기록했습니다.
- GitHub : 프로젝트의 버전 관리와, 개발 과정에서의 협업을 위하여 백엔드와 프론트엔드의 깃헙을 분리하여 운영했습니다. main 브랜치를 건드리지 않고, 각 기능 구현을 별도의 브랜치에서 마친 후, 이를 메인과 merge시키면서 프로젝트를 진행했습니다. 팀원들이 머지 권한을 갖지 않고, 멘토님이 팀원들의 코드를 최종 리뷰한 이후 main에 머지시키는 방식으로 진행했습니다.
- Notion: 회의록을 노션으로 공유했습니다
- Slack
BE
- Mysql: 관계형 데이터베이스 관리시스템인 mysql을 활용했습니다
- Django : 파이썬 웹 프레임워크인 장고를 활용했습니다. MV패턴을 활용했습니다
- 프로젝트를 위한 환경설정은 conda 가상환경을 만들어, 이 가상환경으로 프로젝트를 진행하였습니다
개인 툴
- Notion : 앞서 회의록으로 노션을 활용한 것과 별개로, 개인적으로 1차 프로젝트 전용 노션 페이지를 별도로 구성하여 프로젝트 전반에 대해 기록했습니다.
3. 목표
- 팀 목표 : 기능 구현보다도, 소통과정에 있어 팀원들간 분쟁 없이 화목하게 프로젝트를 마무리하는 것이 목표였습니다. 결론적으로 상호간의 트러블 없이 무사히 프로젝트를 마칠 수 있었기에, 목표를 성공적으로 이룬 듯 합니다.
- 개인 목표 : 맡은 바 기능을 충실히 구현하면서, 구현 과정에 있어서 자기주도적인 해결과 - 타인의 도움을 받는 것 사이의 균형을 유지하는 것을 목표로 했습니다. 블록커를 맞닥드리는 경우, 이전까지는 시간적 인풋이 들더라도 최대한 혼자 골머리를 앓으면서라도 해결하려 했습니다. 하지만 이번 프로젝트의 경우 팀 프로젝트라는 특성상, 진행과정에 있어 호흡을 맞춰가며 진행해야 했기에, 적당한 선에서 고민과 구글링을 마치고 다른 분들에게 도움 또는 키워드 아이디어를 얻으려 의도적으로 노력했습니다. 결론적으로 팀으로써 함께 라인을 맞춰가며 잘 진행하였습니다.
다만 아쉬웠던 점은 프로젝트 전반적으로 걸쳐 급한 마인드를 가지고 진행하다 보니, 주변을 살피지 않고 혼자서 달려가는 경향이 있었는데, 이에 대해서는 아쉬웠던 점에서 자세히 서술하겠습니다.
4. 기능 구현 - 내가 한 부분
제가 담당한 부분은 다음과 같습니다
1. DBuploader 구현
ORM 쿼리문으로 일일히 데이터를 등록하지 않고 , csv파일을 통해 대량으로 데이터를 등록할 수 있도록 DBuploader를 별도로 만들었습니다.
아쉬웠던 점은, 하나의 csv파일을 가지고 db의 모든 테이블에 들어갈 데이터를 모두 넣으려 하다보니, csv파일이 옆으로 매우 길어졌다는 것입니다.
서로 참조관계를 가진 테이블들끼리만 분류를 하여, 서로 관계가 없는 테이블들은 각각 다른
csv파일을 사용하여서 업로드를 진행할 수 있도록 해줄 수도 있었을 텐데, 그렇게 진행하지 않았습니다. 따라서 db에 파일을 업로드함에 있어서 좌우로 스크롤을 계속해야 해서 피로감이 느껴졌고, 테이블들간의 참조관계를 명확하게 확인하며 데이터를 집어넣기 힘들었습니다. 다음프로젝트에서도 dbuploader를 사용하게 될텐데, 여러개의 csv파일을 이용하여 db에 데이터를 넣어서 불편함을 덜면서 진행할 계획입니다.
2. 상품 리스트 페이지 구현
제일 많은 시간과 노력을 쏟았던 View 입니다.
여러번코드를 들어내고 다시 작성하기를 반복했습니다.
앞서 블로그에,필터링 view를 공유했었는데, 여기서 몇번의 변경이 더 있었습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | class ProductView(View): def get(self, request): q = Q() category = request.GET.getlist("category", None) is_caffeinated = request.GET.get("is_caffeinated", None) price_upper = request.GET.get("price_upper", 200000) price_lower = request.GET.get("price_lower", 0) search_query = request.GET.get('search_query', None) if search_query: q.add(Q(name__contains=search_query), Q.AND) if category: categories = category[0].split(',') q.add(Q(category__name__in = categories), Q.AND) if is_caffeinated: q.add(Q(caffeine__gt=0), Q.AND) if is_caffeinated=="True" else q.add(Q(caffeine__exact=0), Q.AND) q.add(Q(price__range = (price_lower, price_upper)),Q.AND) drinks = Drink.objects.filter(q).annotate(average_rating = Avg('review__rating'), review_count=Count('review')) sort_by = request.GET.get('sort_by', None) sort_by_options = { "newest" : "-updated_at", "oldest" : "updated_at", "highest_rating" : "-average_rating", } result = [{ "id" : drink.id, "name" : drink.name, "price" : drink.price, "average_rating" : drink.average_rating if drink.average_rating else 0, "review_count" : drink.review_count, "image" : drink.drinkimage_set.all().first().thumb_img }for drink in drinks.order_by(sort_by_options[sort_by])] return JsonResponse({'result':result}, status = 200) 초 |
최종적인 코드입니다.
절차적으로 if-elif로 코드를 작성한 것을 모두 들어내고, linear한 진행을 방해하는 불필요한 함수 import또한 모두 제거했습니다.
프론트로부터 쿼리파라미터를 제공받아 이에 따라 필터링을 q 객체로 진행해주었으며, 필터링을 마친 쿼리셋에 대하여 정렬을 진행합니다.
정렬에 있어서 drink와 연결된 review 테이블의 ratings 컬럼의 평균을 내서 이에 따라 정렬을 해주는 부분이 매우 큰 블록커로 다가왔습니다. 처음에는 장문의 if-elif문을 작성하여 평균을 직접 작성해주었으나, 이를 제거하고 장고의 annotate기능을 활용하여 테이블에 존재하지 않는 새로운 컬럼을 만든 뒤, 이에 따라 정렬을 해 주었습니다.
q객체, annotate등 장고의 메소드들을 활용하여 코드를 작성하니 훨씬 가독성이 늘어나고 코드 작성이 편리해짐을 느낄 수 있었습니다. 해당 기능들을 사용하면서 공식문서의 위력을 느낄 수 있었습니다. 구글링을 통해 타 블로그 등의 소스를 사용하는 것보다, 공식문서를 집중하여 읽어내고, 이해한 바를 바탕으로 나의 코드에 적용시키는 것이 어렵지만 빠르고 확실하게 가는 방법임을 경험했습니다.
최신순, 오래된순, 평점순의 정렬도 원래는 if문을 사용하여 각각 경우의 수를 분기해 주었으나, 멘토님의 피드백을 받은 이후, 정렬의 경우의 수를 담은 딕셔너리인 sort_by_options를 만들고, 쿼리 파라미터의 키값을 통해 정렬 방법을 전달받아 분기 없이 바로 .order_by 의 인자로 주는 방식으로 변경하였습니다. 이 또한 선형적인 코드 진행에 대해 배울 수 있는 과정이었습니다. 이처럼 많은 것을 배운 코드였습니다
3. 테마(농장)별 정렬이 들어간 메인 페이지
사용한 메소드는 필터링과 대동소이하나, 두개의 컴프리헨션을 중첩해서 사용해서 다소 난항을 겪었던 View입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | class FarmProductView(View): def get(self, request): offset = request.GET.get("offset", 0) limit = request.GET.get("limit", 4) order_method = request.GET.get("order_method", "highest_rating") order_method_options = { "highest_rating" : "-average_rating", "newest" : "-updated_at", "oldest" : "updated_at" } farms = Farm.objects.all() result = { "farm" : [{ "id" : farm.id, "name" : farm.name, "drinks" : [ { "id" : drink.id, "name" : drink.name, "price" : drink.price, "average_rating" : drink.average_rating, "review_count" : drink.review_count, "image" : drink.drinkimage_set.all().first().thumb_img, } for drink in farm.drink_set.all().annotate(average_rating = Avg('review__rating'), review_count=Count('review'))\ .order_by(order_method_options[order_method])[offset:offset+limit]] } for farm in farms] } return JsonResponse({'result':result}, status = 200)처 |
5. 협업 경험
혼자서 장고로 model과 view로 백엔드 로직을 작성하고, postman으로 crud를 확인하는 과정과 다르게, 여러명의 백엔드와, 거기에 프론트엔드 팀원들까지 함께한 협업 경험은 새롭고도 즐거운 경험이었습니다. 협업을 진행하며 이러한 것들을 느꼈습니다.
- 기록의 중요성
- 모델링은 프론트와 협의하면서 신중히 짜자
6. 아쉬운 점
- 회의록을 따로 만들지 말고 트렐로 카드에 직접 기록했으면 어떨까 싶습니다
- 급한 마음에 혼자서 너무 달렸습니다