Django | 스타벅스 뷰(View) 작성(1) - 생성, 조회

Sua·2021년 2월 6일
1

Django

목록 보기
6/23
post-thumbnail
post-custom-banner

뷰는 작성된 모델을 바탕으로, 들어오는 데이터를 어떻게 처리할지에 대한 논리를 맡고 있다.
자 그럼, 스타벅스 제품 모델을 작성한 것에 이어 뷰를 작성해보자.

✅ 테이블 관계 짚고 넘어가기

스타벅스 제품 모델링을 간단하게 도식화하였다. 이 관계를 머릿속에 넣고 뷰 로직을 작성해보자.
만약 models.py에 작성한 자세한 내용을 확인하고 싶다면 요기를 확인하면 된다.

📌 뷰 작성 시 포인트!

1. Django Shell에서 '충분히' 연습하기

shell에서 작동하는 코드는 view에서도 유효하다. 무작정 view 로직부터 작성하지 말고, shell에서 충분히 연습해보자! 연습이 제대로 안 된 것 같다 싶으면 다시 shell으로 돌아가자.
예를 들어 각각의 쿼리셋 메소드가 어떤 형태의 값을 반환하는지, one-to-many 관계에서 참조하는 값은 어떻게 불러오는지 등 말이다.

2. 에러 잘 처리하기

서버 쪽 에러가 나면 500 Error가 발생하고 로직이 중단되기 때문에 에러가 날 경우를 미리 테스트해서 방지해야 한다.

에러를 처리하는 방법에는 try, except문 또는 if문 방식이 있다.
에러가 이미 발생되고 except로 처리하는 것보다 에러가 발생할 경우를 미리 if문으로 대비하는 게 낫다. 에러가 실제로 발생하는 경우를 줄이는 것이 낫기 때문이다.

🖍 뷰 작성하기

모듈 import

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] : MenuViewView 클래스를 상속받는다. 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] : JsonResponseDictionary 형식의 응답(Response) 메시지를 Json으로 변환한다.

JsonResponse 대신 HttpResponse을 쓰면 return HttpResponse(status=200) 이런 식으로 메시지 없이 상태코드만 프론트에 전달된다. 현업에서는 HttpResopnse가 많이 쓰일 수 있지만, 연습하는 입장에서는 메시지까지 보낼 수 있는 JsonResponse 방식을 쓰도록 한다. (참고로 HttpResponse에 메시지 쓰면 서버에 뜨긴 하지만 프론트에 전달되는 건 아니다.)

[5] : 메뉴에는 중복된 값이 들어가지 않아야 한다. filter()exists()로 중복값을 막을 수 있다. 여기서 주의할 점은 filter() 대신 get()을 쓰면 에러가 발생할 수 있으니 주의할 것!(get()은 찾는 값이 없거나, 찾는 값이 2개 이상이면 에러를 발생시킨다.)


CategoryView

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 클래스 간의 관계를 고려해야 한다.(MenuCatergoryone-to-many 관계!)
특히, ForeignKeymenu를 어떻게 다뤄야 하는지가 중요하다.

[1] : Menu는 이름으로 입력받는다.

[2] : [1]에서 전달된 메뉴의 이름이 DB에 있는지 없는지 확인한다.

[3] : [1], [2]에서 메뉴가 있는지 확인 과정을 거쳤으니, Menu.objects.get()으로 그 객체를 반환한다.


ProductView

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] : ProductAllergymany-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 정보를 입력하면서 alleryimage 정보를 같이 입력할 수 있도록 했다.

[2] : is_new 필드는 default 값이 있어 입력되지 않아도 괜찮으므로 조건에서 제외했다.

[3] : allergy_list가 입력이 되었을 때만 아래 코드가 실행된다. 알러지 정보가 없을 수도 있기 때문이다.

[4] : 하나의 음료에 알러지는 여러 개가 있을 수 있으므로 리스트 형태로 받아서, for문으로 하나씩 생성한다.

[5] : get_or_create() 메소드를 사용해서, 알러지가 이미 DB에 있으면 가져오고(get), 없으면 만들어서(create) 가져온다. 주의할 점은 (object, created)형태의 튜플로 값이 반환된다. 우리가 원하는 것은 생성된 객체(object)이므로 [0] 인덱싱으로 값을 가져온다.

영양정보(nutrition)에 대한 부분은 생략하도록 하겠습니다.

⚙ Url 지정하기

HTTP 통신으로 무언가를 생성하고, 조회하기 위해서는 만들어진 Viewurl 경로를 할당해주어야 한다. 즉, View를 외부 세계와 연결시키기 위해서는 url 주소가 있어야 한다.

그렇다면 경로는 어떻게 알 수 있을까?
starbucks/settings.py로 가면 알 수 있다. ROOT_URLCONF = 'starbucks.urls'라고 적혀 있는 걸 보니, starbucks.urls에서 최초 경로를 관리하고 있음을 알 수 있다.

루트 디렉토리의 urls.py

starbucks/urls.py

from django.urls import path, include

urlpatterns = [
    path('products', include('products.urls'))
]

urlpatterns는 리스트 형태로 여러 개의 경로를 받을 수 있다. path 함수 안에 각 요청이 어디로 가야 하는지 작성한다.

include는 앞에 인자로 있는 경로로 들어오면, 그때부터의 경로는 include안에 있는 곳에서 관리한다는 뜻이다. 즉, /products를 치고 들어오면 그 뒤의 urlproducts.urls에서 찾으라는 이야기이다.

참고로 기본 url은 마지막에 "/"를 자동으로 붙여주기때문에, 또 적으면 안 된다. /products라고 적으면 http://http://127.0.0.1:8000//products 슬래시가 두 번 적용되니 주의하도록 한다.


해당 앱의 urls.py

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의 논리에 따라 응답이 처리되서 반환된다.

📲 Httpie로 통신 테스트 하기

자 이제 view와 url 모두 작성이 되었으니, 실제로 HTTP 통신이 되는지 확인해 볼 시간이다. Httpie를 설치해 테스트를 진행하겠다.

통신을 하기 위해서는 서버를 켜야 한다. manage.py가 위치한 루트 디렉토리에서 python manage.py runserver 0:8000로 서버를 킨다. 서버가 켜진 쉘에서는 다른 명령어를 입력할 수 없으므로 다른 쉘에서 작성한다.

POST로 카테고리 생성

http -v POST 127.0.0.1:8000/products/category name="브루드 커피" menu_name="음료"

GET으로 카테고리 정보 조회

http -v GET 127.0.0.1:8000/products/category

POST로 이미 존재하는 카테고리 생성 시도(Error)

http -v POST 127.0.0.1:8000/products/category name="프라푸치노" menu_name="음료"

POST로 name없이 카테고리 생성 시도(Error)

http -v POST 127.0.0.1:8000/products/category menu_name="음료"

Httpie Request 문법은 이 사이트를 참고하면 좋다!

Product의 상세정보를 조회하고, 수정, 삭제하는 부분(ProductDetailView)은 이어지는 포스팅에서 다룰 예정입니다!

참고사이트
https://velog.io/@devmin/Django-login-basic-v1.0

profile
Leave your comfort zone
post-custom-banner

0개의 댓글