🐍 Django 게시판 만들기

Q&A 게시판 만들기 5. views.py 파일 분리해서 작성하기 1

복숭아아이스티에샷추가 2023. 10. 4. 18:00

내가 만든 pybo 프로젝트를 진행하다보면 views.py 파일에 함수가 점점 늘어나 파일 관리에 불편함이 있을 것이다. 점점 방대해지는 이 views.py를 분리하여 개선할 것이다.

 

먼저 views.py 파일에 정의할 함수들을 기능별로 분리할 것이다.

 

1. 기본 관리

2. 질문

3. 답변

4. 댓글

5. 추천

 


0. views 디렉터리를 생성

기능별로 나눈 5개의 view 파일들을 담을 디렉터리를 생성한다.

(파일 경로 = c:/projects/mysite/pybo/views)

 

 

1. 기본 관리 - base_views.py

from django.core.paginator import Paginator
from django.db.models import Q, Count
from django.shortcuts import render, get_object_or_404
from ..models import Question

def index(request):

    page = request.GET.get('page', '1')
    kw = request.GET.get('kw', '')
    so = request.GET.get('so', 'recent')

    if so == 'recommend':
        question_list = Question.objects.annotate(
            num_voter=Count('voter')).order_by('-num_voter', '-create_date')
    elif so == 'popular':
        question_list = Question.objects.annotate(
            num_answer=Count('answer')).order_by('-num_answer', '-create_date')
    else:
        question_list = Question.objects.order_by('-create_date')

    if kw:
        question_list = question_list.filter(
            Q(subject__icontains=kw) |
            Q(content__icontains=kw) |
            Q(author__username__icontains=kw) |
            Q(answer__author__username__icontains=kw)
        ).distinct()
  
    paginator = Paginator(question_list, 10)
    page_obj = paginator.get_page(page)

    context = {'question_list': page_obj, 'page': page, 'kw': kw, 'so': so}

    return render(request, 'pybo/question_list.html', context)

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    context = {'question': question}
    return render(request, 'pybo/question_detail.html', context)

일단 전체 기본 관리 코드는 위와 같다. 하나하나 자세히 살펴보겠다.

 

 

1. 입력인자

page = request.GET.get('page', '1')
kw = request.GET.get('kw', '')
so = request.GET.get('so', 'recent')

 

(1) page : 페이지 - 1은 파라미터가 없는 URL을 위해 기본값으로 1을 지정한 것이다.

(2) kw : 검색어

(3) so : 정렬기준

 

2. 정렬

if so == 'recommend':
	question_list = Question.objects.annotate(
	num_voter=Count('voter')).order_by('-num_voter', '-create_date')
elif so == 'popular':
	question_list = Question.objects.annotate(
	num_answer=Count('answer')).order_by('-num_answer', '-create_date')
else:
	question_list = Question.objects.order_by('-create_date')

(1) Count 함수 : 연결된 테이블의 로우 수를 세어서 그 값을 반환하는 함수이다. 즉, 각 질문들의 추천 수를 반환한다.

 

(2) annotate 함수 : 여러 모델 인스턴스를 가져올 때, 쿼리 결과에 특정 계산된 값을 추가하기 위해 사용하는 함수이다.

예를들어 게시판 조회시 각 글의 추천 수를 표시하고 싶을 때 사용하는 함수인 것이다. 그래서 Question 모델의 필드인 author, subject, content, create_date, modify_date, voter에 질문의 추천 수에 해당하는 num_voter 필드를 임시로 추가해주는 함수이다. 이렇게 추가해야 filter 함수나 order_by 함수에서 num_voter 필드를 사용할 수 있게 된다.

 

(3) order_by 함수 : 조회한 모델 데이터를 특정 속성으로 정렬하는 함수이다.

만약 이 함수에 2개 이상의 인자가 전달되는 경우 1번째 항목부터 우선순위를 매긴다. 즉, 추천수가 같으면 최신순으로 정렬되도록 설정하였다. 정렬 기준이 추천순(recommend)인 경우 추천수가 큰 것부터 정렬하므로 order_by에는 추천 수인 '-num_voter'로 입력했다. '-'기호가 앞에 붙어 있으면 내림차순으로 정렬되는 것이다. 

 

 

3. 조회

if kw:
    question_list = question_list.filter(
        Q(subject__icontains=kw) |
        Q(content__icontains=kw) |
        Q(author__username__icontains=kw) |
        Q(answer__author__username__icontains=kw)
    ).distinct()

(1) Q 함수 : OR 조건으로 데이터를 조회하는 Django 함수이다.

 

(2) distinct 함수 : 조회 결과의 중복을 제거하여 반환하는 함수이다.

 

(3) icontains 함수 : 해당 문자열이 포함되는지 반환해주는 함수이다. 대소문자 구별하지는 않는다.

(대소문자를 구별하고 싶다면 contains를 사용하면 된다.)

예를 들면 subject__incontains=kw 은 "subject에 kw라는 문자열이 존재한다면" 이라는 의미이다.

또한 "__"는 filter 함수에서 모델 필드에 접근하기 위해 사용한 것이다.

 

 

4. 페이징 처리 (Paginator 클래스)

paginator = Paginator(question_list, 10)

 

1번째 파라미터는 전체 데이터, 2번째 파라미터는 페이지당 보여줄 게시물의 개수를 의미한다.

 

page_obj = paginator.get_page(page)

: paginator 를 이용하여 요청된 페이지에 해당하는 페이징 객체 page_obj 를 생성했다. 따라서 page_obj 를 사용하면 Django가 데이터 전체를 조회하는 것이 아니라 해당 페이지의 데이터만 조회할 수 있게 된다. 이렇게 만들어진 page_obj 객체에는 아래와 같은 속성들이 있다. 이 속성들을 활용하면 페이징 처리가 아주 쉬워질 것이다.

paginator.count 전체 게시물 개수 next_page_number 다음 페이지 번호
paginator.per_page 페이지당 보여줄 게시물 개수 has_previous 이전 페이지 유무
paginator.page_range 페이지 범위 has_next 다음 페이지 유무
number 현재 페이지 번호 start_index 현재 페이지 시작 인덱스
previous_page_number 이전 페이지 번호 end_index 현재 페이지 끝 인덱스

 

 

5. context

context = {'question_list': page_obj, 'page': page, 'kw': kw, 'so': so}

조회한 Question 모델 데이터는 context 변수에 저장했다. 이 context 변수는 render 함수를 이용하여 html로 보낸다.

 

 

6. render 함수

return render(request, 'pybo/question_list.html', context)

context에 있는 Question 모델 데이터 question_list를 pybo/question_list.html 파일에 적용하여 HTML 코드로 변환한다.

 

 

그 다음은 detail 함수를 살펴보자. 앞서 본 index 함수보다는 확연히 짧다.

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    context = {'question': question}
    return render(request, 'pybo/question_detail.html', context)

index 함수와 유사하지만 다른 점은 question_id 라는 매개변수가 더 추가되었다는 것이다.

만약 /pybo/2/ 페이지가 호출되면 question_id에 2가 전달된다.

 

 


 

2. 질문 - question_views.py

질문 뷰는 세부적으로 등록, 수정, 삭제 이 세가지로 나눌 수 있기 때문에 이 question_views.py에는 세 가지 함수를 작성할 것이다. 전체 코드는 아래와 같다.

from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone
from ..forms import QuestionForm
from ..models import Question

@login_required(login_url='common:login')
def question_create(request):
    if request.method == 'POST':
        form = QuestionForm(request.POST)
        if form.is_valid():
            question = form.save(commit=False)
            question.author = request.user
            question.create_date = timezone.now()
            question.save()
            return redirect('pybo:index')
    else:
        form = QuestionForm()
    context = {'form': form}
    return render(request, 'pybo/question_form.html', context)

@login_required(login_url='common:login')
def question_modify(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user != question.author:
        messages.error(request, '수정권한이 없습니다')
        return redirect('pybo:detail', question_id=question.id)
    
    if request.method == "POST":
        form = QuestionForm(request.POST, instance=question)
        if form.is_valid():
            question = form.save(commit=False)
            question.author = request.user
            question.modify_date = timezone.now()
            question.save()
            return redirect('pybo:detail', question_id=question_id)
    else:
        form = QuestionForm(instance=question)
    context = {'form': form}
    return render(request, 'pybo/question_form.html', context)

@login_required(login_url='common:login')
def question_delete(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user != question.author:
        messages.error(request, '삭제권한이 없습니다')
        return redirect('pybo:detail', question_id=question.id)
    question.delete()
    return redirect('pybo:index')

 

1. 질문 등록 함수

@login_required(login_url='common:login')
def question_create(request):
    if request.method == 'POST':
        form = QuestionForm(request.POST)
        if form.is_valid():
            question = form.save(commit=False)
            question.author = request.user
            question.create_date = timezone.now()
            question.save()
            return redirect('pybo:index')
    else:
        form = QuestionForm()
    context = {'form': form}
    return render(request, 'pybo/question_form.html', context)

먼저 함수들마다 맨 앞에 있는 @login_required 애너테이션이 있다.

이것은 현재 로그인이 되어있는지 우선 검사하여 ValueError 오류를 방지한다.

만약 로그아웃 상태에서 @login_required 애너테이션이 적용된 함수가 호출되면 자동으로 로그인 화면으로 이동할 것이다.

 

질문 목록 화면에서 질문 등록하는 버튼을 누르면 

/pybo/question/create/ 가  GET 방식으로 요청되어 질문 등록 화면이 나타나고,

질문 등록 화면에서 입력값을 채우고 질문 저장하는 버튼을 누르면

/pybo/question/create/ 가  POST 방식으로 요청되어 데이터가 저장된다.

 

 

이렇게 동일한 URL 요청을 GET과 POST 요청 방식에 따라 다르게 처리했다.

 

여기서 GET과 POST 메서드는 Django에서 Http 프로토콜에서 데이터 전송을 위해 지원하는 메서드이다.

GET 방식은 주로 데이터를 요청하는 용도로 사용한다. 요청URL에 데이터가 노출되므로 보안에는 취약하지만 캐시를 이용하여 빠르게 처리할 수 있다. 즉, 자주 요청되는 데이터에 적합한 메서드이다.

POST 방식은 데이터를 요청 본문에 담아 보낸다. GET 방식과는 달리 요청 URL에 데이터가 노출되지 않으므로 보안성이 높다. 그래서데이터의 양이 많거나 민감한 정보를 전송할 때 사용된다. 하지만 요청 할 때마다 새로운 데이터를 서버로 전송하므로 GET 방식보다 더 많은 자원을 소비한다.

따라서 개발자는 GET과 POST방식을 적절하게 선택해서 사용하면 된다!

 

 

코드를 다시 살펴보면, GET 방식이냐 POST 방식이냐에 따라 QuestionForm이 입력값 없이 객체를 생성하는지 화면에서 전달받은 데이터로 폼의 값이 채워지도록 객체를 생성하는지 달라진다.

 

form.is_valid 함수는 POST 요청으로 받은 form이 유효한지 검사한다. 만약 유효하지 않는 다면 폼에 오류가 저장되어 화면에 전달될 것이다.

 

question = form.save(commit=False) 는 form 으로 Question 모델 데이터를 저장하기 위한 코드이다.

 

여기서 commit=False 는 임시 저장을 의미한다. 즉, 실제 데이터는 아직 저장되지 않은 상태인 것이다.

이렇게 임시 저장하는 이유는 폼으로 질문 데이터를 저장할 경우 Question 모델의 create_date에 값이 설정되지 않아 오류가 발생하기 때문이다. 그래서 임시 저장한 후 question 객체를 반환받아 create_date 에 값을 설정한 후 question.save()로 최종적인 저장이 완성되는 것이다.

 


 

2. 질문 수정 함수

@login_required(login_url='common:login')
def question_modify(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user != question.author:
        messages.error(request, '수정권한이 없습니다')
        return redirect('pybo:detail', question_id=question.id)
    
    if request.method == "POST":
        form = QuestionForm(request.POST, instance=question)
        if form.is_valid():
            question = form.save(commit=False)
            question.author = request.user
            question.modify_date = timezone.now()
            question.save()
            return redirect('pybo:detail', question_id=question_id)
    else:
        form = QuestionForm(instance=question)
    context = {'form': form}
    return render(request, 'pybo/question_form.html', context)

질문 생성 함수와 비슷하지만 질문 수정, 삭제는 해당 글쓴이에게만 주어지는 권한이므로 제한하는 코드를 작성해야한다.

로그인한 사용자인 request.user 와 글쓴이 question.author 가 다르면 '수정권한이 없습니다' 라는 오류가 발생한다.

 

또한 다른 점은 QuestionForm 함수에 instace=question 이라는 매개변수가 추가되었다.

수정버튼을 누르면 기존에 작성된 글이 그대로 폼에 채워진다. 그 상태에서 우리는 폼을 수정할 수 있는 것이다. 

 

form = QuestionForm(instance=question)

즉, 위 코드는 조회한 질문 question을 기본값으로 하여 화면으로 전달받은 입력값들을 덮어서 QuestionForm을 생성하라는 의미이다.

 


 

3. 질문 삭제 함수

@login_required(login_url='common:login')
def question_delete(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user != question.author:
        messages.error(request, '삭제권한이 없습니다')
        return redirect('pybo:detail', question_id=question.id)
    question.delete()
    return redirect('pybo:index')