오늘도 개발

[1차 프로젝트 록차] 상품 목록 GET api 제작기 5 - 리팩토링 본문

TIL & 프로젝트 회고

[1차 프로젝트 록차] 상품 목록 GET api 제작기 5 - 리팩토링

Sueeeeeee 2022. 7. 27. 00:33

어느정도 필터링과 페이지네이션을 구현한 후 PR을 올리고 멘토님께 코드 리뷰를 받았다.

리뷰를 통해 여러 번 수정을 했더니 좀 더 효율적이고 정확한 코드가 된 것 같다.

코드 리뷰로 배운 것  

1. 딕셔너리에 getlist를 사용하면 리스트로 값을 받을 수 있다.

쿼리 파라미터에 다음과 같이 넣으면

# 엔드포인트
/products/list?type=powder&type=tealeaf

views.py에서 이렇게 받을 수 있다.

getlist를 사용하면 type을 키로 사용한 모든 키를 리스트로 한 번에 받을 수 있어서 편리하다.

request.GET
# <QueryDict: {'type': ['powder', 'tealeaf']}>

request.GET.get('type')
# tealeaf
# 리스트 중 하나만 추출됨

request.GET.getlist('type')
# ['powder', 'tealeaf']
# 리스트 전체를 받을 수 있다

 

2. filter()에 리스트를 넣을 수 있다.

리스트에 있는 요소로 쿼리셋을 필터링할 수 있다.

tea_types = request.GET.getlist('type')
# ['powder', 'tealeaf']

Products = Product.objects.filter(types__name__in = tea_types)
# type이 powder, tealeaf 중 하나이면 선택됨

 

3. 딕셔너리를 사용하면 더 정확하게 값을 사용할 수 있다.

아래 코드에서 Product.objects.filter(queries).order_by(sort).distinct()로 

sort를 바로 사용할 수도 있지만, sort 값이 잘못 들어온 경우 나는 오류를 방지할 수 없다.

 

이 때 딕셔너리를 사용하여 sort로 사용할 수 있는 값을 미리 지정해두면

값이 잘못 들어온 경우 키에러를 발생시키고 핸들링 할 수 있다.

sort = request.GET.get('sort', 'new-arrival')

# 'sort'의 값은 'price-desc', 'price-asc', 'new-arrival' 중 하나만 가능함
sort_dict = {
    'price-desc' : '-price',
    'price-asc'  : 'price',
    'new-arrival': '-created_at'
}

# 'sort'가 위 세 값 중 하나가 아닌 경우 에러 반환
if not sort in sort_dict.keys():
    return JsonResponse({'message': 'KEYERROR'}, status=400)

# sort로 order_by 진행
ordering = sort_dict.get(sort)
products = Product.objects.filter(queries).order_by(ordering).distinct()

 

4. 페이지 정보는 상품정보와 분리해서 넣어주어야 프론트에서 사용하기 편리하다.

처음에는 result라는 딕셔너리를 만들고

페이지 정보(limit, offset, 총 상품 개수, 총 페이지 수)와 상품 정보(타이틀, 썸네일 등)을 같이 넣어 보내주었다.

 

하지만 프론트 쪽에서 result를 받았을 때 페이지 정보와 상품 정보가 같이 들어 있으면 작업하기 복잡하다는 피드백을 받았다.

페이지 정보는 total이라는 딕셔너리에, 상품 정보는 products라는 딕셔너리에 분리해서 넣었더니

가독성도 높아지고 프론트에서 사용하기에도 더 편리해진 것 같다.

 

5. 상품처럼 고유한 아이디가 있는 경우엔 아이디도 같이 보내주는 것이 좋다.

상품 아이디는 화면에 보이지 않기 때문에 처음에 응답 메시지에 추가하지 않았다.

하지만 프론트 쪽에서 아이디로 작업해야 하는 경우가 생각보다 많았다.

상품 뿐만 아니라 카테고리 역시 아이디와 카테고리명이 같이 필요한 경우가 많았다.

 

앞으로는 어떤 오브젝트가 고유한 아이디 값을 갖는 경우

처음부터 아이디도 같이 보내주는 것이 좋을 것 같다.

 

+ 리팩토링 결과

class ProductListView(View):
    def get(self, request):
    	# request.GET 딕셔너리에서 사용할 값을 미리 빼놓는다.  
        limit              = int(request.GET.get("limit", 10))
        offset             = int(request.GET.get("offset", 1))
        first_category_id  = request.GET.get('first-category', 1)
        second_category_id = request.GET.get('second-category')
        sort               = request.GET.get('sort', 'new-arrival')
        # 1. getlist를 사용하면 값을 리스트 형식으로 받을 수 있다.
        tea_types          = request.GET.getlist('type')
		
        if second_category_id:
            queries = Q(second_category = second_category_id)

        elif first_category_id:
            queries = Q(second_category__first_category_id = first_category_id)
		
        # 2. 리스트로 쿼리셋을 필터링할 수 있다.
        if tea_types:
            queries &= Q(types__name__in = tea_types)
        
        # 3. 프론트에서 실수로 딕셔너리에 없는 키를 보내면 오류를 낼 수 있다.
        sort_dict = {
            'price-desc' : '-price',
            'price-asc'  : 'price',
            'new-arrival': '-created_at'
        }

        if not sort in sort_dict.keys():
            return JsonResponse({'message': 'KEYERROR'}, status=400)

        ordering = sort_dict.get(sort)
        
        products = Product.objects.filter(queries).order_by(ordering).distinct()
        
        p = Paginator(products, limit)
        pages_count = p.num_pages

        if offset < 1 or offset > pages_count:
            return JsonResponse({'result': 'INVALID_PAGE'}, status=404)
		
        # 4. 페이징 정보도 같이 넣어주어야 하는데, 상품정보와 분리해서 넣어주어야 프론트에서 사용하기 편리하다.
        total = {
            'total_items' : products.count(),
            'total_pages' : pages_count,
            'current_page': offset,
            'limit'       : limit
        }
        
        page_items = p.page(offset) 
        # 5. 상품처럼 고유한 아이디가 있는 경우엔 아이디도 같이 보내주는 것이 좋다.
        products = [{
            'id'              : page_item.id,
            'title'           : page_item.title,
            'price'           : page_item.price,
            'stock'           : page_item.stock,
            'thumbnail_images': [image.url for image in page_item.thumbnail_images.all()],
            'types'           : [type.name for type in page_item.types.all()]
        } for page_item in page_items]

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