🚝

[심화] 프로젝트 개선

1. Redirect

1.1. 결과 페이지의 문제점

지금까지 만든 MBIT 프로젝트의 결과 페이지에는 치명적인 문제점이 있습니다. 웹 브라우저의 탭을 두 개 열어서 하나는 메인 페이지를 하나는 설문을 마친 결과 페이지로 접속합니다.
메인 페이지메인 페이지
메인 페이지
 
결과 페이지결과 페이지
결과 페이지
 
현재 메인 페이지를 보면 데이터 분석과 인공지능이 결과로 나온 인원은 4명입니다. 이제 결과 페이지를 새로고침 해봅시다. 그러면 사진 처럼 양식 다시 제출 확인 메시지 창이 뜨는데 계속 버튼을 눌러봅니다.
notion imagenotion image
 
이 새로고침 과정을 여러 번 반복해봅니다. 그 후 메인 페이지 탭을 새로고침 해봅니다. 그러면 결과 페이지를 새로고침한 횟수만큼 데이터 분석과 인공지능 인원이 늘어나 있는 것을 확인 할 수 있습니다.
notion imagenotion image
 
이는 당연히 우리가 원하는 결과가 아닙니다. 사용자가 결과 페이지를 새로고침해도 페이지만 새로 로딩되기만 원하지 설문 결과가 다시 제출되는 것은 원치 않습니다.
웹 브라우저는 요청을 보내고 응답을 받은 뒤에도 보낸 요청이 무엇이었는지 기억하고 있습니다. 유저가 새로고침을 하면 기억하고 있던 요청을 다시 보내는 것입니다. 즉, 결과 페이지에서 새로고침을 하면 설문 데이터가 서버로 다시 전달되는 것입니다. 이를 방지하기 위해서 redirect를 사용해야 합니다.
 

1.2. redirect란?

redirect는 브라우저에게 어떤 특정한 url로 재요청을 하라고 명령하는 것입니다. 설문 제출 요청을 처리를 한 뒤, 결과 페이지를 렌더링하기만 하는 요청으로 리다이렉트하여 문제점을 해결할 수 있습니다. 설문 제출 요청 후에 결과 페이지를 받기만 하는 요청이 한 번 더 이루어지기 때문에 더 이상 처음의 설문 제출 요청은 웹 브라우저에 남지 않게 됩니다. 왜냐하면 웹 브라우저는 이전 상태는 전혀 기억하지 못하게 되어있기 때문입니다.
[이전] : 설문제출 요청 -> 처리 후 결과페이지 렌더링 [이후] : 설문 제출 요청 -> 처리 -> 결과 페이지 요청 -> 결과 페이지 렌더링
 

1.3. views.py 수정

이제 views.py에서 설문 처리와 결과 페이지를 보여주는 result 함수를 두 개로 나누어야 합니다. 하나는 설문 처리만 하는 submit 함수, 하나는 결과 페이지를 렌더링 하는 result 함수로 나눌 것입니다.
  1. 기존의 result 함수의 이름을 submit으로 바꿉니다.
    1. def submit(request): ...
       
  1. urls.py에서 submit을 요청할 URL로 submit/을 추가합니다.
    1. urlpatterns = [ ... path('submit/', views.submit), # 여기 ]
       
  1. form.html에서 <form> 태그의 action 속성의 URL을 /submit/으로 바꿉니다.
    1. <form id="form" action="/submit/" method="post">
       
  1. 결과 페이지 result.html을 렌더링할 result 함수를 새로 정의합니다.
    1. def result(request): return render(request, 'result.html')
       
  1. submit 함수의 return 값을 다음과 같이 redirect/result/로 재요청을 보내도록 합니다. (redirect를 반드시 import 해줍니다.)
    1. from django.shortcut import render, redirect def submit(request): ... return redirect('/result/')
 

1.4. URL 인자

여기서 문제가 있습니다. 결과페이지에는 결과 개발유형에 대한 정보를 context로 넘겨주었습니다. 하지만 redirect 시에는 context로 넘겨줄 수 없습니다. 대신에 URL argument를 이용합니다.
  1. urls.py에서 결과 페이지를 보여주는 URL을 다음과 같이 수정합니다.
    1. urlpatterns = [ ... path('result/<int:developer_id>/', views.result), ]
      이는 URL 패턴으로 int 타입의 developer_id라는 인자를 받을 수 있다는 뜻입니다.
       
  1. submit 함수의 redirect를 다음과 같이 수정합니다.
    1. def submit(request): ... return redirect(f'/result/{best_developer_id}')
      그러면 URL 패턴의 <int:developer_id> 자리에 best_developer_id 값이 들어가게 됩니다. 예를 들어, best_developer_id 값이 2라면 /result/2/ 라는 URL로 요청이 가게 됩니다.
       
  1. result 함수에서 <int:developer_id> 인자 값을 받으려면 똑같은 이름의 매개변수를 설정해주면 됩니다.
    1. def result(request, developer_id): ...
       
  1. 이제 developer_id를 이용해 개발자 유형을 조회하여 context로 넘겨주면 됩니다.
    1. def result(request, developer_id): developer = Developer.objects.get(pk=developer_id) context = { 'developer': developer, } return render(request, 'result.html', context=context)
      이제 아무리 결과 페이지에서 새로고침을 해도 result 함수만 호출되기 때문에 설문 참여자 수는 늘어나지 않습니다.
       

2. URL 설정 개선

2.1. URL 분리

현재 모든 URL 라우팅 설정을 MBIT/urls.py에 설정하였습니다. 하지만 프로젝트에 앱이 많아지면 URL 설정을 한곳에 모으면 관리하기 힘들어집니다. 그래서 URL 설정을 앱별로 나누어서 관리 하는 것이 좋습니다. main 앱과 관련된 URL을 main/urls.py에 설정해보겠습니다.
  1. 먼저 main 앱 폴더 아래에 urls.py 파일을 만듭니다.
    1. notion imagenotion image
       
  1. urls.py의 내용을 다음과 같이 작성합니다.
    1. from django.urls import path from . import views url_patterns = [ path('', views.index), path('form/', views.form), path('submit/', views.submit), path('result/<int:developer_id>/', views.result), ]
       
  1. MBIT/urls.py를 다음과 같이 수정합니다.
    1. from django.contrib import admin from django.urls import path, include from main import views urlpatterns = [ path('admin/', admin.site.urls), path('', include('main.urls')), ]
      [프로젝트 URL]/~로 요청이 오면 main 앱의 urls.py에서 URL 매칭을 찾으라는 설정입니다.
 

2.2. URL name

여기에서 리다이렉트를 사용하기 위해서 /result/ URL을 /submit//result/로 분리 시키기 위해서 기존의 /result/ URL을 /submit/으로 바꾸었습니다. 이때 /result/ URL을 사용하고 있는 모든 곳에서 바꾸어주어야 했습니다. 예를 들어, form.html에서 <form> 태그의 action 속성을 /result/에서 /submit/으로 바꾸어 주어야 했습니다. 만약 기존에 /result/ URL을 사용하던 곳이 수 없이 많다고 가정해 봅시다. 그 전부를 /submit/으로 바꾸는 것은 정말로 비효율적입니다. 이와 같은 상황을 우리는 '하드코딩' 이라고 표현합니다. 이를 해결할 수 있는 방법이 URL name입니다.
간단히 말하면 URL을 변수(name)에 넣어서 사용하는 겁니다. 특정 URL에 우리가 알 수 있는 변수(name)을 지정하고, 이 URL을 사용해야 하는 곳에서 URL을 직접 사용하는 대신에 이 변수(name)를 사용합니다.
  1. main/urls.py에서 각 path에 name 인자를 추가합니다.
    1. urlpatterns = [ path('', views.index, name='index'), path('form/', views.form, name='form'), path('submit/', views.submit, name='submit'), path('result/<int:developer_id>/', views.result, name='result'), ]
       
  1. main/urls.pyapp_name이라는 변수를 정의합니다.
    1. app_name = 'main' urlpatterns = [ ... ]
      app_name은 URL namespace(이름공간)으로 여러 앱들 마다 동일한 URL name을 가질 때 구별하기 위함입니다. 예를 들어, main 앱 뿐만 아니라 다른 앱에도 submit이라는 URL name이 존재하면 app_name:submit 형식으로 URL name 앞에 namespace를 붙여서 구별할 수 있습니다.
       
  1. 템플릿에서 URL을 사용한 부분들을 URL 태그 {% url '[URL name]' %}를 사용합니다.
      • index.html의 '시작하기' 버튼
        • <a href="{% url 'main:form' %}"> <button class="start" type="button">시작하기</button> </a>
       
      • form.html의 <form> 태그의 aciton 속성
        • <form id="form" action="{% url 'main:submit' %}" method="post">
       
      • result.html의 '테스트 다시 하기' 버튼
        • <a href="{% url 'main:index' %}"> <button type="button">테스트 다시 하기</button> </a>
       
  1. views.py에서 submit 함수의 경우 redirect를 사용하기 위해 URL을 사용한 적이 있습니다. 이 또한 URL name으로 표현할 수 있습니다.
    1. def submit(request): ... return redirect('main:result', developer_id=best_developer_id)
      redirectdeveloper_id라는 키워드 인자가 URL 인자 값으로 들어갑니다.
 
이제 URL을 변경하고 싶을 때 urls.py의 URL 만 변경 시키면 해당 URL name을 사용하는 곳은 자동으로 반영이 됩니다.

3. 템플릿 개선

3.1. 템플릿 이름 구별

지금은 앱이 main 하나지만, 여러개의 앱이 있을 때는 템플릿의 이름이 겹칠 수 있는 문제가 발생합니다. 예를 들어, main 외의 다른 앱들도 form.html이라는 이름의 템플릿을 가지고 싶을 수 있습니다. 하지만 똑같이 form.html이라는 이름으로 만들면 개발자들의 입장에서는 어느 앱의 form.html을 가리키는 것인지 헷갈리게 됩니다. 그래서 각 앱의 templates폴더 아래에 각 앱의 이름의 폴더를 하나 더 만들고 그 안에 템플릿 파일들을 저장합니다. 그러면 각 템플릿의 이름은 앱이름/템플릿명.html 형태가 되어서 똑같은 템플릿 명이더라도 앱이름으로 구별할 수 있게 됩니다.
  1. main 앱의 templates 폴더 아래에 main 폴더를 만들고 템플릿 파일들을 이 폴더에 옮깁니다.
    1. notion imagenotion image
       
  1. views.py에서 render 함수에 쓰인 템플릿 이름들을 수정합니다.
      • index 함수
        • def index(request): ... return render(request, 'main/index.html', context=context)
       
      • form 함수
        • def form(request): ... return render(request, 'main/form.html', context)
       
      • result 함수
        • def result(request, developer_id): ... return render(request, 'main/result.html', context=context)
 

3.2. 템플릿 상속

지금까지 작성한 템플릿들의 문제점은 반복적인 코드가 많다는 것입니다. 각 템플릿 마다 다음과 같은 공통의 코드를 가지고 있습니다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> ... <title>나의 개발 유형찾기</title> </head> <body> ... </body> </html>
이러한 경우 공통의 틀이 되는 코드는 하나의 파일에 한 번만 작성하고, 이를 필요로 하는 템플릿에서 가져다 쓸 수 있도록 만들 수 있습니다. 이를 템플릿 상속이라고 합니다.
 
  1. main/templates/main/ 폴더에 공통의 템플릿을 작성할 base.html 파일을 만들고, 다음과 같이 작성합니다.
    1. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>나의 개발 유형찾기 - final</title> {% block head %} {% endblock head %} <title>Document</title> </head> <body> {% block body %} {% endblock body %} {% block js %} {% endblock js %} </body> </html>
      base.html
      이 템플릿은 가장 큰 틀을 제공하고, 이 틀의 각 {% block [block이름] %}이라는 공간에 각 템플릿들의 고유한 코드가 들어갑니다.
       
  1. index.html을 다음과 같이 수정합니다.
    1. {% extends 'main/base.html' %} {% load static %} {% block head %} <link rel="stylesheet" type="text/css" href="{% static 'css/reset.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}"> {% endblock head %} {% block body %} <section id="main_contents"> <div class="wrapper"> ... </div> </section> {% endblock body %}
      index.html
      • {% extends 'base.html' %} : 부모 템플릿으로 base.html을 사용하겠다는 뜻입니다.
      • {% block [blockname] %} ~ {% endblock [blockname] %}
        • base.html의 각 block 부분에 삽입될 코드를 작성합니다.
           
  1. 동일한 방식으로 form.html, result.html 도 수정합니다.
    1. {% extends 'main/base.html' %} {% load static %} {% block head %} <link rel="stylesheet" type="text/css" href="{% static 'css/reset.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'css/form.css' %}"> <script src="https://code.jquery.com/jquery-3.5.1.js"></script> {% endblock head %} {% block body %} <section id="survey"> <div class="wrapper"> ... </div> </section> {% endblock body %} {% block js %} <script type="text/javascript" src="{% static 'js/form.js' %}"></script> {% endblock js %}
      form.html
       
      {% extends 'main/base.html' %} {% load static %} {% block head %} <meta property="og:title" content="나의 개발 유형은?" /> <meta property="og:image" content="" /> <meta property="og:url" content="" /> <meta property="og:description" content="나에게 꼭 맞는 개발 유형은 무엇일까?" /> <link rel="stylesheet" href="{% static 'css/reset.css' %}"> <link rel="stylesheet" href="{% static 'css/result.css' %}"> <script src="https://code.jquery.com/jquery-3.6.0.js"></script> {% endblock head %} {% block body %} <section id="main_contents"> <div class="wrapper"> ... </div> </section> {% endblock body %} {% block js %} <script type="text/javascript" src="{% static 'js/result.js' %}"></script> <script src="https://developers.kakao.com/sdk/js/kakao.js"></script> {% endblock js %}
      result.html