Django Template·Form과 CRUD
템플릿·CSRF·ModelForm·POST/Redirect/GET으로 명단 등록과 수정 흐름 만들기
이번 문서에서 만드는 것
4주의 config 프로젝트와 lions 앱에서 이어갑니다. 5주에는 목록과 등록 Form, 6주에는 수정·삭제를 완성합니다. 이번 단계는 가상 데이터만 사용하는 로컬 CRUD입니다. 로그인과 소유자 권한은 선택 확장에서 다루며 그 전에는 서비스를 공개하지 않습니다.
HTML form이 보낸 문자열을 검증해 유효한 데이터만 저장합니다. 성공 뒤에는 목록으로 이동합니다. 이 흐름을 중심으로 실습합니다. ORM을 모두 배우기 전에 최소 모델 하나를 사용하고 7–9주에 관리자·조회·관계를 깊게 다룹니다.
5주: 모델과 입력 폼
lions/models.py를 작성합니다. track은 선택지 밖의 입력을 Form에서 거절하도록 정의합니다.
from django.db import models
class Member(models.Model): class Track(models.TextChoices): FRONTEND = "frontend", "프론트엔드" DESIGN = "design", "기획·디자인" BACKEND = "backend", "백엔드"
name = models.CharField(max_length=40) track = models.CharField(max_length=10, choices=Track.choices)
def __str__(self): return self.namepython manage.py makemigrations lionspython manage.py migratemakemigrations는 모델 변경 이력 파일을 만들고 migrate는 DB에 적용합니다. 생성된 migration 파일도 코드와 함께 커밋합니다. 오류가 난다고 DB나 기존 migration을 지우지 않습니다.
새 파일 lions/forms.py를 만듭니다.
from django import formsfrom .models import Member
class MemberForm(forms.ModelForm): class Meta: model = Member fields = ["name", "track"] labels = {"name": "이름", "track": "트랙"}fields를 명시하면 나중에 모델에 관리자용 필드가 추가되어도 입력에 자동 노출되지 않습니다. ModelForm은 기본적으로 문자열 양끝 공백을 제거하고 필수 값·최대 길이·선택지를 검사합니다. 브라우저의 required만으로 서버 검증을 대신할 수 없습니다.
기존 member_api는 유지하고 lions/views.py에 추가합니다.
from django.shortcuts import get_object_or_404, redirect, renderfrom django.views.decorators.http import require_http_methods, require_POSTfrom .forms import MemberFormfrom .models import Member
@require_GETdef member_list(request): members = Member.objects.order_by("id") return render(request, "lions/member_list.html", {"members": members})
@require_http_methods(["GET", "POST"])def member_create(request): form = MemberForm(request.POST if request.method == "POST" else None) if request.method == "POST" and form.is_valid(): form.save() return redirect("lions:member-list") return render(request, "lions/member_form.html", {"form": form})@require_GET는 4주 파일에서 이미 import했습니다. lions/urls.py의 urlpatterns에 두 줄을 추가합니다.
path("members/", views.member_list, name="member-list"),path("members/new/", views.member_create, name="member-create"),lions/templates/lions/member_form.html을 만듭니다. csrf_token은 Django의 기본 CSRF 보호와 함께 작동하며 삭제하면 정상 POST가 403으로 거절됩니다.
<!doctype html><html lang="ko"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>회원 입력</title><main> <h1>회원 입력</h1> <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit">저장</button> </form> <a href="{% url 'lions:member-list' %}">목록으로</a></main></html>lions/templates/lions/member_list.html을 만듭니다.
<!doctype html><html lang="ko"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>회원 명단</title><main> <h1>회원 명단</h1> <a href="{% url 'lions:member-create' %}">회원 등록</a> <ul> {% for member in members %} <li>{{ member.name }} · {{ member.get_track_display }}</li> {% empty %} <li>아직 등록된 회원이 없습니다.</li> {% endfor %} </ul></main></html>/members/에서 등록 링크를 열어 가상 회원을 저장합니다. 성공하면 POST 응답이 redirect를 반환하고 브라우저는 GET으로 목록을 다시 엽니다. 이 흐름을 POST/Redirect/GET이라고 합니다. 새로고침으로 같은 POST가 반복되는 일을 줄이지만 네트워크 중복 요청 전체를 막는 장치는 아닙니다.
5주 권장 90분: 모델·migration 20분, Form·Template 35분, 정상·오류 입력 20분, 설명 15분. 제출물은 빈 목록·정상 등록·공백 이름 오류 화면입니다. 서버가 꺼졌다 켜져도 저장한 회원이 남아야 합니다.
6주: 수정과 삭제
lions/views.py에 추가합니다. 수정할 때 instance를 전달하지 않으면 새 행을 만들게 됩니다.
@require_http_methods(["GET", "POST"])def member_update(request, pk): member = get_object_or_404(Member, pk=pk) form = MemberForm( request.POST if request.method == "POST" else None, instance=member, ) if request.method == "POST" and form.is_valid(): form.save() return redirect("lions:member-list") return render(request, "lions/member_form.html", {"form": form})
@require_POSTdef member_delete(request, pk): member = get_object_or_404(Member, pk=pk) member.delete() return redirect("lions:member-list")lions/urls.py의 urlpatterns에 추가합니다.
path("members/<int:pk>/edit/", views.member_update, name="member-update"),path("members/<int:pk>/delete/", views.member_delete, name="member-delete"),목록 템플릿의 기존 <li>...</li>를 아래로 바꿉니다. 삭제는 GET 링크가 아닌 CSRF가 있는 POST form입니다. 실제 서비스에서는 삭제 확인과 권한을 추가해야 합니다.
<li> {{ member.name }} · {{ member.get_track_display }} <a href="{% url 'lions:member-update' member.pk %}">수정</a> <form method="post" action="{% url 'lions:member-delete' member.pk %}"> {% csrf_token %} <button type="submit">{{ member.name }} 삭제</button> </form></li>회귀를 잡는 테스트
기존 lions/tests.py에 추가합니다. 아래 테스트는 일반 기능 검사이며 Django 기본 테스트 클라이언트는 CSRF 검사를 생략합니다. CSRF 자체는 Client(enforce_csrf_checks=True)를 쓰거나 브라우저의 실제 Form 요청으로 따로 검사합니다.
from .models import Member
class MemberFormTests(TestCase): def test_invalid_name_is_not_saved(self): response = self.client.post(reverse("lions:member-create"), {"name": " ", "track": "frontend"}) self.assertEqual(response.status_code, 200) self.assertEqual(Member.objects.count(), 0) self.assertIn("name", response.context["form"].errors)
def test_update_does_not_insert(self): member = Member.objects.create(name="가람", track="frontend") url = reverse("lions:member-update", args=[member.pk]) self.client.post(url, {"name": "나래", "track": "design"}) member.refresh_from_db() self.assertEqual(member.name, "나래") self.assertEqual(Member.objects.count(), 1)
def test_get_cannot_delete(self): member = Member.objects.create(name="가람", track="frontend") response = self.client.get(reverse("lions:member-delete", args=[member.pk])) self.assertEqual(response.status_code, 405) self.assertTrue(Member.objects.filter(pk=member.pk).exists())6주 권장 90분: 수정 25분, 삭제 20분, 오류·테스트 30분, 리뷰 15분. 완료 기준은 등록·수정·삭제 후 실제 DB 상태를 검사하고 없는 ID는 404, GET 삭제는 405, 공백 이름은 폼 오류로 처리하는 것입니다.
문제 해결과 다음 확장
TemplateDoesNotExist: 앱 등록과templates/lions/경로, render의 이름을 비교합니다.NoReverseMatch: URL의 name·namespace·필요한 pk를 확인합니다.- 잘못된 폼에서 입력이 사라짐: POST 데이터로 만든 bound form을 그대로 다시 render하는지 확인합니다.
- 저장 후 행이 늘어남: 수정 Form의
instance를 확인합니다.
다음 문서에서 관리자 검색, ORM 조회와 과제·제출 관계를 추가합니다.
연결된 PBL 미션과 VOD
| 주차·미션 | 참고 VOD 범위 |
|---|---|
| 5주 · Template와 Form | Python 첫걸음 13·14장 |
| 6주 · CRUD | Python 첫걸음 13·14장 |
5–10주는 동일한 Django 구조·CRUD VOD 범위를 반복 참조한다. 주차별 산출물은 서로 다르므로 문서 실습을 순서대로 확장한다.
강좌 안내: 참고 강좌 1. 이 문서는 영상 전체를 옮긴 전사 자료가 아닙니다. 세부 내용은 위 공식 문서에서 확인합니다.