마켓컬리 클론 프로젝트

SEUNGCHAN BAEK·2021년 4월 25일
2

TIL

목록 보기
13/15

마켓컬리 클론코딩 프로젝트

프론트엔드 깃헙 주소
https://github.com/wecode-bootcamp-korea/19-1st-Market-ChoKurly-frontend

백엔드 깃헙 주소
https://github.com/wecode-bootcamp-korea/19-1st-Market-ChoKurly-backend

진행기간 : 2021.04.12 ~ 2021.04.23
팀명 : MarketChokurly
팀원 : 프론트엔드 : 이예원,서동이,채준형
백엔드 : 백승찬,안정현,김영훈

기술스택

Framework

  • Django

Language

  • Python

DB

  • Mysql
  • RDS
  • AqueryTool
  • CSV

Communication

  • Github
  • Slack
  • Trello
  • Notion

배포 및 통신

  • EC2
  • Postman
  • httpie
  • JSON

백엔드 팀이 구현한 기능

  1. users
  • 회원가입
  • 아이디 찾기
  • 비밀번호 찾기
  • 로그인
  • 리뷰
  • 좋아요
  1. products
  • 카테고리
  • 상품리스트
  • 상품상세정보
  • 상품검색
  1. orders
  • 주문하기
  • 주문상세정보
  • 장바구니

내가 구현한 기능

  • 아이디 찾기
  • 비밀번호 찾기
  • 리뷰
  • 좋아요
  • 카테고리
  • 상품리스트
  • 상품검색

진행방식

Agile(애자일)의 대표 관리 Practice인 Scrum(스크럼) 방법으로 프로젝트를 진행하였다. Scrum은 특정 개발 언어나 방법론에 의존적이지 않으며, 제품 개발 뿐만 아니라 일반적인 프로젝트 관리에도 사용 가능한 프로세스 프레임워크이다. Scrum은 작은 주기(Sprint)로 개발 및 검토를 하며 효율적인 협업 방법을 제공한다. 정보처리기사 공부를 할때 Waterfall 방식과 애자일 방식을 배웠던 기억이 난다. 이론적으로는 어떤 차이가 있는지는 알고 있었지만 이번에 프로젝트를 하면서 애자일의 방식인 Scrum 방식을 사용함으로써 개발자들이 어떤 환경에서 어떤 방식으로 일을 하는지 느낄 수 있었던 값진 시간이였다. 우리 팀은 매일 아침 11시 10분에 만나서 15분~20분간 회의를 진행하였다. 프로젝트를 시작하고 1주일뒤에 중간점검을 가졌다. 2주차에 더 나은 진행방식을 위해 1주일간 진행된 프로젝트 결과물을 보면서 지난 1주일을 되돌아보는 시간을 가졌다.

중간점검을 통해 발견된 Django의 이해부족

내게는 개발자로서 첫번째 프로젝트였던 만큼 얼마나 빠른속도로 좋은 결과물을 만들어 내는가 보다는 팀원끼리의 코드리뷰가 더 중요하다고 생각했었다. 우리는 모델링을 통해 3개의 앱으로 기능을 나누었다. users, products, orders 앱들로 구성된 프로젝트를 3명의 백엔드 멤버들이 어떻게 나누어서 진행을 할까를 고민하다가 서로 코드를 리뷰 해줄수 있는 방식으로 진행하기 위해 하나의 앱에서 뷰를 나누어 작성하는 방식으로 진행하였다. 각각의 앱을 한개씩 맡는 방법도 생각했지만 서로 한번도 해보지않은 기능에 대해서 팀원이 질문하면 대답을 못해줄것 같았다. 그때는 View는 기능 단위로 짜야한다는 말을 제대로 이해하지 못했던 시간이였다. 1주일이 지나고 중간점검을 통해서 내가 View에 대한 이해가 부족했었다는것을 깨달았다. 프론트엔드 멤버들과 매일 소통하면서 나도 모르게 내 머릿속에 기능 단위가 아닌 페이지 단위로 기능을 구현하려고 했었다. 아이디찾기, 비밀번호 찾기 기능을 구현하는 순간에도 내 머릿속에는 아이디 찾기 페이지, 비밀번호 찾기 페이지를 떠올렸던것 같다.

기능 단위를 이해한 후의 나의 모습

난 기능을 하고 있는 View를 작성하고 있다고 생각 했지만 중간점검을 통해 멘토님에게 자신있게 보여주었던 View의 이름이 MainPageView였다. 그전에 내가 만들었던 View는 아이디 찾기와 비밀번호 찾기 였는데 이 두개의 View에서는 문제가 되지 않았는데 그럴수 있었던 이유가 이 두 개는 페이지가 달랐기 때문이였다. 만약 내가 기능이라는 말을 제대로 이해했다면 아이디 찾기와 비밀번호 찾기를 만든 후에 MainPageView라는 View를 만들지 않았을 것이다. 주말을 이용해서 열심히 만든 내 코드가 어떠한 기능을 위한것이 아닌 메인 페이지를 구현하기 위해서 만들어진 코드였다. 다른 웹사이트에서는 모르겠지만 마켓컬리 사이트의 경우 메인페이지를 위한 코드는 상품을 보여주는 View 하나면 가능했다. 이것을 깨닫고 난 후에 팀원들과 하나의 앱을 나누어서 같이 개발하자고 생각했던것이 효율성이 떨어진다는것을 깨달았다. 처음 만들어보는 app에서 우리가 처음부터 정확하게 기능을 나누는것도 어떻게 보면 어려운 일이였다. 예를 들어, 하나의 View를 만들면서 다른사람이 만들고 있는 View도 구현 할 수 있는 경우가 생겼다. 그래서 중간점검이 있고난 후에는 app별로 나누어서 개발하려고 했다.

내가 발전할 수 있었던 코드

class MainPageView(View):
    def get(self,request):
        products = Product.objects.order_by('?')[:21]
        this_products = []
        for product in products:
            this_products.append(
                {
                    "id"    : product.id,
                    "name"  : product.name,
                    "price" : product.price,
                    "discount_rate" : product.discount_rate.discount_rate,
                    "discounted_price" : product.price - (product.price * product.discount_rate.discount_rate),
                    "thumbnail_image": product.thumbnail_image,
                    "sticker":product.sticker.name
                }
            )
        # this_products

        high_discount_products = Product.objects.order_by('-discount_rate')[:7]
        affordable_products = []
        for high_discount_product in high_discount_products:
            affordable_products.append(
                {
                    "id" : high_discount_product.id,
                    "name" : high_discount_product.name,
                    "price" : high_discount_product.price,
                    "discount_rate" : high_discount_product.discount_rate.discount_rate,
                    "discounted_price" : high_discount_product.price - (high_discount_product.discount_rate.discount_rate * high_discount_product.price),
                    "thumbnail_image" : high_discount_product.thumbnail_image,
                    "sticker": high_discount_product.sticker.name
                }
            )

        # affordable_products


        sub_category = SubCategory.objects.get(pk=2)
        product_sets = sub_category.product_set.all()
        product_sets = product_sets.order_by('?')[:7]
        md_recommends = []
        for product_set in product_sets:
            md_recommends.append(
                {
                    "id" : product_set.id,
                    "name" : product_set.name,
                    "price" : product_set.price,
                    "discount_rate" : product_set.discount_rate.discount_rate,
                    "discounted_price" : product_set.price - (product_set.price * product_set.discount_rate.discount_rate),
                    "thumbnail_image" : product_set.thumbnail_image,
                    "sticker": product_set.sticker.name
                }
            )

        # md_recommends


        products = Product.objects.order_by('created_at')[:id]
        today_new_products = []
        for product in products:
            today_new_products.append(
                {
                    "id" : product.id,
                    "name" : product.name,
                    "price" : product.price,
                    "discount_rate" : product.discount_rate.discount_rate,
                    "discounted_price" : product.price - (product.price * product.discount_rate.discount_rate),
                    "thumbnail_image" : product.thumbnail_image,
                    "sticker": product.sticker.name
                }
            )

        # today_new_products

        products = Product.objects.order_by('-stock')[:7]
        hot_products = []
        for product in products:
            hot_products.append(
                {
                    "id"   : product.id,
                    "name" : product.name,
                    "price" : product.price,
                    "discount_rate" : product.discount_rate.discount_rate,
                    "discounted_price" : product.price - (product.price * product.discount_rate.discount_rate),
                    "thumbnail_image" : product.thumbnail_image,
                    "sticker": product.sticker.name
                }
            )

        # hot_products

        products = Product.objects.order_by('price')[:7]
        lowest_price_products = []
        for product in products:
            lowest_price_products.append(
                {
                    "id"   : product.id,
                    "name" : product.name,
                    "price" : product.price,
                    "discount_rate" : product.discount_rate.discount_rate,
                    "discounted_price" : product.price - (product.price * product.discount_rate.discount_rate),
                    "thumbnail_image" : product.thumbnail_image,
                    "sticker": product.sticker.name
                }
            )

        # lowest_price_products

        return JsonResponse({'this_products':this_products, 'affordable_products':affordable_products,
        'md_recommends':md_recommends, 'today_new_products':today_new_products, 'hot_products':hot_products,
        'lowest_price_products':lowest_price_products}, status=200)

내가 주말에 열심히 만들었던 MainPageView이다. 총 6개의 기능을 하는 View인데 신상품,최저가 등등을 메인페이지에서 보여주기 위해서 만든 코드이다. 이렇게 만들고 나서 메인페이지 뷰가 있을 필요가 없다는 것을 깨달았다. 사실 6개의 기능은 한개의 로직으로 구성된 뷰에서 전부 처리가 가능했다.

class ProductListView(View):
    def get(self,request):
        category_id     = request.GET.get('category_id', None)
        sub_category_id = request.GET.get('sub_category_id', None)
        order_by_type   = request.GET.get('order_by_type', None)
        page            = int(request.GET.get('page', 1))
        limit           = int(request.GET.get('limit', 8))
        start           = (page - 1) * limit
        end             = page * limit

        if not category_id and not sub_category_id:
            products = Product.objects.order_by(order_by_type)[start:end]
        if category_id:
            products = Product.objects.filter(category_id=category_id).order_by(order_by_type)[start:end]
        if sub_category_id:
            products = Product.objects.filter(sub_category_id=sub_category_id).order_by(order_by_type)[start:end]

        results = [{

            "id": product.id,
            "name": product.name,
            "original_price": int(product.price),
            "discount_rate": float(product.discount_rate.discount_rate) if product.discount_rate else None,
            "discounted_price": int(product.price - (product.price * product.discount_rate.discount_rate)) if product.discount_rate else None,
            "thumbnail_image": product.thumbnail_image,
            "sticker": product.sticker.name if product.sticker else None,
            "comment": product.productinformation.comment if category_id or sub_category_id else None

        } for product in products]

        return JsonResponse({'RESULTS':results}, status=200)

이 코드는 쿼리 파라미터를 이용해서 원하는 정렬 방식, 원하는 상품의 갯수, 원하는 페이지 상품 등을 프론트엔드로 부터 받아서 원하는 데이터를 보내주는 코드이다. ProductListView를 통해서 어느 페이지에 있는 상품 리스트든 전부 다 이 View 하나로 처리가 가능해졌다. MainPageView라는 이름으로 View를 만든것이 View는 기능단위라는 것을 제대로 인지하지 못했었다는것을 깨닫게 해주는 순간이였다. 그리고 쿼리,패스 파라미터가 얼마나 개발의 속도를 빠르게 만들어주는지도 깨닫는 순간이였다. 만약 쿼리,패스 파라미터가 없었다면 얼마나 많은 View를 만들어야하고 얼마나 많은 중복된 코드가 존재할지 생각만해도 끔찍하다.

class UserLikeView(View):
    @login_required
    def get(self,request):
        product_id = request.GET.get('product_id')

        if not product_id:
            return JsonResponse({'MESSAGE':'INVALID_PRODUCT_ID'}, status=400)

        user       = request.user
        user_check = UserLike.objects.filter(user=user, product=product_id).exists()
        like_count = UserLike.objects.filter(product=product_id).count()
        product    = Product.objects.get(id=product_id)

        if not user_check:
            user.product.add(product)
            user.save()
            like_count += 1
            return JsonResponse({'RESULTS':like_count}, status=200)

        UserLike.objects.filter(user=user, product=product).delete()
        like_count -= 1
        return JsonResponse({'RESULTS':like_count}, status=200)

마지막에 내가 구현한 좋아요 기능이다. UserLike인데 Product와 User 테이블이 다대다 관계를 형성하면서 만들어진 중간 테이블인데 처음에 내가 구현할때는 좋아요를 누르고 취소하고를 반복할때 데이터를 추가하고 삭제하고를 반복하면 될거라고 생각했다. 하지만 데이터를 추가하고 삭제하는것을 반복하면 그만큼 DB관리에는 효율적이지 못하다는것을 알게 되었다. 또 한번 모델링 수정이 필요한 순간이였다.

class UserLikeView(View):
    @login_required
    def get(self,request):
        product_id = request.GET.get('product_id')

        if not product_id:
            return JsonResponse({'MESSAGE':'INVALID_PRODUCT_ID'}, status=400)

        user = request.user
        like, like_check = UserLike.objects.get_or_create(user=user,product=product_id)

        if not like_check:
            if like.is_like:
                like.is_like = False
            else:
                like.is_like = True

        like.save()
        total = UserLike.objects.filter(product=product_id, is_like=True).count()

        return JsonResponse({'RESULTS': total}, status=200)

데이터를 추가하고 반복하고를 하지않고 좋아요 기능을 구현한 코드이다. 우선 Product와 User의 중간 테이블인 UserLike 테이블에 is_like라는 BooleanField를 추가했다. 좋아요를 누르면 True 좋아요를 취소하면 False로 처리하기 위해서였다. 이렇게되면 데이터를 추가하고 삭제하고를 하지않고 좋아요 기능을 구현할 수 있다. get_or_create 메소드를 처음으로 사용해본 코드였다. 인스턴스가 생성된 적이 없으면 True를 반환하고, 생성된 적이 없으면 False를 반환한다.

백엔드도 보여줄것이 있었던 코드

class FindPasswordView(View):
    def random_choices(self):
        length      = 8
        string_pool = string.ascii_letters + string.digits
        auth_num    = ''

        for i in range(length):
            auth_num += random.choice(string_pool)

        return auth_num

    def post(self, request):
        data     = json.loads(request.body)
        auth_num = self.random_choices()

        try:
            name = data['name']
            identification = data['identification']
            email = data['email']

            if not User.objects.filter(Q(name=name) & Q(identification=identification) & Q(email=email)).exists():
                return JsonResponse({'MESSAGE':'INVALID_USER'}, status=400)

            salt = bcrypt.gensalt()
            hashed_password = bcrypt.hashpw(auth_num.encode('utf-8'), salt)
            hashed_password = hashed_password.decode('utf-8')

            user = User.objects.get(email=email)
            user.password = hashed_password
            user.save()

            email = EmailMessage('[CHOKURLY] 이메일 인증번호', auth_num, to=[email])
            email.send()

            return JsonResponse({'MESSAGE':'SUCCESS'}, status=200)

        except KeyError:
            return JsonResponse({'MESSAGE':'KEY_ERROR'}, status=400)

이 코드는 개인적으로 재밌었던 코드이다. 프론트엔드에 비해서 보여줄것이 없는 백엔드 입장에서는 흥미로웠던 코드였다. 사용자가 비밀번호를 찾을때 사용자 이메일로 직접 인증번호를 보내는 코드인데 진짜 이메일로 내가 원하는 제목과 내용으로 보내진다. 발표할때 많은 사람들이 신기해 했던 기능이다. 몇가지 셋팅이 필요한 코드이다. my.settings.py 에서 필요한 설정을 입력한 후에 gmail 계정의 보안등급을 낮춰야 사용이 가능했다. 2차 프로젝트때는 핸드폰 인증하는법을 구현해 봐야겠다.

파이썬의 간결함

나는 java,c의 문법을 공부한 적이 있다. 파이썬으로 알고리즘 문제를 풀면서 파이썬이 간결한 언어임은 깨닫고 있었지만 이번 프로젝트를 하면서 더 많이 느낀것 같다. 특히 이번 프로젝트에서 list comprehension을 많이 사용하면서 왜 list comprehension을 파이썬의 꽃이라고 하는지 알 것 같았다.


results = [{

            "id": product.id,
            "name": product.name,
            "original_price": int(product.price),
            "discount_rate": float(product.discount_rate.discount_rate) if product.discount_rate else None,
            "discounted_price": int(product.price - (product.price * product.discount_rate.discount_rate)) if product.discount_rate else None,
            "thumbnail_image": product.thumbnail_image,
            "sticker": product.sticker.name if product.sticker else None,
            "comment": product.productinformation.comment if category_id or sub_category_id else None

        } for product in products]

        return JsonResponse({'RESULTS':results}, status=200)
        

상품 리스트를 list comprehension으로 구현한 코드이다. results 리스트에 담아서 프론트에게 전달하는 코드인데 여기서 깨달은 점은 list compreshension으로 인해 중복코드를 줄일 수 있고 중첩 for문을 가독성 있게 표현 할 수 있다는 점이다. 중첩 for문을 쓰는것보다 list comprehension을 사용하면 어떤 데이터가 담기는지 한번에 알아보기 쉬워진다. 리스트 안에 딕셔너리를 담는 형태인데 만약 위의 코드처럼 딕셔너리 값에 조건문을 사용할 수 없다면 아마 중복 코드가 밑에 많이 생겨날 것이다. 파이썬의 간결함을 느꼈다.

모델링의 중요성

프로젝트를 시작할때 모델링을 빠르게 끝내고 View 작성하는데 많은 시간을 투자 해야겠다고 생각했다. 하지만 내 생각과는 다르게 모델링은 빨리 끝나지 않았고 많은 수정을 해야만했다. 우리팀은 프로젝트가 끝나는 금요일 하루 전에도 모델링 수정을 해야만했다. 사실 이 과정들은 너무나도 당연했다. 경험이 쌓이면 모델링을 하면서 View 로직이 보인다고 말씀하셨던 멘토님이 떠오른다. 우리가 모델링을 할때 View에서 어떤 로직으로 구현할지에 대해 전혀 생각 하지 못했다. 그저 이것은 일대다, 다대다, 일대일 관계일 것이다 라는 관점만 생각했기 때문에 모델링은 완벽할 수 없었다. View를 만들면서 모델링이 잘못됬구나 라는것을 깨닫고 많은 수정을 하게 되었다. 아마 2차 프로젝트를 하면서는 모델링 수정이 1차 프로젝트보다는 많이 줄지 않을까 라는 생각을 해본다.

프로젝트를 하면서 느낀점

과도한 목표 설정

나는 이번 프로젝트에서 가장 중요하게 생각했던것이 많은 기능을 구현 하는 것이였다. 기능 구현에만 집중 하다보니 프론트와의 소통이 많이 이루어지지 않았다. 물론 매일같이 얘기를 나누고 노력한것은 사실이지만 기능을 구현했을때 빠르게 통신을 해보려고 하지 않았다. 그렇게 하다가 보니 프로젝트를 마무리 하는 과정, 즉 전체적인 기능을 확인해보는 상황에서 많은 시간이 지체되었고 급하게 마무리한 느낌이 들었다. 실제로 ppt로 발표한 내용보다 백엔드는 더 많은 기능이 구현되었지만 함께 프로젝트를 하면서 더 많은 기능을 구현하는것이 팀 프로젝트를 하면서 좋은 방향이였을까에 대한 의문이 남는다. 많은 기능 구현보다 프론트와 소통을 더 중요하게 생각했다면 조금 더 완성도 높은 결과물을 만들어냈을것 같다. 실제로 ppt 발표하는날에 배포를 하지 못해 local server로 발표를 진행하였다. 2차 프로젝트때는 기능 구현에 대한 욕심을 조금 줄이고 프론트와 더 많은 소통을 통해 완성도 높은 결과물을 만들어내려고 노력해야겠다.

내가 맡은 일에만 집중한 것

팀 프로젝트 였지만 각자 맡은 부분이 달랐고 내가 맡은 일을 하기에도 어떻게보면 많이 버거웠었던것도 사실이지만 돌이켜서 생각해보면 나는 조금 더 시간이 남았던것 같다. 나와 맞춰봐야 하는 프론트엔드 예원님과 늦게까지 연락하면서 Visual Studio Code의 Live Share 를 통해서 통신한 결과 다른 팀원보다 일찍 구현이 완료 되었다. 그때 나는 추가 구현에 대한 욕심이 있었고 하나라도 더 해봐야지 라는 생각이 들었다. 그래서 더 많은 기능을 구현하기 위해서 혼자만 너무 앞서 나갔던것 같다. 그때 추가 구현을 하지 않고 팀원들의 부족한 점에 더 많은 관심을 기울이고 수정하는데 도움을 더 주었다면 마지막에 시간이 부족하지 않았을것 같다는 생각이 든다. 그렇게 했다면 발표 준비에도 더 많은 시간을 투자할 수 있었고 배포까지 해서 더 완성도 있게 프로젝트를 마무리 했을것 같다.

같이 일하고 싶은 개발자

1차 프로젝트를 끝내고 지친 몸으로 세션을 들었을때 ppt 마지막 slide 내용이 같이 일하고 싶은 개발자였다. 많은 생각이 들게한 문장이였다. 사실 개발자는 팀으로 일을 하기 때문에 주니어 면접은 인성면접이라는 말이 나올 정도로 실력보다는 같이 일하고 싶은 사람이 채용에 큰 영향을 준다는 말을 들은적이 있다. 나는 그 말을 들었을때 나는 사람들하고 잘 지내기 때문에 아무런 문제 없을것 같다는 생각이 들었다. 하지만 프로젝트를 끝내고 '같이 일하고 싶은 개발자'라는 문장을 보았을때 나를 돌이켜보면서 팀원들이 나를 진짜 같이 일하고 싶은 개발자라는 말에 동의를 할까 라는 궁금증이 생겼다. 물론 싸운적이 없고 함께 즐겁게 프로젝트를 진행했다. 하지만 어떤 사람은 과정을 중시하고 어떤 사람은 결과를 중요하게 생각 할수도 있다. 멘토님이 우리팀을 어떤 목적으로 구성했을지는 모르겠지만 프로젝트를 진행하면서 내가 백엔드 팀을 이끌어야 하는 포지션이 되어버렸다. 내가 잘해서가 아니라 단순히 다른 팀원에 비해 조금 더 적극적이였고 질문이 많았으며 말을 많이 하는 성격탓에 그렇게 되어버린것 같다. 같이 일하고 싶은 개발자는 무엇일까? 재밌는 사람, 성격이 좋은사람, 다른사람의 의견을 받아들일수 있는 사람 등등 많은 이유가 있겠지만 내가 생각하는 같이 일하고 싶은 개발자는 같은 목표를 향해서 같은 열정으로 함께 만들어가는 것이라고 생각한다. 지난 2주를 돌아보면 정말 최선을 다했고 후회는 없다. 지금 당연히 조금 향상된 실력으로 2주 전으로 돌아간다면 더 잘할 수 있겠지만 그건 욕심인것같다. 같이 일하고 싶은 개발자가 되려면 다른사람들과 같은 보폭으로 나아가는것도 중요한것 같다. 그 전에는 단순하게 생각했던것 같다. 잘하는 사람하고 일하면 좋겠지 라는 생각을 했지만 내게 팀원 5명을 뽑아서 프로젝트를 진행하게 해준다면 난 단순히 잘하는 사람만을 뽑을것 같지는 않다. 앞으로 2차 프로젝트가 남았고 기업협업도 남았다. 진짜 함께 일하고 싶은 개발자는 어떤 사람일까에 대해서 더 많은 생각을 해보고 그런 사람이 되려고 더 많이 노력해야겠다.

profile
백엔드 개발자가 되는 그날까지

0개의 댓글