Django(장고) 웹 프레임워크 구조
참고 https://docs.djangoproject.com
장고는 웹 사이트를 설계할 때 애플리케이션 프로그램들이 모여서 프로젝트를 개발하는 개념이다.
우리가 흔히 아는 네이버, 당근 마켓 앱 등은 장고에서 프로젝트로 불릴 것이다.
- 프로젝트(Project): 웹 사이트 자체(당근마켓 App 그 자체)
- 애플리케이션(App): 프로젝트 내의 모듈화된 단위 프로그램(당근마켓 앱 내의 로그인 기능, 게시판 기능 등)
MVT 패턴
웹 개발 시 언급되는 MVC패턴은 데이터(Model), 사용자 인터페이스(View), 데이터 처리 로직(Controller)에 대한 코딩을 분리해서 독립적으로 개발 할 수 있는 설계 방식이다.
장고 프레임워크에서 View는 Template, Controller는 View라고 표현하며, MVC 대신 MVT패턴이라고 부른다.
- Model: 데이터 베이스에 저장되는 데이터와 관련된 부분 정의
- View: 프로그램 로직이 동작하여 데이터를 가공, 처리한 결과를 Template에 전달
- Template: 사용자에게 보여지는 UI 부분
- 클라이언트의 요청을 받으면 최상위(프로젝트) URLconf(URL과 View를 매핑해놓은 집합) 로딩
- urlpatterns 변수에 지정되어 있는 URL 리스트 검사
- 위에서 부터 순서대로 URL 리스트의 내용을 검사하면서 URL 패턴이 매치되면 검사 종료
- 요청된 URL과 매치된 URL의 View 호출
- 담당 View는 로직을 실행하여, DB 처리가 필요하면 Model을 통해 처리하고 결과 반환
- View는 로직 처리가 끝나면 Template를 사용해 클라이언트에 전송할 HTML 파일 생성
- View는 최종 결과로 HTML파일을 클라이언트에게 응답
1. Model - 데이터베이스 정의
모델은 사용될 데이터에 대한 정의를 담고 있는 장고의 클래스이다.
장고는 ORM기법을 사용하는데 ORM은 객체와 관계형 데이터베이스를 연결(매핑)해주는 역할을 한다. 데이터베이스에 접근하기 위해 SQL 언어를 사용하지 않고도 데이터베이스를 조작할 수 있다. 즉, 하나의 모델 클래스는 하나의 테이블에 매핑되고, 모델 클래스의 속성은 테이블의 컬럼과 연결된다.
장고에서 Model은 [각 Django App 안]에 기본적으로 생성되는 models.py 모듈 안에 정의하게 된다. model.py 파일에 정의한 Person 모델은 장고 내부적으로 SQL명령을 사용하여 다음과 같은 데이터 테이블을 생성한다.
장고에서는 테이블을 하나의 클래스로 정의하고, 테이블의 컬럼은 클래스의 변수(속성)로 매핑한다.
🔎 데이터베이스 변경사항 반영
1) python manage.py makemigrations(데이터베이스 변경사항 지시서)
2) python manage.py migrate(데이터베이스에 변경사항 반영)
2. URL 정의
클라이언트로부터 URL 요청을 받으면, 장고는 urls.py 파일에 정의된 URL 패턴과 매칭되는지 분석한다. urls.py 파일에 URL과 처리 함수(View)를 연결(매핑)하는 코드를 작성하는데 이러한 URl과 View의 매핑 집합을 URLconf라고 한다.
1) 실제 프로젝트 내 URL 설계와 작동 원리
- 요청받은 URL은 가장 먼저 최상위 프로젝트 폴더 내의 urls.py로 접근, URL 패턴 매칭 확인
- 최상위 URLconf의 urlpatterns 리스트 변수에 path 메서드로 URL 패턴 지정
- URL 패턴이 매칭되면 호출할 View 지정
🔎 요청받은 URL이 어떻게 프로젝트 내의 urls.py로 접근이 가능한 것일까?
장고는 URL 분석시, project명/settings.py 파일에 ROOT_URLCONF 항목에 정의된 urls.py 파일을 가장 먼저 분석한다.
# django_project/settings.py
ROOT_URLCONF = 'django_project.urls'
ROOT_URLCONF를 통해 아래의 urls.py로 이동
# django_project/urls.py
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
장고는 프로젝트를 생성하면, 기본적으로 관리자 페이지(admin)를 제공하기 때문에 프로젝트 내의 urls.py를 보면 위와 같은 코드가 미리 작성되어 있다. 요청된 URL이 .../admin/이면 path('admin/', admin.site.urls)에 매칭이 되고, 장고에서 제공해주는 admin 앱의 site가 호출된다.
2) 개별 애플리케이션마다 URL 분리, 계층적 URL 패턴 설계
URLconf 모듈을 개별 애플리케이션 단위로 계층적인 URL 패턴을 설계할 수 있다. 계층적으로 URL 패턴을 설계하면, 패턴의 변경이 쉬워지고 하위 URLconf를 재사용할 수 있는 장점이 있다.
3) 최상위 ROOT_URLconf와 분리하는 방법
먼저 app의 urls를 가져오기 위해 from django.urls import에 include 함수를 추가하고, path에서 요청 받은 url을 하위의 URLconf 에게 처리를 위임한다.
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('myapp/', include('myapp.urls')), # myapp의 하위 URLconf로 url 처리 위임
]
최상위 URLconf에서 넘겨받은 'myapp/'은 myapp 애플리케이션 내의 URLconf에서 처리된다.
# django_project/myapp/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.landing),
path('<int:post_id>/', views.detail),
]
4) 템플릿에서 하드코딩된 URL 제거하기: URL 패턴 이름 사용
{% for post in posts %}
<li><a href="/myapp/{{ post.id }}">{{ post.title }}</a></li>
{% endfor %}
위의 URL 링크는 URL주소가 변경될 때마다 템플릿에서 사용한 모든 URL들을 일일이 찾아가며 수정해야하는 어려움이 있다. 이러한 문제점을 해결하고자 URL의 실제 link 대신 link의 주소가 매핑되어 있는 이름을 사용해야 한다.
1. 링크 주소 대신 이름을 사용하려면 URL패턴 이름에 name 속성을 부여하면 된다.
# django_project/myapp/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.landing, name='landing'),
path('<int:post_id>/', views.detail, name='detail'),
]
2. 템플릿에서 URL패턴 이름 사용
{% for post in posts %}
<li><a href="{% url 'detail' post.id %}">{{ post.title }}</a></li>
{% endfor %}
cf. 만약 뷰의 URL을 myapp/detail_list/12/로 바꾸고 싶다면, 이제는 템플릿 각각에서 모두 바꿔줄 필요없이 myapp/urls.py에서 path에 datail_list만 추가하면 된다.
# django_project/myapp/urls.py
urlpatterns = [
path('detail_list/<int:post_id>/', views.detail, name='detail'),
]
5) URL의 이름 공간(namespace) 정하기: app명/urls.py의 app_name 변수
지금까지 프로젝트는 myapp이라는 앱 하나만 만들었지만, 실제 Django 프로젝트는 수많은 앱으로 이루어져 있다.
예를 들어, myapp 앱의 URL패턴 이름과 blog 앱의 URL패턴 이름이 모두 detail이 되는 경우가 발생한다. 이 둘을 구별하기 위해 app_name 변수로 URL 패턴의 이름이 서로 충돌하는 것을 방지하기 위한 이름 공간이 필요한 것이다.
app명/urls.py 파일에 app_name을 추가하여 어플리케이션의 이름공간을 설정한다.
# django_project/myapp/urls.py
app_name = 'myapp'
urlpatterns = [
path('detail_list/<int:post_id>/', views.detail, name='detail'),
]
템플릿에서도 'url패턴 이름' 에서 'app_name:url패턴명' 형태로 이름공간을 추가해준다.
{% for post in posts %}
<li><a href="{% url 'myapp:detail' post.id %}">{{ post.title }}</a></li>
{% endfor %}
3. View - 로직 정의
URL에 매핑된 뷰가 호출되면 뷰는 해당 애플리케이션의 로직에 맞는 처리를 하고, 그 결과를 HTML로 변환하기 위해 템플릿 처리를 한 후, 최종 HTML로 된 응답 데이터를 웹 클라이언트한테 반환하는 역할을 한다.
장고에서의 뷰는 [해당 App 내]의 view.py 파일에 클래스의 메소드나 함수 형태로 작성되며, 웹 요청을 받고 응답을 반환해준다. 응답은 HTML 데이터일 수도 있고, 리다이렉션 명령일 수도 있고, 404 에러 메세지일 수도 있다. 즉, 다양한 형태의 응답 데이터를 만들어내기 위한 로직을 뷰에 작성하는 것이다.
🔎 현재 날짜와 시간을 알려달라는 요청이 오면?
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It is now %s.</body></html>" % now
return HttpResponse(html)
🔎 반환하는 객체가 HttpResponse가 아닌, HttpResponseRedirect 객체는 무엇일까?
# detail.html
<form actions="{% url 'polls:vote' question.id %}" method="post">
# urls.py
path('polls/<int:question_id>/vote/', views.vote, name='vote')
# POST 데이터를 처리하는 views.py
from django.http import HttpResponseRedirect
def vote(request, question_id):
....
# 폼 데이터를 처리한 후, 그 결과를 보여주는 페이지로 리다이렉트 시켜준다.
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
템플릿에 있는 폼을 제출하면, 폼의 기능에 의해 /polls/5/vote/와 같은 URL이 POST 방식으로 넘어오고 urls.py에서 URL에 맞는 처리 함수(View)를 연결(매핑)해준다. 이때 vote() 함수의 파라미터로 question_id 인자를 넘겨 받는다.
HttpResponseRedirect 클래스의 생성자는 리다이렉트할 타겟 URL을 인자로 받는데, 타겟 URL은 reverse() 함수로 만든다. reverse() 함수는 무엇을 의미할까? 일반적으로 URL로 이에 매핑되는 view를 호출하는데, reverse()는 이와 반대 방향으로 view에서 URL을 호출한다. 그래서 reverse() 함수는 URLconf에 이미 정의된 URL 패턴명('polls:results')을 활용해서 URL 스트링('/polls/3/results/')을 추출하게 된다.
# views.py
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
# urls.py
path('polls/<int:question_id>/results/', views.results, name='results')
myapp/urls.py 에서 'about_me/' 요청을 받으면, 아래 views.py 파일에 정의한 about_me 함수가 실행이 된다.
# django_project/myapp/views.py
from django.shortcuts import render
# render함수 내의 'myapp/about_me.html'은 url 주소가 아니라,
# myapp/templates/myapp/about_me.html에 저장된 html문서를 불러오게 된다.
def about_me(request):
return render(request, 'myapp/about_me.html')
render() 함수는 request 객체와 템플릿 이름은 필수 인자로 받고, 세번째 인자인 context 사전형 객체는 선택적으로 받을 수 있다. render() 함수를 통해 템플릿에 context를 채워 넣어 표현한 결과를 HttpResponse 객체와 함께 반환한다. View에서 템플릿에 전달할 데이터가 있으면 세번째 인자로 Dictionary를 전달하는 것이다. 즉, 템플릿 내부에서 템플릿 언어를 통해 이 딕셔너리 타입 데이터의 키를 호출하여 데이터를 가져올 수 있다.
render와 redirect 함수를 헷갈릴 수 있는데, redirect는 단지 URL로 이동하는 것이다. render는 말 그대로 템플릿을 렌더링(HTML 파일로 처리한 결과를 응답으로 전달)하는 것이고, redirect는 이동한 URL에 맞는 view를 실행하게 된다. 더 엄밀히 말해 장고에서의 렌더링은 템플릿 코드를 템플릿 파일(html, xml, json)로 해석하는 과정을 말한다.
# 모델에 구현한 Post 테이블에 접근
from .models import Post
from django.shortcuts import render
def post_list(request):
posts = Post.objects.all()
context = {
'posts_box': posts,
}
return render(request, 'blog/post_list.html', context)
ex. blog/post_list.html
context의 키 값인 posts_box로 posts 테이블 호출
{% for p in posts_box %}
<li>{{ p.title }}</li>
{% endfor %}
4. Template - 화면 UI 정의
Template은 View로부터 전달받은 데이터를 템플릿에 적용하여 HTML을 만드는데 사용된다.
Template은 HTML 파일로서 Django 개발 가이드라인은 App폴더/templates/App명/템플릿 파일처럼, 각 App 폴더 밑에 templates 서브 폴더를 만들고 다시 그 안에 App명을 사용하여 서브 폴더를 만든 후 템플릿 파일을 그 안에 넣기를 권장한다. (ex. my_django 프로젝트/blog_app1/templates/blog_app1/index.html)
그 이유는 복수의 App들이 동일한 이름의 템플릿을 가진 경우, View에서 잘못된 템플릿을 가져올 수 있기 때문이다.
예를 들면 App1에 create.html이 있고 App2에도 동일한 create.html 템플릿이 있는 경우, App2의 View에서 create.html를 지정했을 때 처음 App1의 create.html을 사용하게 된다. 이는 템플릿을 찾을 때 자신의 App 내의 템플릿을 먼저 찾는 것이 아니라, 전체 App들의 템플릿 폴더들을 처음부터 순서대로 찾기 때문이다. View에서 'App2/create.html' 과 같이 템플릿명을 지정하면 이런 혼동은 없어진다.
템플릿은 물론 순수하게 HTML로만 쓰여진 정적(Static) HTML 파일일 수 있지만, 거의 대부분의 경우 View로부터 어떤 데이터를 전달 받아 HTML 템플릿 안에서 전달받은 데이터를 동적으로 처리해서 사용한다.
🔎 View에서 전달 받은 데이터를 가져오려면 딕셔너리의 키를 호출해야 하는데 Python 언어를 알아듣지 못하는 HTML은 어떻게 키 값을 호출할까?
프로젝트 내의 settings.py 파일에서 TEMPLATES의 'BACKEND'를 보면, 장고에 내장된 장고 템플릿 언어가 적용된 것을 볼 수 있다. 이를 통해 HTML 템플릿이 Python 문법을 이해할 수 있게 된다.
# django_project/settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
...
이제 HTML 템플릿에서는 장고가 지원해주는 템플릿 언어를 사용하여 View가 넘겨준 데이터를 전달 받을 수 있다.
# 템블릿 변수: {{ 변수명 }}
<body>
<h1>{{ posts_box }}</h1>
</body>
# 템플릿 태그(로직 구현): {% %}
{% for item in dataList %}
<li>{{ item }}</li>
{% endfor %}