권태형 저자님의 '백엔드를 위한 DJANGO REST FRAMEWORK with 파이썬'을 기반으로 정리한 글입니다.
Chapter 3: Django로 Todo 목록 웹 서비스 만들기
1. Todo 목록 웹 서비스 시작하기
프로젝트 생성하기

Todo 프로젝트 설정하기
settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'todo',
]
TIME_ZONE = 'Asia/Seoul'
그리고 이후 개발에 필요한 파일들을 미리 만들어놓는다.
- todo 앱 폴더 안에 templates 폴더를 만들고 그 안에 다시 todo 폴더 생성
- todo 앱 폴더 안에 forms.py, models.py, urls.py 생성
마지막으로 관리자 계정을 생성한다.

Django가 내부 데이터베이스 테이블을 만들지 못한 상태에서 createsuperuser 명령을 실행하면 오류가 발생한다. 때문에 해당 오류 방지를 위해 migration을 적용해 Django가 필요한 모든 DB 테이블을 자동으로 생성할 수 있도록 해야 한다.
Todo 모델 생성하기
프로젝트의 첫 단계는 언제나 모델을 생성하는 것이다.
현재 필요한 필드를 가진 모델을 구현한 결과는 다음과 같다.
from django.db import models
class Todo(models.Model):
title = models.CharField(max_length=100)
description = models.TextField(blank=True)
created = models.DateTimeField(auto_now_add=True)
completed = models.BooleanField(default=False)
important = models.BooleanField(default=False)
def __str__(self):
return self.title
이제 아래 코드를 통해 모델에 대한 마이그레이션을 진행하면 모델 생성이 완료된다.
python manage.py makemigrations
python manage.py migrate
그리고나서 관리자 페이지에서 모델을 확인할 수 있도록, todo/admin.py에서 Todo 모델을 등록한다.
from django.contrib import admin
from .models import Todo
admin.site.register(Todo)
관리자 페이지에 접속하려면 url이 설정되어야 하기 때문에, mytodo/urls.py에서 path가 아래처럼 잘 설정되어 있는지 확인한다.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
]
2. Todo 전체 조회 기능 만들기
Todo 전체 조회 기능 컨셉
첫 번째 페이지인 Todo 전체 조회 기능에서는 완료되지 않은, 즉 진행 중인 Todo만 보여주고 완료된 Todo는 다른 페이지에서 보여주도록 한다.
Bootstrap으로 좀 더 멋진 템플릿 만들기
Bootstrap은 가장 유명하고 오래된 CSS 프레임워크이다. 미리 스타일이 정의되어 있으며, 사용법이 간단하고 정갈한 결과물이 만들어지기 때문에 지금처럼 간단하게 꾸미고 싶은 경우 유용하게 활용할 수 있다.
Template
todo_list.html
<html>
<head>
<title>TODO 목록 앱</title>
<Link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
/>
<Link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/boot-strap-icons.css" />
</head>
<body>
<div class="container">
<h1>TODO 목록 앱</h1>
<p>
<a href=""><i class="bi-plus"></i>Add Todo</a>
<a href="" class="btn btn-primary" style="float:right">완료한 TODO목록</a>
</p>
<ul class="list-group">
{% for todo in todos %}
<li class="list-group-item">
<a href="">{{todo.title}}</a>
{% if todo.important %}
<span class="badge badge-danger">!</span>
{% endif %}
<div style="float:right">
<a href="" class="btn btn-danger">완료</a>
<a href="" class="btn btn-outline-primary">수정하기</a>
</div>
</li>
{% endfor %}
</ul>
</div>
</body>
</html>
Bootstrap 사용 법
1. <head> 안에 <link> 태그가 있고 여기에서 Bootstrap을 불러온다.
Bootstrap을 사용하기 위한 방법은 두 가지, 첫 번째 방법은 Bootstrap 홈페이지에서 css 파일들을 받아와 프로젝트 폴더에 집어넣는 방식이고, 두 번째 방식은 웹 링크 형태로 제공되는 css 파일을 가져다 쓰도록 참조시키는 방식이다.
성능 면에서는 첫 번째 방법이 당연히 더 좋겠지만 간단히 적용할 땐 웹 링크 방식이 편리하다. 그리고 이런 방식을 cdn(content delivery network) 방식이라고 한다.
2. 각 태그마다 class를 작성한다.
Bootstrap은 결국 미리 작성된 CSS이므로, CSS의 기본 작성 단위인 class를 따르고 있다. class를 쓰면 여러 태그를 한꺼번에 스타일링 가능한데, 이러한 class를 통해 Bootstrap에서 작성한 스타일을 각 태그마다 적용하고 있으며 이 class 이름은 Bootstrap이 미리 정의해놓은 값이다.
<a></a> vs <link />
- <a>: 사용자가 클릭하는 링크
- <link>: 브라우저가 참고하는 외부 리소스 연결
<link>의 rel 속성은 현재 문서와 외부 리소스 간의 관계(relationship)를 나타내는 속성이다.
- stylesheet: 외부 css 파일을 연결
list 태그 예시
<ul>
<li>사과</li>
<li>바나나</li>
<li>딸기</li>
</ul>
기타 태그 및 css 속성
- <i></i>: icon 삽입시 사용하는 태그
- <span></span>: inline으로 특정 부분만 스타일이나 클래스를 적용할 때 쓰는 태그
- style="float: right": 요소를 오른쪽으로 띄워 배치하고, 다른 내용이 왼쪽으로 감싸지게 하는 CSS 속성
※ HTML Tag auto close 적용 안 될시, vscode 하단에 언어가 HTML로 잘 설정되어 있는지 확인하고 안 되어 있으면 HTML로 설정해야 한다.
View
완료되지 않은 Todo만 전달해야 하기 때문에 completed=False 옵션으로 필터링한다.
from django.shortcuts import render, redirect
from .models import Todo
def todo_list(request):
todos = Todo.objects.filter(completed=False)
return render(request, 'todo/todo_list.html', {'todos': todos})
URL
메인 페이지에서 Todo 목록을 보여줄 수 있도록 todo/urls.py를 아래와 같이 작성한다.
from django.urls import path
from . import views
urlpatterns = [
path('', views.todo_list, name='todo_list'),
]
또한, todo 앱 url을 프로젝트 url에 연결해야 하기에, mytodo/urls.py에 아래와 같이 작성한다.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('todo/', include('todo.urls')),
]
3. Todo 상세 조회 기능 만들기
Todo 상세 조회 기능 컨셉
Todo를 선택했을 때 조회할 수 있는 기능으로, Todo 제목과 설명을 나타내도록 구현한다.
Template
todo_detail.html
<html>
<head>
<title>TODO 목록 앱</title>
<Link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
/>
<Link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/boot-strap-icons.css" />
</head>
<body>
<div class="container">
<h1>TODO 상세보기</h1>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{todo.title}}</h5>
<p class="card-text">{{todo.description}}</p>
<a href="{% url 'todo_list' %}" class="btn btn-primary">목록으로</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
View
def todo_detail(request, pk):
todo = Todo.objects.get(id=pk)
return render(request, 'todo/todo_detail.html', {'todo': todo})
선택된 Todo의 pk인 id를 기반으로 Todo 객체를 찾아 todo_detail.html로 전달할 수 있도록 위와 같이 작성하고, todo_list.html의 버튼에 링크를 넣는다.
<a href="{% url 'todo_detail' pk=todo.pk %}">{{todo.title}}</a>
URL
url은 todo/urls.py에서 /pk/로 설정하여 해당 Todo를 연결할 수 있도록 한다.
urlpatterns = [
path('', views.todo_list, name='todo_list'),
path('<int:pk>/', views.todo_detail, name='todo_detail'),
]
4. Todo 생성 기능 만들기
Todo 생성 기능 컨셉
todo/forms.py에 제목, 설명, 중요도를 입력할 수 있도록 입력 폼을 만들어 주어야 한다.
Template
todo/forms.py
from django import forms
from .models import Todo
class TodoForm(forms.ModelForm):
class Meta:
model = Todo
fields = ('title', 'description', 'important')
이렇게 작성한 폼을 활용하여 템플릿을 작성한다.
todo_post.html
<html>
<head>
<title>TODO 목록 앱</title>
<Link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
/>
<Link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/boot-strap-icons.css" />
</head>
<body>
<div class="container">
<h1>TODO 추가하기</h1>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<form method="POST">
{% csrf_token %} {{form.as_p}}
<button type="submit" class="btn btn-primary">등록</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
View
Todo를 생성하기 위한 뷰는 POST 요청을 처리할 수 있어야 한다. POST 요청이 들어왔을 때는 폼을 검증하고 데이터를 저장할 수 있도록 하며, GET 요청이 들어왔을 때는 폼을 포함한 템플릿 페이지를 보여주면 된다.
from django.shortcuts import render, redirect
from .models import Todo
from .forms import TodoForm
def todo_list(request):
todos = Todo.objects.filter(completed=False)
return render(request, 'todo/todo_list.html', {'todos': todos})
def todo_detail(request, pk):
todo = Todo.objects.get(id=pk)
return render(request, 'todo/todo_detail.html', {'todo': todo})
def todo_post(request):
if request.method == "POST":
form = TodoForm(request.POST)
if form.is_valid():
todo = form.save(commit=False)
todo.save()
return redirect('todo_list')
else:
form = TodoForm()
return render(request, 'todo/todo_post.html', {'form': form})
그 후, todo_list.html의 버튼에 링크를 넣어준다.
<a href="{% url 'todo_post' %}"><i class="bi-plus"></i>Add Todo</a>
URL
urlpatterns = [
path('', views.todo_list, name='todo_list'),
path('<int:pk>/', views.todo_detail, name='todo_detail'),
path('post/', views.todo_post, name='todo_post'),
]
5. Todo 수정 기능 만들기
Todo 수정 기능 컨셉
수정 기능을 만들 때는 생성 기능과 동일하게 작성하되, 기존 Todo 데이터를 불러와 함게 템플릿으로 보내는 것만 구현하면 된다.
View
def todo_post(request):
if request.method == "POST":
form = TodoForm(request.POST)
if form.is_valid():
todo = form.save(commit=False)
todo.save()
return redirect('todo_list')
else:
form = TodoForm()
return render(request, 'todo/todo_post.html', {'form': form})
def todo_edit(request, pk):
todo = Todo.objects.get(id=pk)
if request.method == "POST":
form = TodoForm(request.POST, instance=todo)
if form.is_valid():
todo = form.save(commit=False)
todo.save()
return redirect('todo_list')
else:
form = TodoForm(instance=todo)
return render(request, 'todo/todo_post.html', {'form': form})
그리고 todo_list.html의 버튼에 링크를 다음과 같이 추가한다.
<a href="{% url 'todo_post' %}"><i class="bi-plus"></i>Add Todo</a>
<a href="{% url 'todo_edit' pk=todo.pk %}" class="btn btn-outline-primary">수정하기</a>
URL
urlpatterns = [
path('', views.todo_list, name='todo_list'),
path('<int:pk>/', views.todo_detail, name='todo_detail'),
path('post/', views.todo_post, name='todo_post'),
path('<int:pk>/edit/', views.todo_edit, name='todo_edit'),
]
6. Todo 완료 기능 만들기
Todo 완료 기능 컨셉
Todo 완료 기능은 완료 버튼을 눌렀을 때 Todo의 completed을 True로 설정해 주는 기능이다. 또한 완료 Todo 조회 기능은 완료된 Todo만 필터링하여 볼 수 있도록 하는 기능이다.
Template
done_list.html
<html>
<head>
<title>TODO 목록 앱</title>
<Link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
/>
<Link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/boot-strap-icons.css" />
</head>
<body>
<div class="container">
<h1>DONE 목록</h1>
<p>
<a href="{% url 'todo_list' %}" class="btn btn-outline-primary">홈으로</a>
</p>
<ul class="list-group">
{% for done in dones %}
<li class="list-group-item">
<a href="{% url 'todo_detail' pk=done.pk %}">{{done.title}}</a>
{% if done.important %}
<span class="badge badge-danger">!</span>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</body>
</html>
그리고나서 todo_list.html의 버튼에 링크를 추가한다.
<a href="{% url 'done_list' %}" class="btn btn-primary" style="float:right">완료한 TODO 목록</a>
View
Todo 완료 기능에서 필요한 뷰는 두 가지로, 완료된 목록을 보여주는 기능과 Todo를 완료로 바꿔주는 기능이 있다.
완료된 목록을 보여주는 것은 앞서 todo_list() 뷰와 동일하게 작성되며 completed=True로 필터링 하는 것만 다르다. 그리고 Todo를 완료로 바꾸기 위해서는 Todo 객체를 가져와 completed을 True로 설정한 다음 저장하면 된다.
def done_list(request):
dones = Todo.objects.filter(completed=True)
return render(request, 'todo/done_list.html', {'dones': dones})
def todo_done(request, pk):
todo = Todo.objects.get(id=pk)
todo.completed = True
todo.save()
return redirect('todo_list')
마찬가지로, todo_list.html 내 버튼에 링크를 추가해 준다.
<a href="{% url 'todo_done' pk=todo.pk %}" class="btn btn-danger">완료</a>
URL
마지막으로 url을 연결한다.
urlpatterns = [
path('', views.todo_list, name='todo_list'),
path('<int:pk>/', views.todo_detail, name='todo_detail'),
path('post/', views.todo_post, name='todo_post'),
path('<int:pk>/edit/', views.todo_edit, name='todo_edit'),
path('done/', views.done_list, name='done_list'),
path('done/<int:pk>/', views.todo_done, name='todo_done'),
]
※ Django에서 pk와 id는 기본 키를 가리키며, 기본 키가 나중에 id가 아닌 다른 필드로 바뀌어도 pk는 항상 작동하므로, URL이나 템플릿에서는 pk로 통일하는 것이 안전하다.

'Web > Django' 카테고리의 다른 글
| ForeignKey와 OneToOneField (0) | 2025.11.19 |
|---|---|
| Django로 웹 개발하기: photo_list (1) | 2025.10.29 |