뷰는 작성된 모델을 바탕으로, 들어오는 데이터를 어떻게 처리할지에 대한 논리를 맡고 있다.
자 그럼, 스타벅스 제품 모델을 작성한 것에 이어 뷰를 작성해보자.
스타벅스 제품 모델링을 간단하게 도식화하였다. 이 관계를 머릿속에 넣고 뷰 로직을 작성해보자.
만약 models.py에 작성한 자세한 내용을 확인하고 싶다면 요기를 확인하면 된다.
shell에서 작동하는 코드는 view에서도 유효하다. 무작정 view 로직부터 작성하지 말고, shell에서 충분히 연습해보자! 연습이 제대로 안 된 것 같다 싶으면 다시 shell으로 돌아가자.
예를 들어 각각의 쿼리셋 메소드가 어떤 형태의 값을 반환하는지, one-to-many 관계에서 참조하는 값은 어떻게 불러오는지 등 말이다.
서버 쪽 에러가 나면 500 Error
가 발생하고 로직이 중단되기 때문에 에러가 날 경우를 미리 테스트해서 방지해야 한다.
에러를 처리하는 방법에는 try, except문
또는 if문
방식이 있다.
에러가 이미 발생되고 except
로 처리하는 것보다 에러가 발생할 경우를 미리 if문
으로 대비하는 게 낫다. 에러가 실제로 발생하는 경우를 줄이는 것이 낫기 때문이다.
import json
from django.views import View
from django.http import JsonResponse
from products.models import (
Menu,
Category,
Product,
Image,
Allergy,
)
python 모듈
, django 모듈
, 내가 만든 클래스
순서대로 내가 필요한 모듈을 import해서 사용한다.
MenuView
는 처음 작성하는 클래스이므로 작성된 코드의 각각이 무엇을 의미하는지 하나씩 설명하도록 하겠다.
MenuView
클래스 안에는 전체 메뉴의 정보를 가져오는 get
함수와 메뉴 정보를 등록하는 post
함수가 있다.
class MenuView(View): [1]
def get(self, request): [2]
menu = list(Menu.objects.values())
return JsonResponse({'data':menu}, status=200)
[1] : MenuView
는 View
클래스를 상속받는다. View
라는 클래스를 만든 적이 없는데? 싶지만 장고에서 미리 만들어 둔 클래스다. 우리는 그냥 갖다 쓰기만 하면 된다.
[2] : self
는 객체의 인스턴스 그 자체를 말한다. 단번에 이해하기 어렵다면, self에 대해 더 알고 싶다면 이 사이트를 참고하자.
request
는 프론트엔드에서 보내는 요청(Request)을 받을 수 있는 파라미터이다. Request 메시지는 start line, headers, body 세 부분으로 구분되어 있다. 좀 더 자세히 알고 싶다면 이 포스팅을 참고하자.
def post(self, request):
data = json.loads(request.body) [1]
menu_name = data.get('name', None) [2]
if not menu_name: [3]
return JsonResponse({'message': 'KEY_ERROR'}, status=400) [4]
if Menu.objects.filter(name=data['name']).exists(): [5]
return JsonResponse({'message': 'ALREADY_EXISTS'}, status=409)
Menu.objects.create(name=menu_name)
return JsonResponse({'message': 'SUCCESS'}, status=201)
[1] : 프론트에서 request body에 들어 있는 정보는 Json
형식으로 들어온다. 왜냐하면 프론트와 백에서 사용하는 언어가 다르므로 서로 정보를 주고 받을 때는 Json
형식을 사용하기로 약속했기 때문이다. 따라서 json.loads()는 들어온 정보(Json)를 장고에서 사용할 수 있도록 파이썬 Dictionary
형태로 변환하는 역할을 한다.
[2] : data
에는 딕셔너리 형태로 값이 들어있다. 딕셔너리는 키를 사용해서 값을 가져올 수 있따. data['name']
이런 식으로 말이다. 하지만 이 방식은 키가 안 들어올 경우 KeyError
를 발생된다. KeyError
를 방지하기 위해 data.get('name', None)
을 사용한다. 이제 키가 들어오지 않으면 에러를 발생하는 대신 None
을 반환한다.
[3] : [2]에서 키가 들어오지 않을 경우(menu_name이 None일 경우), 프론트엔드 쪽에 키가 들어오지 않았다는 것을 알려야 한다.
[4] : JsonResponse
로 Dictionary
형식의 응답(Response) 메시지를 Json
으로 변환한다.
JsonResponse
대신HttpResponse
을 쓰면return HttpResponse(status=200)
이런 식으로 메시지 없이 상태코드만 프론트에 전달된다. 현업에서는HttpResopnse
가 많이 쓰일 수 있지만, 연습하는 입장에서는 메시지까지 보낼 수 있는JsonResponse
방식을 쓰도록 한다. (참고로HttpResponse
에 메시지 쓰면 서버에 뜨긴 하지만 프론트에 전달되는 건 아니다.)
[5] : 메뉴에는 중복된 값이 들어가지 않아야 한다. filter()
와 exists()
로 중복값을 막을 수 있다. 여기서 주의할 점은 filter()
대신 get()
을 쓰면 에러가 발생할 수 있으니 주의할 것!(get()
은 찾는 값이 없거나, 찾는 값이 2개 이상이면 에러를 발생시킨다.)
class CategoryView(View):
def get(self, request):
category = list(Category.objects.values())
return JsonResponse({'data':category}, status=200)
def post(self, request):
data = json.loads(request.body)
category_name = data.get('name', None)
menu_name = data.get('menu_name', None) [1]
if not (category_name and menu_name):
return JsonResponse({'message': 'KEY_ERROR'}, status=400)
if Category.objects.filter(name=category_name).exists():
return JsonResponse({'message': 'ALREADY_EXISTS'}, status=409)
if not Menu.objects.filter(name=menu_name).exists(): [2]
return JsonResponse({'message': 'FOREIGN_KEY_DOES_NOT_EXIST'}, status=404)
menu = Menu.objects.get(name=menu_name) [3]
Category.objects.create(
name=category_name,
menu=menu
)
return JsonResponse({'message': 'SUCCESS'}, status=201)
CategoryView
부터 model 클래스 간의 관계를 고려해야 한다.(Menu
와 Catergory
는 one-to-many
관계!)
특히, ForeignKey
인 menu
를 어떻게 다뤄야 하는지가 중요하다.
[1] : Menu
는 이름으로 입력받는다.
[2] : [1]에서 전달된 메뉴의 이름
이 DB에 있는지 없는지 확인한다.
[3] : [1], [2]에서 메뉴
가 있는지 확인 과정을 거쳤으니, Menu.objects.get()
으로 그 객체를 반환한다.
Product
에는 연결된 관계가 많다보니 고려해야 할 요소들이 많다.
class ProductView(View):
def get(self, request):
product_list = [{ [1]
'id' : product.id, [3]
'korean_name' : product.korean_name,
'english_name' : product.english_name,
'description' : product.description,
'is_new' : product.is_new,
'allergy' : list(product.allergy.values('name')) [4]
} for product in Product.objects.all()] [2]
return JsonResponse({'data':product_list}, status=200)
[1] : product_list
를 리스트 컴프리헨션
으로 구하였다. for문
과 append
를 활용하는 것보다 리스트 컴프리헨션
이 속도가 더 빠르고, 가독성이 좋아지고, 코드의 길이도 준다. 좀 더 파이써닉한 방식이라 할 수 있다.
[2] : all()
은 QuerySet안의 모든 객체
를 리스트 형태로 반환하므로, product
에는객체
가 들어간다.
[3] : 객체(product)
는 dot(.) notation으로 자신이 가지고 있는필드(id)
를 불러올 수 있다. -> product.id
[4] : Product
와 Allergy
는 many-to-many
관계이다. many-to-many
관계에서의 참조는 추가로 포스팅할 예정이다.
def post(self, request):
data = json.loads(request.body)
korean_name = data.get('korean_name', None)
english_name = data.get('english_name', None)
description = data.get('description', None)
is_new = data.get('is_new', None)
category_name = data.get('category', None)
allergy_list = data.get('allergy', None) [1]
image_list = data.get('image', None)
if not (korean_name and english_name and description and category_name): [2]
return JsonResponse({'message': 'KEY_ERROR'}, status=400)
if not Category.objects.filter(name=category_name).exists():
return JsonResponse({'message': 'FOREIGN_KEY_DOES_NOT_EXIST'}, status=404)
category = Category.objects.get(name=data.get('category'))
product = Product.objects.create(
korean_name = korean_name,
english_name = english_name,
description = description,
is_new = is_new,
category = category
)
if allergy_list: [3]
for allergy_value in allergy_list: [4]
allergy = Allergy.objects.get_or_create(name=allergy_value)[0] [5]
product.allergy.add(allergy)
if image_list:
for image in image_list:
Image.objects.create(
image_url = image,
product = product
)
return JsonResponse({'message': 'SUCCESS'}, status=201)
[1] : product
정보를 입력하면서 allery
와 image
정보를 같이 입력할 수 있도록 했다.
[2] : is_new
필드는 default
값이 있어 입력되지 않아도 괜찮으므로 조건에서 제외했다.
[3] : allergy_list
가 입력이 되었을 때만 아래 코드가 실행된다. 알러지 정보가 없을 수도 있기 때문이다.
[4] : 하나의 음료에 알러지는 여러 개가 있을 수 있으므로 리스트
형태로 받아서, for문
으로 하나씩 생성한다.
[5] : get_or_create()
메소드를 사용해서, 알러지가 이미 DB에 있으면 가져오고(get), 없으면 만들어서(create) 가져온다. 주의할 점은 (object, created)
형태의 튜플로 값이 반환된다. 우리가 원하는 것은 생성된 객체(object)
이므로 [0]
인덱싱으로 값을 가져온다.
영양정보(nutrition)에 대한 부분은 생략하도록 하겠습니다.
HTTP 통신으로 무언가를 생성하고, 조회하기 위해서는 만들어진 View
에 url
경로를 할당해주어야 한다. 즉, View
를 외부 세계와 연결시키기 위해서는 url
주소가 있어야 한다.
그렇다면 경로는 어떻게 알 수 있을까?
starbucks/settings.py
로 가면 알 수 있다. ROOT_URLCONF = 'starbucks.urls'
라고 적혀 있는 걸 보니, starbucks.urls
에서 최초 경로를 관리하고 있음을 알 수 있다.
starbucks/urls.py
from django.urls import path, include
urlpatterns = [
path('products', include('products.urls'))
]
urlpatterns
는 리스트 형태로 여러 개의 경로를 받을 수 있다. path
함수 안에 각 요청이 어디로 가야 하는지 작성한다.
include
는 앞에 인자로 있는 경로로 들어오면, 그때부터의 경로는 include
안에 있는 곳에서 관리한다는 뜻이다. 즉, /products
를 치고 들어오면 그 뒤의 url
은 products.urls
에서 찾으라는 이야기이다.
참고로 기본 url은 마지막에 "/"를 자동으로 붙여주기때문에, 또 적으면 안 된다. /products
라고 적으면 http://http://127.0.0.1:8000//products
슬래시가 두 번 적용되니 주의하도록 한다.
products/urls.py
from django.urls import path
from .views import MenuView, CategoryView, ProductView
urlpatterns = [
path('/menu', MenuView.as_view()),
path('/category', CategoryView.as_view()),
path('/product', ProductView.as_view()),
]
이번에는 include
를 타고 들어온 products.urls
를 작성해보겠다.
먼저 우리가 아까 작성한 view를 사용하기 위해 import한다. 그리고 현재 경로로 들어온 데이터 요청을 우리가 만든 MenuView, CategoryView, ProductView 클래스를 통해 처리한다.
그리고 우리가 만든 클래스는 MenuView(View)
와 같이 장고의 View 클래스를 상속 받았는데,
이 View 클래스는 내장 함수를 반환하는 as_view()
클래스 메서드를 제공한다.
as_view()
는 클래스의 인스턴스를 생성하고, 이 인스턴스의 dispatch()
메소드를 호출한다. dispatch() 메서드에서는 뷰가 받은 요청을 검사해서 HTTP 요청 메서드(GET, POST 등)를 알아낸 다음, 인스턴스 내에 해당 이름을 갖는 메서드로 요청을 전달한다. 만약 뷰 안에 요청으로 온 메서드가 정의되어있지 않으면 HttpResponseNotAllowed
예외가 발생한다.
방금 만든 우리의 뷰를 가지고 대입해보면, http://127.0.0.1:8000/products/menu name="음료"
를 통해 요청이 들어오면, as_view()
가 MenuView
의 인스턴스를 생성하고, dispatch()
메서드를 통해 들어온 요청 메서드가 뷰 안에서 정의한 POST인지 확인한다. 이 경우 POST가 있기 때문에 정상적으로 정의된 MenuView의 논리에 따라 응답이 처리되서 반환된다.
자 이제 view와 url 모두 작성이 되었으니, 실제로 HTTP 통신이 되는지 확인해 볼 시간이다. Httpie
를 설치해 테스트를 진행하겠다.
통신을 하기 위해서는 서버를 켜야 한다. manage.py
가 위치한 루트 디렉토리에서 python manage.py runserver 0:8000
로 서버를 킨다. 서버가 켜진 쉘에서는 다른 명령어를 입력할 수 없으므로 다른 쉘에서 작성한다.
http -v POST 127.0.0.1:8000/products/category name="브루드 커피" menu_name="음료"
http -v GET 127.0.0.1:8000/products/category
http -v POST 127.0.0.1:8000/products/category name="프라푸치노" menu_name="음료"
http -v POST 127.0.0.1:8000/products/category menu_name="음료"
Httpie Request 문법은 이 사이트를 참고하면 좋다!
Product의 상세정보를 조회하고, 수정, 삭제하는 부분(ProductDetailView)은 이어지는 포스팅에서 다룰 예정입니다!