[Django] 장고 뷰(View)
- -
장고 뷰(View)
장고(Django)에서 뷰(View)는 웹 애플리케이션의 사용자 인터페이스를 정의하고 클라이언트로부터 오는 요청을 처리하는 부분이다. 장고에서 추구하는 뷰에 대한 특징들에 대해 알아보는 시간을 가져보자.
단순성
뷰를 작성하는 것은 함수를 작성하는 것만큼 단순하고 직관적이어야 한다.
- 뷰는 요청 처리의 지휘자이다.
- 뷰에서 모든 처리를 하지 말 것
- 뷰는 비즈니스 처리를 위임할 뿐, 직접 비즈니스 로직을 구현하지는 않는다.
- Model/Form/Serializer를 적절히 활용해야 한다.
- 개발자는 함수로 처리할 수 있는 일을 하기 위해 클래스의 인스턴스를 굳이 생성하지 않아도 된다.
- 물론 함수(Function Based View)로 복잡한 처리를 할 수 있고, 클래스(Class Based View)를 활용한 처리도 가능하다.
# app/views.py
from django.http import HttpResponse, HttpRequest
from django.shortcuts import render
from django.views.generic import TemplateView
from app.models import Post
def index(request : HttpRequest):
return render(request, "app/index.html")
위의 index 뷰는 장고에서 흔히 볼 수 있는 간단한 형태의 뷰이다. index 뷰에서 요청을 처리하게 되면 app/index.html 템플릿 파일 내용을 기반으로 html 응답을 생성해서 응답한다.
index = TemplateView.as_view(template_name = "app/index.html")
html 템플릿을 통해 응답을 구현할 일이 장고에서는 정말 많다. 그래서 TemplateView라는 클래스 뷰를 통해 로직이 아닌 설정으로서 뷰를 구현할 수도 있다. django/views/generic.py 경로에 구현돼 있으며, 이름이 generic인 것은 이미 다양한 뷰 구현을 패턴화 시켜놓았다는 의미이다.
함수로 직접 구현한 index뷰와 지금은 코드길이가 큰 차이가 없다는 걸 확인할 수 있다. 하지만 코드의 길이가 중요한 게 아니라, 코드를 봤을 때 의도가 명확하게 드러나는지가 중요하다. 함수 뷰는 그 뷰가 어떤 동작을 하는지 살펴보려면 그 구현을 전부 다 봐야 하는데 반면, 클래스 뷰는 사용되는 클래스와 설정코드를 보면 빠르게 뷰의 의도를 파악할 수 있게 된다.
위의 index뷰는 정적인 HTML 응답일 때의 상황이고, 아래의 post_list뷰는 데이터베이스를 조회해서 HTML 응답을 만드는 코드이다. 한 번 살펴보자.
# app/views.py
from django.http import HttpResponse, HttpRequest
from django.shortcuts import render
from django.views.generic import ListView
from app.models import Post
def post_list(request):
qs = Post.objects.all()
return render(request, "app/post_list.html",{
"post_list", qs,
})
Post 모델을 통해서 쿼리 셋 객체인 qs를 생성하고, 템플릿 내에서 qs 객체를 참조할 이름으로서 "post_list"를 지정했다.
post_list = ListView.as_view(model = Post)
장고에서 제공하는 제네릭 뷰(Generic View)중 하나인 ListView를 사용하여 데이터베이스 모델(위의 예제의 경우 Post)의 목록을 보여주기 위한 뷰를 생성하는 코드이다. 여기에서 ListView는 일반적으로 모델의 목록을 보여주기 위한 뷰를 제공한다. as_view는 클래스 기반 뷰를 함수 기반 뷰로 변환하는 역할을 한다. 조금 더 자세히 정리해 보면...
- ListVIew는 Django에서 제공하는 뷰 클래스로, 특히 데이터베이스 모델의 목록을 보여주는 데 사용된다.
- as_view 메서드는 클래스 기반 뷰를 URL 패턴에 등록할 때 사용된다. 이 메서드는 클래스를 함수 기반 뷰로 변환하여 사용할 수 있도록 해준다.
- model = Post는 ListView에게 어떤 모델의 목록을 보여줄 것인지를 알려주는 파라미터이다. 여기서는 Post 모델의 목록을 보여주도록 설정했다.
위의 코드를 사용하면 Post 모델의 목록을 보여주는 뷰를 생성하고, 이를 URL 패턴에 등록하여 웹 애플리케이션에서 해당 뷰에 접근할 수 있게 된다. 이렇게 생성된 뷰는 Post 모델의 데이터를 자동으로 가져와 목록을 보여주게 된다.
📢 model = Post는 아무 조건이 없이 전체 Post 목록을 보여준다, 그러면 조건을 걸어서 목록을 가져올 땐 어떻게 할까?
ListView를 사용할 때 기본적으로는 해당 모델의 모든 레코드를 보여준다. 하지만 원하는 조건에 따라 목록을 필터링하려면 아래와 같이 ListView를 상속받은 클래스 기반 뷰에서 필드로 커스터마이징 할 수 있다.
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post # Post 모델을 사용
template_name = 'post_list.html' # 사용할 템플릿 파일 지정 (선택적)
context_object_name = 'posts' # 템플릿에서 사용할 컨텍스트 변수명 지정 (선택적)
ordering = ['-pub_date'] # 정렬 기준 지정 (선택적)
paginate_by = 10 # 페이지네이션을 사용할 경우 한 페이지당 표시할 아이템 수 지정 (선택적)
queryset = Post.objects.all()[:5] # 처음 5개의 레코드만 가져오기 (선택적)
📢 위의 예제에서 필드 이름들은 정해진 약속들인가? 아니면 개발자가 임의로 정해도 되는 것인가?
위의 코드에서 사용한 model, template_name, context_object_name, queryset 등은 장고에서 미리 정의된 속성들이며, 약속된 이름이다. 이들은 클래스 기반 뷰에서 특정 목적을 위해 사용되는 특별한 속성들이다.
- model - 뷰에서 사용할 모델 클래스를 지정한다.
- template_name - 해당 뷰에서 사용할 템플릿 파일의 경로를 지정한다.
- context_object_name - 템플릿에서 사용할 컨텍스트 변수의 이름을 지정한다.
- queryset - 뷰에서 사용할 쿼리 셋을 지정한다.
이러한 속성들은 특별한 의미를 가지며, 임의로 변경하면 해당 뷰가 기대한 대로 동작하지 않을 수 있다. 개발자는 이들을 적절하게 설정하여 원하는 동작을 얻을 수 있다.
요청 객체의 사용
요청의 모든 메타 정보를 담고 있는 요청 객체
요청 객체(HttpRequest 타입)는 장고에서 자동으로 생성하여, 뷰에 명시적으로 전달한다.
- 뷰 함수의 첫 번째 인자는 HttpRequest 객체이다
- 요청 헤더, 요청 인자(GET/POST/FILES), 세션, 쿠키, 로그인 유저 등
- 뷰를 테스트할 때는 "가짜" 요청 객체를 전달하는 방식으로 가볍고 깔끔한 테스트를 만들 수 있다.
템플릿 시스템과의 느슨한 결합
명시적으로 템플릿 시스템을 호출하여, 필요한 문자열을 렌더링 한다.
return render(request, "app/index.html")
뷰에서는 명시적으로 템플릿 시스템을 호출하여 필요한 문자열을 생성(랜더링)한다. render API를 통해 템플릿 시스템을 활용하는 의도를 명확히 보여주고 있다.
# app/views.py
from django.http import HttpResponse, HttpRequest
from django.shortcuts import render
from django.views.generic import ListView
from app.models import Post
def post_list(request):
qs = Post.objects.all()
return render(request, "app/post_list.html",{
"post_list", qs,
})
render 함수는 django/shortcuts.py 내에 구현되어 있다. render는 장고 템플릿 엔진을 활용해서 복잡한 문자열을 생성(랜더링)하고, 그 반환값은 HttpResponse 타입으로서 뷰에서 즉시 응답으로서 사용할 수 있다.
모든 뷰(함수, 클래스 기반 뷰)에서는 복잡한 HTML 템플릿 응답을 생성할 때에는 반드시 render API를 사용하게 된다.
GET과 POST를 구분
일반적으로 조회 목적으로 GET 방식을, 생성/수정/삭제 목적으로 POST 방식 사용
- GET - 주로 조회 목적
- 웹 스펙 상, 요청 패킷에 헤더만 존재하고, body가 없다.
- GET 요청에 대한 부가적인 데이터는 QueryString과 헤더로만 전달 가능
- 파일 업로드 불가 - QueryString 포맷에는 파일을 담을 수 없다.
- POST - 주로 생성/수정/삭제 목적
- 요청에 대한 부가적인 데이터는 QueryString과 헤더 및 body로 전달 가능
- 요청 패킷에 body가 있기에 파일 업로드 가능
- 조회에 사용할 수는 있지만, POST 요청은 캐싱할 수 없기에 매번 DB 조회가 발생한다. 이는 조회가 아주 많은 페이지(ex : 이벤트 페이지)에서 비효율적이다.
📢 QueryString은 URL 주소 뒤에?를 붙여 데이터를 전달하는 방식을 의미한다.(예 : example/posts? post_id=1)
검색어를 처리하는 일반적인 뷰에 대해 한 번 살펴보자.
def post_list(request):
qs = Post.objects.all()
query = request.GET.get("query","")
if query:
qs = qs.filter(title_icontains = query)
return render(request, "app/post_list.html", {
"post_list" : qs,
"query" : query,
})
https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=0&ie=utf8&query=django
검색어 요청을 처리하는 부분을 한 번 살펴보자. 네이버 공식 홈페이지에서 django로 검색을 하면 위와 같은 URL이 된다. 물음표(?) 뒷부분을 QueryString이라고 하며 다수의 key=value로 구성된다. query 값으로는 django가 있는 것을 확인할 수 있다.
코드에서 request.GET은 QueryDict 타입으로서 사전(dict)의 확장 타입이며 중복 키를 허용한다. request.GET은 사전 계열이기에. get 메서드를 통해 지정 키값을 가져올 수 있다. 지정 키가 없다면 None을 반환하는 데, 다른 값을 반환하려면 두 번째 인자로 디폴트 값을 지정한다. request.GET에 "query" 키가 없다면 빈 문자열 ""을 반환한다.(파이썬 딕셔너리 참고) 그래서 만약 query 변수의 길이가 1 이상일 경우 참(True)이 된다.
요청객체에서의 요청 인자
# django/http/request.py
from django.http import QueryDict
from django.utils.datastructures import MultiValueDict
# http에서는 중복 key를 처리할 수 있어야만 한다.
# a=1&a=1&b=2
# MultiValueDict은 중복 Key를 허용하는 Dict
# QueryDict은 MultiValueDict을 상속받았으며
# QueryString 문자열을 파싱하여 MultiValueDict을 구성한다.
class HttpRequest:
"""A basic HTTP request"""
# 중략
def __init__(self):
self.GET = QueryDict(mutable = True)
self.POST = QueryDict(mutable = True)
# 중략
self.FILES = MultiValueDict()
# 생략
뷰에서는 요청인자 request.GET, request.POST, request.FILES 3가지가 있다. request 클래스 내역을 보면, self.GET, self.POST, self.FILES가 정의되어 있다. GET/POST는 QueryDict 타입이며, FILES는 MultiValueDict 타입임을 확인할 수 있다.
QueryDict 클래스는 MultiValueDict 클래스를 상속받았다, 그럼 MultiValueDict은 무엇일까? MultiValueDict은 중복 key를 허용한다. 파이썬의 기본 딕셔너리는 중복 키를 허용하지 않지만 HTTP에서는 중복 key를 처리할 수 있어야 한다는 스펙이 있다.
위의 코드에서 주석으로 처리된 QueryString을 예로 보면 a=1&a=1&b=2와 같이 있을 때 모든 값들은 전부 다 전달되어야 한다. 만약에 우리가 로컬에서 서버로 사진 100장을 한 번에 업로드한다고 했을 때, 각각의 사진파일마다 photo1, photo2, photo3... 이렇게 파라미터명을 다르게 준다면, 이름 지정의 어려움이 있을 수 있다.
그에 반해, 사진 100개의 파라미터명을 동일하게 지정해도 서버로 사진을 업로드할 수 있다면 보다 편리할 것이다. 그게 http의 스펙이다. 따라서, 그러한 파라미터들을 저장할 자료구조에서는 키 중복을 허용해야 한다는 것이고, 이를 위한 구현체가 MultiValueDict이다. 그리고 QueryDict은 중복 키가 있는 값들(QueryString 등)을 전처리하여 MultiValueDict으로 저장하는 사전인 것이다.
from django.http import HttpRequest
def myview(request : HttpRequest)
# 요청 URL의 QueryString을 참조
# POST 요청에서도 요청 URL에 QueryString이 있을 수 있다.
request.GET
# POST 요청의 data
request.POST
# POST 요청의 file data
request.FILES
요청 타입 GET/POST/PUT/PATCH/DELETE 등의 요청에서도 요청 URL과 QueryString 문자열은 있을 수 있다. 그러니, request.GET은 요청 URL의 QueryString 문자열을 전처리하여 저장한 값이기에 다른 요청타입에서도 값이 있을 수 있다.
POST 요청에서는 요청바디(body)에 값을 담아 전달할 수 있다. 이를 전처리하여 파일 데이터들은 request.FILES에 담고, 파일을 제외한 나머지 데이터들은 request.POST에 담긴다.
GET 요청과 POST 요청처리 로직을 패턴화
장고에서는 이를 패턴화 하여 클래스 기반 뷰(Class Based View)를 제공하고 있다.
def post_new(request):
# request.GET은 요청 URL의 QueryString을 참조하기에 POST 요청에서도 사용할 수 있다.
if request.method == "POST":
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save()
return redirect(post)
else:
form = PostForm()
return render(request, "app/post_form.html",{
"form" : form,
})
함수 기반 뷰에서 Form 처리를 하는 전형적인 코드를 한 번 살펴보자. 사용자가 어떤 HTML 폼을 작성해서 저장했을 때, 이를 처리하는 가장 일반적인 패턴이다. 하나의 뷰에서 POST 요청일 때와 GET 요청일 때의 처리를 분기한 걸 확인할 수 있다. 여기서 주의할 것은 아래의 else는 if request.method == "POST"에 대한 else로 처리해야 한다. 들여 쓰기를 주의하자.
GET 요청을 받으면 생성/수정 HTML 폼을 노출하고, POST 요청을 받으면 생성/수정 HTML 폼에 대한 저장 요청을 처리하는 데 유효성 검사를 한다. form.isvalid()로 본 요청을 처리할 수 있는 권한이 있는지, 지원하지 않는 값을 저장한 것은 아닌 지 등을 모두 검사해서 단 하나의 유효성 검사라도 실패를 한다면 거짓(False)을 리턴하게 된다. 그럼 render API를 통해 에러 폼내역을 보여주게 된다.
만약 유효성 검사에 통과하게 되면 통과한 값들로 필요한 처리(DB에 저장, 이메일 발송 등)를 하고 다른 페이지로 이동하게 된다.
이러한 생성에 대한 폼 처리 뷰를 패턴화 하여 아래와 같이 클래스 기반 뷰를 장고에서 제공한다.
# GET/POST 요청 처리를 패턴화하여, 클래스 기반 뷰를 제공
from django.views.generic import CreateView
post_new = CreateView.as_view(
model = Post,
form_class = PostForm,
)
# 혹은 아래와 같이 정의도 가능
class PostNew(CreateView):
model = Post
form_class = PostForm
위의 예제에선 생성에 대한 폼 처리 뷰이기 때문에 CreateView를 사용한다. 만약 수정에 대한 폼 처리 뷰를 사용하고자 한다면 UpdateView 클래스를 사용하면 된다. CreateVIew와 UpdateView를 사용하는 코드는 지정하는 클래스 타입만 다를 뿐, 나머지 옵션들은 거의 동일하다.
그래서 매번 함수 기반 뷰로 구성할 수도 있지만, 뷰의 시작 구현은 클래스 기반 뷰로 간결하게 시작하고, 상황에 따라 클래스를 확장하는 형태로 가도 좋다. 혹은 클래스 기반 뷰로 운영하다, 패턴화 하기 힘든 로직이 많을 경우 함수 기반 뷰로 넘어가도 좋은 방법이다.
📢 클래스 기반 뷰를 정의하는 2가지 방법
post_new = CreateView.as_view(
model = Post,
form_class = PostForm,
)
class PostNew(CreateView):
model = Post
form_class = PostForm
위의 두 코드는 동일한 작업을 하는 코드이다. 첫 번째 코드는 as_view 메서드를 사용하여 뷰를 생성하고, 두 번째 코드는 클래스를 직접 정의한 것으로서 CreateView 클래스를 상속받아 필요한 속성들을 지정한 형태이다. 두 방식 모두 CreateView를 기반으로 한 뷰를 정의하는 것이며, 뷰를 생성할 때 필요한 모델 및 폼 클래스를 지정하고 있다. 보통은 클래스를 직접 정의하는 방식이 더 많이 사용되며, 필요에 따라 클래스를 더 자세히 커스터마이징 할 수 있다.
제네릭 뷰를 사용할 때 클래스 안에서 정의하는 몇 가지 필드들은 해당 클래스에서 이미 정의된 속성들이다. 다양한 클래스들이 있지만 간단하게 대표적인 ListView, DetailView, CreateView, UpdateView, DeleteView 클래스들이 공통적으로 가지고 있는 속성들에 대해 알아보자. 각 클래스만 가지고 있는 고유의 속성들도 있으나, 지금은 위의 클래스들이 공통으로 가지고 있는 속성들에 대해만 간단하게 살펴보겠다.
속성 이름 | 속성 역할 |
model | 모든 제네릭 뷰에서 사용되는 중요한 속성으로, 이를 통해 해당 뷰에서 어떤 모델과 상호작용할 것인지를 지정한다. |
template_name | 렌더링에 사용할 템플릿 파일의 경로를 지정한다. 이 속성을 사용하여 특정한 템플릿을 지정할 수 있다. |
context_object_name | 템플릿에서 사용할 컨텍스트 변수의 이름을 지정한다. 이 변수는 템플릿에서 모델의 인스턴스(또는 목록)에 접근 하는 데 사용된다. |
form_class | CreateView와 UpdateView에서 사용되며, 폼 클래스를 지정한다. 이를 통해 사용자가 모델 인스턴스를 생성 또는 업데이트 할 때 사용할 폼을 설정할 수 있다. |
success_url | 폼 제출 후, 이동할 URL을 지정한다. CreateView, UpdateView, DeleteView에서 주로 사용된다. |
model_form_class | CreateView와 UpdateView에서 사용되며, 모델 폼 클래스를 지정한다. 모델 폼은 모델과 관련된 폼을 간단하게 만들어준다. |
queryset | 해당 뷰에 사용될 모델의 쿼리셋을 지정한다. |
pk_url_kwarg | URL에서 사용되는 기본 키(primary key) 매개변수의 이름을 지정한다. |
slug_field | URL에서 사용되는 슬러그 필드의 이름을 지정한다. |
from django.views.generic import ListView
from .models import Post
class PostList(ListView):
model = Post
template_name = 'post_list.html'
context_object_name = 'post_list'
paginate_by = 10 # 페이지당 항목 수
from django.views.generic import DetailView
from .models import Post
class PostDetail(DetailView):
model = Post
template_name = 'post_detail.html'
context_object_name = 'post_detail'
pk_url_kwarg = 'your_model_id' # URL에서 사용되는 기본 키 매개변수의 이름
# forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content']
# views.py
from django.views.generic import UpdateView
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
class PostUpdate(UpdateView):
model = Post
form_class = PostForm
template_name = 'post_update.html'
context_object_name = 'post'
success_url = reverse_lazy('post_list') # 업데이트 성공 후 이동할 URL
forms.py와 views.py 코드를 실제 프로젝트에서는 따로 분리하지만 위의 예제 코드에서는 편의를 위해 코드를 한 곳에 모두 적었다는 점을 참고하자. 실제로는 forms.py와 views.py는 분리되어 있다.
# forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content']
# views.py
from django.views.generic import CreateView, ListView
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
class PostCreate(CreateView):
model = Post
form_class = PostForm
template_name = 'post_create.html'
success_url = reverse_lazy('post_list') # 생성 성공 후 이동할 URL
마찬가지로 forms.py와 views.py는 실제로 분리되어 있다는 점을 참고하고, 위의 예제는 편의를 위해 저렇게 한 곳에서 작성되었다는 점을 명심해 주길 바란다.
from django.views.generic import DeleteView
from django.urls import reverse_lazy
from .models import Post
class PostDelete(DeleteView):
model = Post
template_name = 'post_confirm_delete.html'
success_url = reverse_lazy('post_list') # 삭제 성공 후 이동할 URL
'Framework > Django' 카테고리의 다른 글
[Django] 장고 템플릿(Template) (0) | 2024.01.02 |
---|---|
[Django] 장고 URL 설계 (0) | 2023.12.17 |
[Django] 장고에서의 요청 처리 (0) | 2023.12.14 |
[Django] 장고의 설계 철학 (1) | 2023.12.12 |
[Django] 장고 app 생성 (0) | 2023.11.01 |
소중한 공감 감사합니다