프로젝트를 진행하면서 작성한 코드에 대해 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"
과 같은 방식으로 사용하는 것이다.
삼항연산자에 대해 대략적으로 이해한 후, 연우님의 리뷰에 따라 MainPage View와 MenuPage View를 합쳐봤다. 두 View의 기능은 이렇다.
product
, category
, option
id를 참조하는 모든 item의 정보를 제공한다.concept
으로 분류되어 Dictionary에 담기고, 각 Dictionary는 concept
에 대한 정보와concept
에 포함된 모든 item을 Dictionary 형태로 information
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에 해당하는 데이터를 보내라고 하셨다.
내가 이해한 연우님의 조언은,
- 코드를 합치되 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을 통해 받기로 했다. Q()
Expression을 사용하여 가독성 있게 만들었다.그런데 수정을 진행하면서 부딪힌 가장 중요한 문제가 있었다.
Menu일때 Dic List 안의 Dic List, Main일때 Dic List 형태로 만들고자 하면 Main일때 밖의 For문인
for concept in ~~
이 돌지 않고 하나의 요소만 나와야 한다.
내가 아직 많이 배우지 않은 점도 있지만, 아무리 생각해도 조건문을 통해 For문이 돌거나 돌지 않게 하는 방법이 생각나지 않았다. 검색을 통해 봐도 .. 삼항 연산자에서 if
앞에 와야 하는 것은 value
나 expression
이고, for문 같은 iteration
은 해당이 없는 것 같았다. 고민하느라 많은 시간을 쓰지 않고 우선 타협하기로 했다.
Menu인 경우 results의 형태가 [ {concept1, ..., [{item}, ....]}, {concept2, ..., [{item}, ....]}, {concept3, ..., [{item}, ....]}, ...]인 것처럼 Main도 [{}] 안에 하나의 리스트만 담고있는, [ { [ {Item1}, {item2}, ....] } ] 형태로 만들기로 했다. 리스트 안의 딕셔너리 안의 리스트이다..
데이터 형태를 같게 하기로 마음먹고, 수많은(정말로 많은) 에러와 시행착오를 거쳐 다음과 같이 코드를 작성했다.
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 요소 제거
하나씩 살펴보자.
resuts
List를 생성하는 바깥 For문이 Menu인 경우 concept 개수만큼, Main인 경우 Best Item 개수만큼 돌도록 삼항연산자를 사용해 concept
를 선언해주었다.
Main인 경우 concept.name
에서의 concept
가 실제로는 Item
Instance가 되어서, 실제로 Item
이 가지고 있지 않은 Field를 불러올때 에러가 났다. 그래서 Main인 경우 None 처리 하였다.
Color
는 Nullablae하게 지정하였다. 그래서 Color가 비어있는 경우 color.name
을 호출할 때 NoneType Object
는 Attribute를 가질 수 없다는 에러가 나왔다. 따라서 item.color
가 None인 경우 None으로 지정되도록 하였다.
Menu인 경우 concept
과 option
에 따라 Item을 filtering하여 Dictionary에 담기게 하였고, Main인 경우 concept
가 QuerySet이 아니라 Instance가 되어 Iterable하지 않기 때문에 []
로 리스트에 넣어줘서 For문을 돌 수 있도록 해주었다.
Dictionary에서 요소를 빼지 않을 경우 다음과 같은 결과가 나온다.
보기에도 불편하고 불필요한 데이터라고 생각하여 제거해주었다.
이렇게 코드를 수정한 후 POSTMAN 을 통해 통신한 결과 다음과 같이 요청에 반응했다.
Main Page인 경우
Menu Page인 경우
사실 아직도 이렇게 하는 것이 맞는 방식인지 모르겠다.그렇지만 혼자 고민하고 해결하는 과정에서 수많은 에러와 마주치고, 하나씩 해결하는 즐거움이 있었다고 생각한다.
또한 처음으로 Q() Expression을 아주 간단하게 써봤는데, 좀 더 복잡한 Filtering에 사용해보고 싶다 날짜 별 데이터, 제외와 포함, 필터링에 자주 사용하는 Attribute등에 대해 공부해보겠다.
'color' : item.color.name if not item.color is None else None, # [3] color가 없는 경우 None 반환 <--- 이 부분 제가 찾던 코드입니다~! 감사합니다