점프 투 장고를 참조하여 포스팅하였습니다.
https://wikidocs.net/70649
앞서 생성한 mysite 프로젝트 단독으로는 아무런 일도 할 수 없고, 프로젝트에 기능을 추가하기 위해서는 앱을 생성해야 한다. 게시판 기능을 담당할 pybo 앱을 생성하는 방법에 대해 알아보자.
명령 프롬프트에서 다음 명령어를 이용하여 pybo 앱을 생성한다.
(mysite) C:\projects\mysite> django-admin startapp pybo
(mysite) C:\projects\mysite>
파이참을 보면 다음과 같이 pybo 앱 관련한 파일과 디렉토리가 생성되었음을 확인할 수 있다.
브라우저에서 http://localhost:8000/pybo 페이지를 요청했을 때 "안녕하세요 pybo에 오신것을 환영합니다."라는 문자열을 출력하도록 만들어 보자.
(mysite) C:\projects\mysite> python manage.py runserver
URL 매핑을 추가하기 위해 config/urls.py 파일을 다음과 같이 수정한다.
from django.contrib import admin
from django.urls import path
from pybo import views # 추가해주었음
urlpatterns = [
path('admin/', admin.site.urls),
path('pybo/', views.index),
# pybo/ URL이 요청되면 views.index를 호출하라는 매핑, views.index는 views.py 파일의 index 함수를 의미
]
urlpatterns에서 실제 URL은 http://localhost:8000/pybo 이지만 호스트명과 포트가 생략된 pybo/로 매핑해야 한다. 왜냐하면 호스트(예:localhost)와 포트(예:8000)는 서버가 어떤 환경에서 실행되는지에 따라 변하기 때문이다.
또 한가지 pybo/ 를 pybo라고 하지 않고 뒤에 슬래시(/)를 하나 더 붙여 주었다. 이렇게 뒤에 슬래시를 붙여주면 브라우저 주소창에 http://localhost:8000/pybo 라고 입력해도 자동으로 http://localhost:8000/pybo/ 처럼 변환된다. 이렇게 되는 이유는 URL을 정규화하는 장고의 기능 때문이다. 특별한 경우가 아니라면 URL 매핑시 항상 끝에 슬래시를 붙여 준다.
2번에서 URL이 요청되면 views.index를 호출하라는 매핑을 추가 해주었기 때문에 view.py에 index 함수를 추가해주어야 한다.
from django.http import HttpResponse
def index(request):
return HttpResponse("안녕하세요 pybo에 오신것을 환영합니다.")
HttpResponse는 요청에 대한 응답을 할때 사용한다. 여기서는 "안녕하세요 pybo에 오신것을 환영합니다." 라는 문자열을 브라우저에 출력하기 위해 사용되었다.
브라우저에서 http://localhost:8000/pybo 페이지를 요청해보면 '안녕하세요 pybo에 오신 것을 환영합니다.' 문자열이 잘 출력된 것을 볼 수 있다.
브라우저에서 로컬 서버로 http://localhost:8000/pybo 페이지를 요청하면 urls.py 파일에서 /pybo URL 매핑을 확인하여 views.py 파일의 index 함수를 호출하고 호출한 결과를 브라우저에 반영한다.
pybo 앱에 관한 것들은 pybo 앱 디렉터리 하위에 위치해야한다. 그렇지 않으면 pybo와 관련된 URL 매핑을 추가할 때마다 config/urls.py 파일을 매번 수정해주어야 한다.
config/url.py 파일을 다음과 같이 수정해보자.
from django.contrib import admin
from django.urls import path, include
from pybo import views # 더 이상 필요하지 않으므로 삭제
urlpatterns = [
path('admin/', admin.site.urls),
path('pybo/', include('pybo.urls')),
]
pybo/ URL에 대한 매핑을 path('pybo/', views.index) 에서 path('pybo/', include('pybo.urls'))로 수정했다.
path('pybo/', include('pybo.urls'))의 의미는 pybo/로 시작하는 페이지를 요청하면 이제 pybo/urls.py 파일의 매핑 정보를 읽어서 처리하라는 의미이다. 따라서 이제 pybo/로 시작하는 URL을 추가해야 할 때 config/urls.py 파일을 수정할 필요없이 pybo/urls.py 파일만 수정하면 된다.
pybo/urls.py 파일은 다음과 같이 작성한다.
from django.urls import path
from . import views
urlpatterns = [
path('', views.index),
]
다만 path('', views.index) 처럼 pybo/ 가 생략된 '' 이 사용되었다. 이렇게 되는 이유는 config/urls.py 파일에서 이미 pybo/로 시작하는 URL이 pybo/urls.py 파일과 먼저 매핑되었기 때문이다.
즉, pybo/ URL은 config/urls.py 파일에 매핑된 pybo/ 와 pybo/urls.py 파일에 매핑된 '' 이 더해져 pybo/가 된다.
python manage.py migrate 명령을 실행하여 해당 앱들이 필요로 하는 데이터베이스 테이블들을 생성한다. admin, auth, contenttypes, sessions 앱들은 장고 프로젝트 생성시 기본적으로 설치되는 앱들로, migrate를 수행하면 admin, auth, contenttypes, sessions 앱들이 사용하는 테이블들이 생성된다.
(mysite) C:\projects\mysite>python manage.py migrate
파이보는 질문과 답변을 할 수 있는 파이썬 게시판 서비스이므로 파이보에는 질문과 답변에 해당하는 데이터 모델이 있어야 한다.
[Question 모델]
질문의 제목, 질문의 내용, 질문을 작성한 일시
[Answer 모델]
질문(어떤 질문의 답변인지 알아야하므로), 답변의 내용, 답변을 작성한 일시
위에서 생각한 속성을 바탕으로 질문(Question)과 답변(Answer)에 해당되는 모델을 pybo/models.py 파일에 정의한다.
from django.db import models
class Question(models.Model):
subject = models.CharField(max_length=200) # 최대 200자 제한 -> 글자수의 길이가 제한된 텍스트는 CharField 사용
content = models.TextField() # 내용(content)처럼 글자수를 제한할 수 없는 텍스트는 TextField를 사용
create_date = models.DateTimeField() # 작성일시처럼 날짜와 시간에 관계된 속성은 DateTimeField를 사용
class Answer(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
# Answer 모델은 질문에 대한 답변에 해당되므로 Question 모델을 속성으로 가져가야 한다.
content = models.TextField()
create_date = models.DateTimeField()
on_delete=models.CASCADE의 의미는 이 답변과 연결된 질문(Question)이 삭제될 경우 답변(Answer)도 함께 삭제된다는 의미이다.
작성한 모델을 바탕으로 테이블을 생성해볼 것이다. 테이블 생성을 위해 pybo 앱을 config/settings.py 파일의 INSTALLED_APPS 항목에 추가해야 한다. INSTALLED_APPS에 추가한 pybo.apps.PyboConfig 클래스는 pybo/apps.py 파일에 있는 클래스이다. 이 파일은 pybo 앱 생성시 자동으로 만들어지는 파일로 따로 만들 필요가 없다.
(... 생략 ...)
INSTALLED_APPS = [
'pybo.apps.PyboConfig',
'django.contrib.admin',
'django.contrib.auth',
(... 생략 ...)
]
(... 생략 ...)
모델이 신규로 생성되거나 변경되면 makemigrations 명령을 먼저 수행한 후에 migrate 명령을 수행해야 한다.
python manage.py makemigrations
makemigrations 명령은 모델을 생성하거나 모델에 변화가 있을 경우에 실행해야 하는 명령이다. 위 명령을 수행하면 pybo\migrations\0001_initial.py 라는 파이썬 파일이 자동으로 생성된다.
migrate 명령을 수행하여 실제 테이블을 생성한다.
python manage.py migrate
python manage.py shell
일반적인 파이썬 셸을 실행하는 것이 아니라 python manage.py shell 처럼 장고 셸을 실행해야 함에 주의해야 한다. 장고 셸은 장고에 필요한 환경들이 자동으로 설정되어 실행된다.
Question과 Answer 모델은 장고 셸에서 다음처럼 import하여 사용할 수 있다.
from pybo.models import Question, Answer
Question 모델을 이용하여 질문 데이터 만들기
>>> from django.utils import timezone
>>> q = Question(subject='pybo가 무엇인가요?', content='pybo에 대해서 알고 싶습니다.', create_date=timezone.now())
>>> q.save()
위처럼 Question 모델의 객체 q를 생성한 후 save 함수를 실행하면 질문 데이터가 1건 생성된다.
데이터가 1건 생성되면 반드시 다음처럼 id 값이 생성된다.
>>> q.id
1
id는 모델 데이터의 유일한 값으로 프라이머리 키(PK)라고도 한다. 이 id 값은 데이터를 생성할 때마다 1씩 증가된다.
[두 번째 질문 생성]
>>> q = Question(subject='장고 모델 질문입니다.', content='id는 자동으로 생성되나요?', create_date=timezone.now())
>>> q.save()
>>> q.id
2
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>, <Question: Question object (2)>]>
Question.objects를 사용하여 저장한 질문을 조회할 수 있다. Question.objects.all()은 모든 Question 데이터를 조회하는 함수이고, 결과값으로는 QuerySet 객체가 리턴되는데 위처럼 Question 객체를 포함한다.
Question object (1), Question object (2) 에서 1과 2는 Question 데이터의 id 값이다.
다음처럼 Question 모델에 str 메서드를 추가하면 id 값 대신 제목을 표시할 수 있다.
(... 생략 ...)
class Question(models.Model):
subject = models.CharField(max_length=200)
content = models.TextField()
create_date = models.DateTimeField()
def __str__(self):
return self.subject
(... 생략 ...)
모델이 변경되었으므로 장고 셸을 재시작해야 한다. 장고 셸을 종료하기 위해서는 장고 셸에서 Ctrl+Z 또는 quit()을 입력하면 된다.
위 코드로 수정한 후 실행해보면 다음과 같은 결과를 얻을 수 있다.
(mysite) c:\projects\mysite>python manage.py shell
>>> from pybo.models import Question, Answer
>>> Question.objects.all()
<QuerySet [<Question: pybo가 무엇인가요?>, <Question: 장고 모델 질문입니다.>]>
>>>
모델에 메서드가 추가될 경우에는 makemigrations와 migrate를 수행할 필요가 없다. makemigrations, migrate 명령이 필요한 경우는 모델의 속성이 변경되었을 경우이다.
>>> Question.objects.filter(id=1)
<QuerySet [<Question: pybo가 무엇인가요?>]>
filter는 조건에 해당되는 데이터를 모두 리턴해 주기 때문에 다건을 의미하는 QuerySet이 리턴된다.
>>> Question.objects.get(id=1)
<Question: pybo가 무엇인가요?>
get으로 조회할 경우 QuerySet이 아닌 Question 모델 객체가 리턴되었다. filter는 다건을 리턴하지만 get은 한 건만 리턴하기 때문이다. 보통 get은 id와 같은 유일한 값으로 조회할 경우에만 사용한다.
>>> Question.objects.filter(subject__contains='장고')
<QuerySet [<Question: 장고 모델 질문입니다.>]>
>>> q = Question.objects.get(id=2)
>>> q
<Question: 장고 모델 질문입니다.>
>>> q.subject = 'Django Model Question'
>>> q.save()
>>> q
<Question: Django Model Question>
반드시 save를 수행해주어야지 변경된 데이터가 반영된다.
>>> q = Question.objects.get(id=1)
>>> q.delete()
(1, {'pybo.Question': 1})
삭제될 때는 위와 같이 추가 정보가 리턴되는데 (1, {'pybo.Question': 1})은 Question 모델이 1개 삭제되었음을 의미한다.
>>> q = Question.objects.get(id=2)
>>> q
<Question: Django Model Question>
>>> from django.utils import timezone
>>> a = Answer(question=q, content='네 자동으로 생성됩니다.', create_date=timezone.now())
>>> a.save()
>>> a.id
1
>>> a = Answer.objects.get(id=1)
>>> a
<Answer: Answer object (1)>
>>> a.question
<Question: Django Model Question>
>>> q.answer_set.all()
<QuerySet [<Answer: Answer object (1)>]>
Question 모델에는 answer_set 이라는 속성이 없지만 Answer 모델에 Question 모델이 ForignKey로 연결되어 있기 때문에 q.answer_set 과 같은 역방향 접근이 가능하다.
장고의 admin.py로 할 수 있는 다양한 기능을 살펴볼 것이다.
장고 관리자를 사용하기 위해서는 장고 관리자 화면에 접속할 수 있는 슈퍼유저(superuser)를 먼저 생성해야 한다.
python manage.py createsuperuser
슈퍼유저가 생성되었으니 로컬 서버를 구동한 후 http://localhost:8000/admin/에 접속하여 로그인하면 다음과 같은 화면이 나온다.
admin.py 파일에 admin.site.register 명령어로 Question 모델을 등록하면 다음과 같이 Question 모델이 추가된 것을 볼 수 있다. 이제 관리자 화면에서 Question 모델을 관리할 수 있는 것이다. Question 모델의 "+ 추가" 링크를 클릭해보면 다음처럼 Question을 신규로 생성할 수 있는 화면이 나타난다. 신규 질문을 생성할 수도 있고 조회, 수정, 삭제도 가능하다.
from django.contrib import admin
from .models import Question
admin.site.register(Question)
Answer 모델도 동일한 방법으로 등록하면 Question 모델과 마찬가지로 장고 관리자에서 사용할 수 있다.
관리자 화면에서 제목(subject)으로 질문 데이터를 검색할 수 있다. pybo/admin.py 파일을 다음과 같이 수정해야 한다.
from django.contrib import admin
from .models import Question
class QuestionAdmin(admin.ModelAdmin): # QuestionAdmin 클래스를 생성
search_fields = ['subject'] # 제목 검색을 위해 search_fields 속성에 subject 추가
admin.site.register(Question, QuestionAdmin)
위와 같이 검색 화면이 생성된 것을 확인할 수 있다. 검색어를 입력하고 검색 버튼을 클릭하면 검색어가 포함된 Question 데이터가 조회된다.
http://localhost:8000/pybo/ 페이지 요청시 등록한 질문들을 조회할 수 있도록 pybo/views.py 파일의 index 함수를 다음과 같이 변경해준다.
from django.shortcuts import render
from .models import Question
def index(request):
question_list = Question.objects.order_by('-create_date')
context = {'question_list': question_list}
return render(request, 'pybo/question_list.html', context)
질문 목록은 Question.objects 함수로 얻을 수 있고, order_by('-create_date')를 사용하면 작성일시 역순으로 정렬해준다. '-' 기호가 붙어 있으면 역방향, 없으면 순방향 정렬을 의미한다.
render 함수는 파이썬 데이터를 템플릿에 적용하여 HTML로 반환하는 함수이다. 즉, 질문 목록으로 조회한 question_list 데이터를 pybo/question_list.html 파일에 적용하여 HTML을 생성한 후 리턴한다.
render 함수에서 사용한 pybo/question_list.html 템플릿 파일을 만들어야 한다. pybo/question_list.html과 같은 파일을 템플릿(Template)이라고 부른다. 템플릿 파일은 HTML 파일과 비슷하지만 파이썬 데이터를 읽어서 사용할수 있는 HTML 파일이다.
파일을 생성하기 전 템플릿 파일을 저장할 디렉터리를 먼저 만들어야 하므로 config/settings.py 파일의 TEMPLATES 항목에 다음과 같이 설정해준다.
(... 생략 ...)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
(... 생략 ...)
DIRS는 템플릿 디렉터리를 여러개 등록할 수 있도록 리스트로 되어있다. 파이보는 BASE_DIR / 'templates' 디렉터리 한 개만 등록한다. BASE_DIR / 'templates'에서 BASE_DIR은c:\projects\mysite 이므로 추가한 디렉터리의 전체 경로는 다음과 같다.
c:\projects\mysite\templates
현재 이 디렉토리는 없으므로 mkdir을 사용하여 생성해주어야 한다.
(mysite) c:\projects\mysite> mkdir templates
하나의 웹 사이트에서 여러 앱을 사용할 때, 여러 앱의 화면을 구성하는 템플릿은 한 디렉터리에 모아 관리하는 편이 편리하므로 앱(App) 디렉터리 하위에 템플릿 디렉터리를 두는 방법이 아닌 앞으로 공통으로 사용하는 템플릿은 projects/mysite/templates에 저장하고, pybo 앱은 템플릿 디렉터리로 /projects/mysite/templates/pybo 디렉터리를 사용할 것이다.
projects/mysite/templates/pybo/question_list.html와 같은 디렉토리로 question_list.html 파일을 생성해준 후 다음과 같이 작성한다.
{% if question_list %}
<ul>
{% for question in question_list %}
<li><a href="/pybo/{{ question.id }}/">{{ question.subject }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>질문이 없습니다.</p>
{% endif %}
위와 같이 {% %}로 둘러싸인 문장들은 템플릿 태그라고 한다.
{% if 조건문1 %}
<p>조건문1에 해당되는 경우</p>
{% elif 조건문2 %}
<p>조건문2에 해당되는 경우</p>
{% else %}
<p>조건문1, 2에 모두 해당되지 않는 경우</p>
{% endif %}
마지막에 {% endif %}로 닫아주어야 한다.
{% for item in list %}
<p>순서: {{ forloop.counter }} </p>
<p>{{ item }}</p>
{% endfor %}
마지막에 {% endfor %}로 닫아주어야 한다.
질문 목록을 선택했을 때 내용이 잘 나타나도록 하기 위해 URL을 매핑해야 한다.
질문 목록 화면에서 링크를 클릭하여 요청한 질문 URL은 다음과 같다.
http://localhost:8000/pybo/2/
=> id값이 2인 Question을 상세 조회한다.
이 동작이 잘 수행되도록 pybo/urls.py 파일을 수정해야한다.
from django.urls import path
from . import views
urlpatterns = [
path('', views.index),
path('<int:question_id>/', views.detail),
]
path('<int:question_id>/', views.detail) 라는 URL 매핑을 추가했다. 이제 http://localhost:8000/pybo/2/ 페이지가 요청되면 여기에 등록한 매핑 룰에 의해 http://localhost:8000/pybo/<int:question_id>/ 가 적용되어 question_id 에 2가 저장되고 views.detail 함수도 실행될 것이다. <int:question_id> 에서 int는 숫자가 매핑됨을 의미한다.
이제 views.detail 함수를 만들어야 한다. pybo/views.py 파일에 detail 함수를 추가한다.
def detail(request, question_id):
question = Question.objects.get(id=question_id)
context = {'question': question}
return render(request, 'pybo/question_detail.html', context)
detail 함수에서 사용할 pybo/question_detail.html 템플릿을 다음처럼 작성한다.
<h1>{{ question.subject }}</h1>
<div>
{{ question.content }}
</div>
{{ question.subject }}과 {{ question.content }}에는 질문 제목고 내용이 매핑되고 질문은 h1태그로 인해 크게 html에 표시된다.
http://localhost:8000/pybo/2/ 페이지를 요청해 보면 오류가 나지 않는 것을 확인할 수 있다.
없는 페이지를 요청했을 때 404 에러가 뜨도록 detail 함수를 수정한다.
from django.shortcuts import render, get_object_or_404
from .models import Question
(... 생략 ...)
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)
수정 후 http://localhost:8000/pybo/30/ 페이지를 요청해보면 404 오류 페이지가 출력되는 것을 확인할 수 있다.
템플릿에 사용된 URL의 하드코딩을 없애는 방법에 대해서 알아볼 것이다.
question_list.html 템플릿에 사용된 링크는 다음과 같다.
{{ question.subject }}
하지만 이는 http://localhost:8000/pybo/question/2 또는 http://localhost:8000/pybo/2/question 처럼 변경될 수 있다.
오류가 발생할 때마다 수정하지 않으려면 해당 URL에 대한 실제 링크 대신 링크의 주소가 1:1 매핑되어 있는 별칭을 사용해야 한다.
링크의 주소 대신 별칭을 사용하기 위해 URL 매핑에 name 속성을 부여한다. pybo/urls.py 파일을 다음과 같이 수정한다.
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name='detail'),
]
http://localhost:8000/pybo/ URL은 index, http://localhost:8000/pybo/2와 같은 URL에는 detail 이라는 별칭을 부여한 것이다.
pybo/urls.py 파일에 별칭을 추가하면 템플릿에서 다음과 같이 사용할 수 있다.
{% if question_list %}
<ul>
{% for question in question_list %}
<li><a href="{% url 'detail' question.id %}">{{ question.subject }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>질문이 없습니다.</p>
{% endif %}
하드코딩 되어 있던 /pybo/{{ question.id }} 링크를 {% url 'detail' question.id %}로 변경했다. 여기서 question.id는 URL 매핑에 정의된 <int:question_id>에 전달해야 하는 값을 의미한다.
pybo 앱 이외의 다른 앱이 프로젝트에 추가될 경우, 서로 다른 앱에서 동일한 URL 별칭을 사용하면 중복이 발생할 것이다.
이 문제를 해결하려면 pybo/urls.py 파일에 네임스페이스를 의미하는 app_name 변수를 지정해야한다. 다음처럼 pybo/urls.py 파일에 app_name을 추가한다.
from django.urls import path
from . import views
app_name = 'pybo'
urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name='detail'),
]
오류가 나지 않게 하기 위해서는 템플릿에서 사용한 URL 별칭에 detail 앞에 pybo 라는 네임스페이스를 붙여 다음과 같이 지정한다.
{% if question_list %}
<ul>
{% for question in question_list %}
<li><a href="{% url 'pybo:detail' question.id %}">{{ question.subject }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>질문이 없습니다.</p>
{% endif %}