[TIL 12일자] 데브코스 데이터엔지니어링

·2023년 4월 25일
0

데브코스

목록 보기
11/55
post-thumbnail
post-custom-banner

📚 오늘 공부한 내용

1. 뷰(View)에서 템플릿(Templates) 연결

  • html을 사용해서 데이터를 잘 보이게 정리해 주는 도구가 템플릿(Templates)이다.

1) HTML 템플릿 생성

  • 뷰(View)템플릿(Template)으로 연결해 주기 위해 먼저 템플릿(Template)을 만들어 주어야 한다.
  • polls 앱에 templates/polls라는 폴더를 추가한 후 index.html이라는 기본 화면을 만들어 주었다.
  • 이후 Django 서버에 접속했을 때 보여 줄 화면을 html을 통해 작업한다.
<ul>
    <li>{{first question}}</li> <!--넘어오는 파라미터를 표출해 주기 위해서는 {{파라미터의 KEY}}로 작성해 준다.-->
<ul>

2) 뷰(View)에서 템플릿 연결

  • views.py로 간다. 이때 models.py에 있는 데이터를 화면에서 표출해 줄 것이므로 from .models import *를 해 준다.
  • 또한 템플릿(template)으로 그려 주기 위해서는 from django.shortcuts import render을 해 주고 render 메소드를 사용해 주어야 한다.
  • render(request, '템플릿(Template_name).html', 내용)
from .models import *
from django.shortcuts import render

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5] #pub_date를 역순으로 정렬을 해서 다섯 개를 가지고 온다.
    context = {'first_question': latest_question_list[0]}
    return render(request, 'polls/index.html', context)

.order_by()의 SQL

  • 위의 ORDER BY문을 .SQL로 출력하면 다음과 같다.
SELECT "polls_question"."id"
     , "polls_question"."question_text"
     , "polls_question"."pub_date"
 FROM "polls_question" 
ORDER BY "polls_question"."pub_date" 
 DESC LIMIT 5

3) 여러 개의 리스트를 표출

  • 먼저 2에서 latest_question_list[0]를 전체 리스트를 보낼 수 있도록 수정해 준다.
from django.shortcuts import render
from django.http import HttpResponse
from .models import *
# Create your views here.

def index(request):
    lastest_question_list = Question.objects.order_by('-pub_date')[:5] #pub_date를 역순으로 정렬을 해서 다섯 개를 가지고 온다.
    context = {'questions': lastest_question_list}
    return render(request, 'polls/index.html', context)
  • 이후 템플릿에서 수정을 해 주는데 이때 {{questions[0]}} 이렇게 index를 호출해 주면 오류가 발생한다. 그 원인은 html에서는 index 호출을 저렇게 하는 것이 아니라 {{questions.0}} 이렇게 해 주어야 한다.
  • 우리는 여러 개의 리스트를 표출할 것이기 때문에 html에서 for문을 돌려야 한다. for문if-else 조건문{% %}를 사용하여 작성해야 한다.
{% if questions %} <!--if문 표현 방법 닫을 때는 endif를 사용한다.-->
<ul>
    {% for question in questions %} <!--for문 표현 방법 닫을 때는 endfor를 사용한다.-->
    <li> {{question}} </li>
    {% endfor %}
</ul>
{% else %}
<p>no questions</p>
{% endif %}

2. 상세 페이지(detail)

1) 상세 페이지 HTML 템플릿 생성

  • detail.html이라는 HTML 템플릿Templates/polls 폴더에 생성한다.
  • 상세 페이지는 질문의 내용(question.question_text)과 그에 해당하는 대답(question.choice_set)을 보여 준다.
  • 이때 question.chioce_set.all은 하나가 아니기 때문에 for문을 써 주어야 하며 기존 Django ShellVisual Studio Code에서는 choice_set.all()이라 쓰지만 html에서는 그렇게 쓸 경우 오류가 발생한다. 꼭 .choice_set.all이라고 써 주어야 한다.
<h1>
    {{question.question_text}}
</h1>

<ul>
    {% for answer in question.choice_set.all %}
    <li>
        {{answer.choice_text}}
    </li>
    {% endfor %}
</ul>

2) 상세 페이지 urls.py에 추가하여 url 생성

  • url이 있어야 해당 페이지로 이동이 가능하기 때문에 urls.py에 상세 페이지를 추가해 준다.
  • 이때 상세 페이지는 특정 파라미터(question_id)를 넘겨서 보내 주는 방식으로 구현한다. (왜냐하면 question_id가 pk이고, 중복이 없는 고유 값이기 때문이다.)
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'), #이 방식이 question_id를 넘겨 주는 방식 (views.detail에) 
    path('some_url', views.some_url),
]

3) 상세 페이지 views.py에 생성 후 표출

  • 상세 페이지는 question_id와 같은 유일한 고유 값인 pk로 설정해 준다.
def detail(request, question_id):
    question = Question.objects.get(pk=question_id)   #파라미터로 넘겨 준 question_id와 일치하는 question을 담는다
    return render(request, 'polls/detail.html', {'question': question}) #이 question을 question으로 해서 detail.html로 보내 준다.
  • 이렇게 코드 작업을 마치면 다음과 같이 polls/(question_id)를 넘겨 주면 그 question_id에 해당하는 질문과 답변이 나오게 된다.

4) 상세 페이지 링크 추가하기 (질문을 클릭하면 상세로 이동하도록)

  • click 이동 시에는 <a> 태그를 이용하게 된다.
  • main 템플릿으로 사용한 index.html로 수정이 가능하다.
{% if questions %}
<ul>
    {% for question in questions %}
    <li> <a href="/polls/{{question.id}}">{{question.question_text}} </a></li>
    {% endfor %}
</ul>
{% else %}
<p>no questions</p>
{% endif %}
  • 반대로 코드상에서도 수정할 수 있다. 이때는 다음과 같이 html 템플릿이 바뀌게 된다.
{% if questions %}
<ul>
    {% for question in questions %}
    <li> <a href="{% url 'polls:detail' question.id %}">{{question.question_text}} </a></li> #polls를 지정하지 않고 바로 detail과 연결 
    {% endfor %}
</ul>
{% else %}
<p>no questions</p>
{% endif %}
  • 그러면 다음과 같이 각 상세창과 연결될 수 있게 질문들이 바뀌게 된다.

3. 404 ERROR 처리

  • 404 Page Not Found는 사용자가 뭔가 요청을 잘못 준 것 같다는 응답을 서버에서 주는 것이다.
  • status code: 404인 것이다.
  • 그런데 우리가 만든 Django Page에서 없는 파라미터를 보내 http://127.0.0.1:8000/polls/101/ 다음과 같은 창을 만들면 pk 데이터 중 101이 존재하지 않기 때문에 status code:404가 나와야 하는데 status code:500으로 나게 된다.
    • 왜? 원인을 Django가 알지 못하기 때문에.
    • 이것 역시 404로 가도록 처리해 주어야 한다.
  • views.py 화면에서 404 에러를 직접 raise 해 준다.
from django.http import Http404

def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

  • 하지만 404 에러는 자주 발생하는 경우이기 때문에 django.shortcuts에서 get_object_or_404 서비스를 제공한다.
  • 이를 사용해 404를 처리할 경우 코드는 다음과 같아진다.
from django.shortcuts import render, get_object_or_404

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})
  • 이 방법을 사용하면 코드가 더 간단해지기 때문에 이 방법을 사용하도록 한다.

4. 폼 (Forms)

  • 우리가 만든 QuestionChoice를 이용해서 사용자들이 직접 투표할 수 있는 설문조사 폼(forms)을 만들어 보자
  • 먼저 html 작업을 진행해야 한다.

1) 투표를 위한 Form 생성

  • 질문을 선택해 질문에 해당하는 설문 조사 창을 만들어야 하기 때문에 detail.html이 수정되어야 함을 알 수 있다.
<form action='#' method="post"> <!--설문 조사를 할 경우 다음 action="#" 여기에 모션이 들어가게 될 것이다.-->
    <h1>
        {{question.question_text}}
    </h1>

    {% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter}}" value="{{choice.id}}">
    <label for="choice{{forloop.counter}}">
        {{choice.choice_text}}
    </label>
    <br>
    {% endfor %}
    <input type="submit" value="Vote">
</form>
  • 이렇게 작업을 하면 다음과 같은 상세 창이 생기게 되는데 이때 vote 버튼을 눌러 투표를 하게 되면 403 Forbidden 오류가 발생한다.
  • 제출할 token이 없는데 Form을 제출하려고 해서 생기는 다음과 같은 오류이다. 이 경우 detail.html에서 token을 추가해 주면 된다.
<form action='#' method="post">
    {% csrf_token %} <!--추가된 토큰(기본적으로 제공되는 토큰이다.)-->
    <h1>
        {{question.question_text}}
    </h1>

    {% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter}}" value="{{choice.id}}">
    <label for="choice{{forloop.counter}}">
        {{choice.choice_text}}
    </label>
    <br>
    {% endfor %}
    <input type="submit" value="Vote">
</form>
  • 이 토큰 처리를 하고 나면 오류가 발생하지는 않지만 어떤 작업이 일어나지는 않는다. 왜냐하면 action이 비어 있기 때문이다.

2) 투표 후 url 생성

  • 투표 후 연결될 url을 추가해 준다.
from django.urls import path
from . import views

app_name= 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
    path('some_url', views.some_url),
]

3) views.py에 연결될 url의 환경 생성

  • views.py에서 question_id를 받아 준 후 해당 questionchoice_set을 가지고 와 준다.
  • 이때 choice_setchoice 즉, choice_id를 통해 선택한 choice 값을 담아 둔다.
  • 담아 둔 choice 값을 1을 올려 준 후 이 내용을 DB에도 반영해야 하기 때문에 .save()를 써 주게 된다.
  • HttpResponseRedirect(reverse(위치))는 지정된 위치로 되돌아가게 해 주는 기능이다. 즉, 모든 투표가 완료되면 다시 index.html 목록 화면으로 돌아가도록 구현해 주었다.
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, HttpResponseRedirect
from .models import *
from django.http import Http404
from django.urls import reverse

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    selected_choice = question.choice_set.get(pk=request.POST['choice'])
    selected_choice.votes += 1
    selected_choice.save()
    return HttpResponseRedirect(reverse('polls:index'))
  • 이렇게 완성한 코드로 직접 투표를 해 보면 votes 컬럼이 1이 올라간 것을 볼 수 있다.

5. 에러 처리

1) 아무것도 선택하지 않은 경우 혹은 선택할 항목이 삭제된 경우 (KEYERROR)

  • 만약 vote에서 아무것도 선택하지 않는다면 다음과 같은 오류가 발생한다.
  • 이러한 오류는 try-except문을 통해서 처리해 줄 수 있다.
#views.py
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try: 
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {'question': question, 'error_message': '선택이 없습니다.'})
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:index'))
  • 만약 vote를 한 순간에 선택지가 Choice 모델에서 사라진다면 어떻게 될까? DoesNotExist 오류가 발생하게 된다. 그렇기 때문에 Choice.DoesNotExist에 대해서도 처리를 해 준 것이다.
  • 이를 화면에 표출해 주기 위해 detail.html을 수정한다.
<form action={% url 'polls:vote' question.id %} method="post">
    {% csrf_token %}
    <h1>
        {{question.question_text}}
    </h1>
    {% if error_message %} <!--만약 error_message가 존재한다면 error_message를 보여 준다-->
    <p><strong>{{error_message}}</strong></p>
    {% endif %}
    {% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter}}" value="{{choice.id}}">
    <label for="choice{{forloop.counter}}">
        {{choice.choice_text}}
    </label>
    <br>
    {% endfor %}
    <input type="submit" value="Vote">
</form>
  • 다음과 같이 ERROR가 발생하는 경우 ERROR 문구를 띄워 주게 된다.

2) 동시 선택이 일어난 경우

  • 두 서버가 동시에 하나의 DB에 접속하는 경우 발생하는 오류를 생각해 보아야 한다.
  • 현재 코드는 서버에서 selected_choice.votes += 1을 해 주고 있다. 만약 이 경우 A 서버와 B 서버가 동시에 접속하고 현재 votes가 1이라면 둘을 합쳐서 3이 되어야 하는데 2가 된다.
  • 이런 경우를 대비하여 votes += 1과 같은 일을 서버에서 하는 것이 아니라 DB에서 처리하게 해 주어야 한다.
  • FDB에서 바로 가지고 온 값이다.
from django.db.models import F

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try: 
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {'question': question, 'error_message': '선택이 없습니다.'})
    else:
        selected_choice.votes = F('votes') + 1  #DB에서 votes를 바로 읽어서 오고 읽어서 온 값에 1을 더한다.
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:index'))

6. 결과 화면 생성

  • 위에서 detail을 만든 것과 동일한 순서를 진행한다. 다만 투표 후에 보여 주는 화면이 result.html 화면이 되는 것이다.

1) url을 생성

from django.urls import path
from . import views

app_name= 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
    path('<int:question_id>/result/', views.result, name='result'), #result 화면을 추가해 준다.
    path('some_url', views.some_url),
]

2) views.py로 result 생성 및 연결

  • 먼저 result를 생성한다.
  • 기존 vote에 투표 후 result로 넘어가도록 연결해 주어야 한다. 이때 result는 파라미터가 있기 때문에 그냥 호출하는 것이 아니라 args=(parameter,)를 추가해서 호출한다. (꼭 parameter를 호출한 뒤 ,를 붙이는 걸 기억해 둔다.)
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, HttpResponseRedirect
from .models import *
from django.http import Http404
from django.urls import reverse
from django.db.models import F

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try: 
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {'question': question, 'error_message': '선택이 없습니다.'})
    else:
        selected_choice.votes = F('votes') + 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:result', args=(question.id,)))

def result(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/result.html', {'question': question})

3) result.html을 통해 템플릿 생성

  • 투표 결과를 보여 주는 화면을 생성하자.
  • 투표의 질문과 투표 선택지에 따른 votes 투표 수를 보여 주면 될 것 같다.
<h1>
    {{question.question_text}}
</h1>
<br>

{% for choice in question.choice_set.all %}
<label>
    {{choice.choice_text}} -- {{choice.votes}}
</label>
<br>
{% endfor %}
  • 다음과 같은 화면이 나오게 된다.

7. admin 커스터마이징

1) Question add 화면 커스터마이징

  • Question의 컬럼인 question_textpub_date를 입력받는 화면을 커스터마이징 하기 위해 admin.py의 코드를 수정해 준다.
  • 이때 class를 만들어서 커스터마이징 해 주며 일전의 admin.site.register(model_name)을 했던 것과 동일하게 admin.site.register(model_name, customizing_class_name)을 호출해 주어야 한다.
  • class는 fieldsets을 만들어 준다.
  • 이때 pub_date 같은 경우 auto_now_add = True(이미 자동으로 추가가 되도록 설정해 둠.) 처리를 해 주었기 때문에 그냥 데이터를 받으면 오류가 발생하여 readonly 처리해 주어야 한다.
  • 또한 classes를 통해 visibility를 설정해 줄 수 있다.
class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        ('질문 섹션', {'fields': ['question_text']}),
        ('생성일', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    readonly_fields = ['pub_date']

admin.site.register(Question, QuestionAdmin)

2) Choice의 항목도 Question에서 같이 추가

  • Question의 커스터마이징 class에 inlines를 사용해 준다.
  • inlines으로 들어갈 부분도 동일하게 class를 생성해 주는데 인자로 layout을 설정할 수 있다.
    • StackedInline : 세로로 배치
    • TabularInline : 가로로 배치
  • modelextra를 설정해 줄 수 있는데 이때 extra는 몇 개를 추가할 수 있게 할 것이냐는 설정이다.
class ChoiceInline(admin.TabularInline): #StackedInline은 세로로 쌓이고 TabularInline은 가로로 쌓임 
    model = Choice
    extra = 3

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        ('질문 섹션', {'fields': ['question_text']}),
        ('생성일', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    readonly_fields = ['pub_date']
    inlines = [ChoiceInline]

admin.site.register(Question, QuestionAdmin)

3) 목록을 리스트 형식으로 수정

  • 먼저 목록을 수정해 주기 위해서는 admin.py의 커스터마이징 class에 list_display를 추가한다.
  • list_display 시에는 목록에 보여 주고 싶은 modelcolumn을 설정해 준다.
class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        ('질문 섹션', {'fields': ['question_text']}),
        ('생성일', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    list_display = ('question_text', 'pub_date', 'was_published_recently')
    readonly_fields = ['pub_date']
    inlines = [ChoiceInline]
  • 이때 목록의 이름을 설정해 주기 위해서는 models.py를 수정해 주어야 한다.
  • column으로 정의된 경우 .CharField()나 .DateTimeField()등 Field를 정의해 줄 때 인자로 verbose_name을 지정해 주면 된다.
  • 함수로 정의한 경우 함수 위에 @admin.display(description ='')를 해 주면 되는데 boolean 값의 경우 boolean=True 설정으로 보기 예쁜 형태로 만들어 줄 수 있다.
from django.db import models
from django.utils import timezone
import datetime
from django.contrib import admin 

#model은 models.Model을 상속받아야 한다.
class Question(models.Model):
    question_text = models.CharField(max_length = 200, verbose_name ='질문')
    pub_date = models.DateTimeField(auto_now_add = True, verbose_name='생성일')

    @admin.display(boolean=True, description='최근 생성(하루 기준)')
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

    def __str__(self):
        if self.was_published_recently():
            new_badge = '[new]'
        else:
            new_badge = ''
        return f'{new_badge} 제목: {self.question_text}, 날짜: {self.pub_date}'

4) 필터(검색) 기능 추가

  • 필터(검색) 기능을 사용하기 위해서는 admin.py의 커스터마이징 class를 수정해 주어야 한다.
  • 날짜는 list_filter를 이용하면 된다. Django에서 제공하는 필터 기능이다.
  • 검색어 입력이 필요한 요소들은 search_fields를 사용하면 된다. 이때 넘기는 파라미터 역시 정해 주어야 한다.
class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        ('질문 섹션', {'fields': ['question_text']}),
        ('생성일', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    list_display = ('question_text', 'pub_date', 'was_published_recently')
    readonly_fields = ['pub_date']
    inlines = [ChoiceInline]
    list_filter = ['pub_date']
    search_fields = ['question_text', 'choice__choice_text'] #질문 내용뿐만 아니라 choice 항목도 같이 검색되도록 구현

admin.site.register(Question, QuestionAdmin)

🔎 어려웠던 내용 & 새로 알게 된 내용

1. *args와 **kwargs

  • *args는 함수에서 사용되는 파라미터로 함수가 호출되고 여러 개의 인자(argument)를 입력받는 상황에서 유연성을 높여 준다.
def example(*args):
    for arg in args:
        print(arg)
  • **kwargs는 함수에서 사용되는 파라미터로 함수가 호출될 때 여러 개의 키워드 인자(keyword argument)-dictionary 인자를 받을 수 있는 기능을 제공한다.
def example(**kwargs):
    for key, value in kwargs.items():
        print(f"{key} = {value}")

2. CRUD(Create, Read, Update, Delete)

  • 소프트웨어가 가져야 하는 가장 기본적인 데이터 처리 기능을 말한다.

✍ 회고

Django는 정리할수록 웹 페이지 크롤링과 같이 따로 포스팅을 해야 되겠다는 생각이 든다. 생각보다 내용이 많이 길어지는 게 TIL을 쓰면서 느끼게 된다. 혼자 가볍게 응용해 볼 수 있는 것(토이 프로젝트)을 생각해 보고 응용하면서 포스팅을 하면 좋겠다는 생각이 들었다.

profile
송의 개발 LOG
post-custom-banner

0개의 댓글