TIL 40 | Ternary Operator

임종성·2021년 8월 7일
1

TIL

목록 보기
11/22
post-thumbnail

프로젝트를 진행하면서 작성한 코드에 대해 Review를 받았다. 메인 페이지와 메뉴 페이지 GET 기능을 나누어 만들었는데, 삼항연산자를 사용해 데이터를 Main에 필요한 데이터와 Menu에 필요한 데이터를 나누어 보내주는 방식으로 처리하라는 리뷰를 받았다. 삼항연산자라는 단어를 처음 보았기에 블로깅해보려 한다.

삼항연산자

처음 단어를 들었을때는 뭔가 복잡한 연산자인줄 알았는데, 검색을 좀 해보니 list 작성을 한줄로 표현하는 list comprehension처럼 if-else 조건문을 한줄로 간결하게 표현하는 방식이라고 이해했다.

Condition이 참이면 True_Value를 취하고 거짓이면 False_Value를 취한다.
[True Value] if [Condition] else [False_Value]

예를 들어 어떤 Data를 반환하는데 Age>=30면 "Old", False면 "Young" 을 반환한다고 할때
"old" if Age>=30 else Young"과 같은 방식으로 사용하는 것이다.

First Thought

삼항연산자에 대해 대략적으로 이해한 후, 연우님의 리뷰에 따라 MainPage View와 MenuPage View를 합쳐봤다. 두 View의 기능은 이렇다.

  1. MainPage View
    • 전체 주문량이 높은 순으로 8개 Item의 정보를 제공한다.
    • 각 Item 정보가 Dictionary 형태로 담겨있고, Dictionary가 List에 담겨 프론트에게 제공된다.
    • [ {item1}, {item2}, ..., {item8}]
    • Dictionary가 담긴 List
  2. MenuPage VIew
    • product, category, option id를 참조하는 모든 item의 정보를 제공한다.
    • Item들은 concept으로 분류되어 Dictionary에 담기고, 각 Dictionary는 concept에 대한 정보와concept에 포함된 모든 item을 Dictionary 형태로 information List에 담아 준다.
    • [{concept name, concept description, [{item1 information}, {item2 information},.....]]
    • (Dicitionary가 담긴 List)를 포함하는 Dictionary들이 담긴 List

처음 리뷰를 받았을때, 단순히 한 View에서 두 기능을 사용하라는 뜻으로 받아들이고, 우선 두 View를 합친 후 아래와 같이 PR 했다.


import json
from django.http     import JsonResponse
from django.views    import View
from products.models import Option, Product, Category, Concept, Item
class PageView(View):
     def get(self, request):
        try:
            product_id  = request.GET.get('product_id', None)
            category_id = request.GET.get('category_id', None)
            option_id   = request.GET.get('option_id', None)

            if not product_id and not category_id and not option_id:
                items = Item.objects.order_by('-order_quantity')[:3]
                main= [
                    {
                        'id'             : item.id,
                        'name'           : item.name,
                        'price'          : item.price,
                        'discount'       : item.price - item.discount,
                        'stock'          : item.stock,
                        'image'          : item.image_set.get(main=1).image_url
                    } for item in items]

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

            if not Product.objects.filter(id=product_id).exists() or not Category.objects.filter(id=category_id).exists() or not Option.objects.filter(id=option_id):
                return JsonResponse({'MESSAGE':'NO_MENU'}, status=400)
            
            concepts = Concept.objects.filter(category_id = category_id)
            menu = [
                {
                        'concept'             : concept.name,
                        'concept_description' : concept.content,
                        'concpet_id'          : concept.id,
                        'information'         : [
                            {
                                'id'       : item.id,
                                'name'     : item.name,
                                'price'    : item.price,
                                'discount' : item.discount,
                                'stock'    : item.stock,
                                'color'    : item.color.name,
                                'image'    : item.image_set.get(main=1).image_url
                            } for item in Item.objects.filter(concept_id=concept.id, option_id=option_id)]
                } for concept in concepts]

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

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

이 후에 연우님이 오셔서 Menu Page와 Main Page의 정보 (두 View 모두 item에 대한 정보를 담고있다)가 비슷하니, 아예 저 리스트를 합쳐서 조건을 통해 Main인 경우 Main에 해당하는 데이터만 보내고, menu인 경우 Menu에 해당하는 데이터를 보내라고 하셨다.

Second Thghout

내가 이해한 연우님의 조언은,

  • 코드를 합치되 Main, Menu 여부에 따라 다른 결과를 도출하는 것
  • Main Page인 경우 8개의 Best Item만 리스트에 담기
  • Menu Page인 경우 Concept 별로 Item 정보를 담아 리스트에 담기

일단 합쳐보고 이것저것 건드리니 아래와 같은 코드가 나왔다.

class PageView(View):
     def get(self, request):
        try:
            product_id  = request.GET.get('product_id', None)
            category_id = request.GET.get('category_id', None)
            option_id   = request.GET.get('option_id', None)
            main        = request.GET.get('main', None)
            sample      = [1]

            if not main and (not Product.objects.filter(id=product_id).exists() or not Category.objects.filter(id=category_id).exists() or not Option.objects.filter(id=option_id)):
                return JsonResponse({'MESSAGE':'NO_MENU'}, status=400)

            concepts = Concept.objects.filter(category_id = category_id) if not main else None

            results = [
                {
                        'concept'             : concept.name,
                        'concept_description' : concept.content,
                        'concpet_id'          : concept.id,
                        'information'         : [
                            {
                                'id'       : item.id,
                                'name'     : item.name,
                                'price'    : item.price,
                                'discount' : item.discount,
                                'stock'    : item.stock,
                                'color'    : item.color.name,
                                'image'    : item.image_set.get(main=1).image_url
                            } for item in (Item.objects.filter(Q(concept_id=concept.id)&Q(option_id=option_id)) if not main else Item.objects.order_by('-order_quantity')[:3])]
                } for concept in (concepts if not main else sample)]
  • product, category, option id와 main 여부는 Query String을 통해 받기로 했다.
  • Item Filtering 단계에서는 Q() Expression을 사용하여 가독성 있게 만들었다.

그런데 수정을 진행하면서 부딪힌 가장 중요한 문제가 있었다.

Menu일때 Dic List 안의 Dic List, Main일때 Dic List 형태로 만들고자 하면 Main일때 밖의 For문인 for concept in ~~이 돌지 않고 하나의 요소만 나와야 한다.

내가 아직 많이 배우지 않은 점도 있지만, 아무리 생각해도 조건문을 통해 For문이 돌거나 돌지 않게 하는 방법이 생각나지 않았다. 검색을 통해 봐도 .. 삼항 연산자에서 if 앞에 와야 하는 것은 valueexpression 이고, for문 같은 iteration은 해당이 없는 것 같았다. 고민하느라 많은 시간을 쓰지 않고 우선 타협하기로 했다.

Menu인 경우 results의 형태가 [ {concept1, ..., [{item}, ....]}, {concept2, ..., [{item}, ....]}, {concept3, ..., [{item}, ....]}, ...]인 것처럼 Main도 [{}] 안에 하나의 리스트만 담고있는, [ { [ {Item1}, {item2}, ....] } ] 형태로 만들기로 했다. 리스트 안의 딕셔너리 안의 리스트이다..

Final Code

데이터 형태를 같게 하기로 마음먹고, 수많은(정말로 많은) 에러와 시행착오를 거쳐 다음과 같이 코드를 작성했다.

class PageView(View):
     def get(self, request):
        try:
            product_id  = request.GET.get('product_id', None)
            category_id = request.GET.get('category_id', None)
            option_id   = request.GET.get('option_id', None)
            main        = request.GET.get('main', None)

            if not main and (not Product.objects.filter(id=product_id).exists() or not Category.objects.filter(id=category_id).exists() or not Option.objects.filter(id=option_id)):
                return JsonResponse({'MESSAGE':'NO_MENU'}, status=400)

	    # [1] 삼항연산자를 사용해 concepts에 들어갈 QuerySet 구분
            concepts = Concept.objects.filter(item__category_id = category_id) if not main else Item.objects.order_by('-order_quantity')[:8] 
            
            results = [
                {
                        'concept'             : concept.name if not main else None, # [2] 삼항연산자로 Main이면 None, Menu면 concept 정보
                        'concept_description' : concept.content if not main else None,
                        'concept_id'          : concept.id if not main else None,
                        'information'         : [
                            {
                                'id'       : item.id,
                                'name'     : item.name,
                                'price'    : item.price,
                                'discount' : item.discount,
                                'stock'    : item.stock,
                                'color'    : item.color.name if not item.color is None else None, # [3] color가 없는 경우 None 반환
                                'image'    : item.image_set.get(main=1).image_url
                                						  		  		# [4] main이면 Best item
                            } for item in (Item.objects.filter(Q(concept_id=concept.id)&Q(option_id=option_id)) if not main else [concept])]
                } for concept in concepts]

            if main: 
                for result in results:
                    del result['concept'], result['concept_description'], result['concept_id']
                    #[5] Main인 경우 깔끔하게 하기 위해 Dictionary 요소 제거

하나씩 살펴보자.

  1. resuts List를 생성하는 바깥 For문이 Menu인 경우 concept 개수만큼, Main인 경우 Best Item 개수만큼 돌도록 삼항연산자를 사용해 concept를 선언해주었다.

  2. Main인 경우 concept.name에서의 concept가 실제로는 Item Instance가 되어서, 실제로 Item이 가지고 있지 않은 Field를 불러올때 에러가 났다. 그래서 Main인 경우 None 처리 하였다.

  3. Color는 Nullablae하게 지정하였다. 그래서 Color가 비어있는 경우 color.name을 호출할 때 NoneType Object는 Attribute를 가질 수 없다는 에러가 나왔다. 따라서 item.color가 None인 경우 None으로 지정되도록 하였다.

  4. Menu인 경우 conceptoption에 따라 Item을 filtering하여 Dictionary에 담기게 하였고, Main인 경우 concept가 QuerySet이 아니라 Instance가 되어 Iterable하지 않기 때문에 []로 리스트에 넣어줘서 For문을 돌 수 있도록 해주었다.

  5. Dictionary에서 요소를 빼지 않을 경우 다음과 같은 결과가 나온다.
    보기에도 불편하고 불필요한 데이터라고 생각하여 제거해주었다.

이렇게 코드를 수정한 후 POSTMAN 을 통해 통신한 결과 다음과 같이 요청에 반응했다.

Main Page인 경우

Menu Page인 경우


TIL

사실 아직도 이렇게 하는 것이 맞는 방식인지 모르겠다.그렇지만 혼자 고민하고 해결하는 과정에서 수많은 에러와 마주치고, 하나씩 해결하는 즐거움이 있었다고 생각한다.

또한 처음으로 Q() Expression을 아주 간단하게 써봤는데, 좀 더 복잡한 Filtering에 사용해보고 싶다 날짜 별 데이터, 제외와 포함, 필터링에 자주 사용하는 Attribute등에 대해 공부해보겠다.

   
profile
어디를 가든 마음을 다해 가자

1개의 댓글

comment-user-thumbnail
2021년 8월 8일

'color' : item.color.name if not item.color is None else None, # [3] color가 없는 경우 None 반환 <--- 이 부분 제가 찾던 코드입니다~! 감사합니다

답글 달기