장고는 “빠르게, 안전하게, 확장 가능하게” 웹 서비스를 만들 수 있도록 돕는 파이썬 웹 프레임워크로 ORM, 인증, 관리자(admin), 폼, 템플릿, 라우팅 등 웹앱의 필수 부품을 기본 탑재하고, 재사용 가능한 앱 구조로 대규모 개발에도 유리하다.

config/ # 프로젝트 설정(전역)
├─ settings.py # 환경설정
├─ urls.py # 전역 URL 라우팅
├─ wsgi.py/asgi.py # 배포/비동기 엔트리
blog/ # 앱(기능 단위)
├─ apps.py # 앱 설정
├─ models.py # 데이터 모델(ORM)
├─ views.py # 뷰(비즈니스 로직)
├─ urls.py # 앱 라우팅
├─ admin.py # 관리자 사이트 등록
├─ templates/ # 템플릿(HTML)
├─ migrations/ # 마이그레이션 파일
├─ tests.py # 단위 테스트
manage.py # 관리 커맨드 엔트리
# 1) 설치
pip install django
# 2) 프로젝트 생성
django-admin startproject config .
# 3) 앱 생성 (예: blog)
python manage.py startapp blog
# 4) DB 준비 & 슈퍼유저 생성
python manage.py migrate
python manage.py createsuperuser
# 5) 개발 서버 실행
python manage.py runserver
앱 등록 – config/settings.py
INSTALLED_APPS = [
# Django 기본 앱들...
'blog', # 생성한 앱 등록
]
apps.py — 앱 설정# blog/apps.py
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
verbose_name = "블로그"
models.py — 데이터 모델(ORM)# blog/models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Post(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
is_published = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
마이그레이션:
python manage.py makemigrations
python manage.py migrate
admin.py — 관리자 등록# blog/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ("id", "title", "author", "is_published", "created_at")
search_fields = ("title", "body")
list_filter = ("is_published", "created_at")
→ /admin 접속 후 슈퍼유저로 로그인하면 CRUD UI 자동 제공.
views.py — 뷰(로직)# blog/views.py
from django.views.generic import ListView, DetailView
from .models import Post
class PostListView(ListView):
model = Post
queryset = Post.objects.filter(is_published=True)
template_name = "blog/post_list.html"
context_object_name = "posts"
class PostDetailView(DetailView):
model = Post
template_name = "blog/post_detail.html"
urls.py — 라우팅# config/urls.py (전역)
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls")),
]
# blog/urls.py (앱)
from django.urls import path
from .views import post_list, post_detail, PostListView, PostDetailView
urlpatterns = [
path("", PostListView.as_view(), name="post_list"),
path("posts/<int:pk>/", PostDetailView.as_view(), name="post_detail"),
# 함수형으로 쓰려면 아래처럼:
# path("", post_list, name="post_list"),
# path("posts/<int:pk>/", post_detail, name="post_detail"),
]
templates/<!-- blog/templates/blog/post_list.html -->
{% extends "base.html" %}
{% block content %}
<h1>게시글</h1>
<ul>
{% for post in posts %}
<li><a href="{% url 'post_detail' post.pk %}">{{ post.title }}</a> — {{ post.author }}</li>
{% empty %}
<li>게시글이 없습니다.</li>
{% endfor %}
</ul>
{% endblock %}
<!-- blog/templates/blog/post_detail.html -->
{% extends "base.html" %}
{% block content %}
<h1>{{ post.title }}</h1>
<p>{{ post.body|linebreaks }}</p>
<small>{{ post.created_at }}</small>
{% endblock %}
<!-- templates/base.html (프로젝트 공통 레이아웃) -->
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>{% block title %}My Blog{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}">
</head>
<body>
<header><a href="{% url 'post_list' %}">홈</a></header>
<main>{% block content %}{% endblock %}</main>
</body>
</html>
템플릿 경로 설정 – config/settings.py
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"], # 프로젝트 공통 템플릿 폴더
"APP_DIRS": True, # 각 앱 내부 templates/ 사용
"OPTIONS": {"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
# ...
]},
},
]
# settings.py
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"] # 개발용
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media" # 업로드 파일 저장 경로
템플릿에서:
{% load static %}
<link rel="stylesheet" href="{% static 'css/main.css' %}">
forms.py# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ("title", "body", "is_published")
# blog/views.py (폼 사용 예)
from django.shortcuts import redirect
from .forms import PostForm
def post_create(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect("post_detail", pk=post.pk)
else:
form = PostForm()
return render(request, "blog/post_form.html", {"form": form})
# views.py
from django.contrib.auth.decorators import login_required
@login_required
def secret_page(request):
return render(request, "secret.html")
# settings.py
LOGIN_URL = "/accounts/login/"
LOGIN_REDIRECT_URL = "/"
tests.py# blog/tests.py
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from .models import Post
class PostTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user("u", "u@test.com", "pw")
self.post = Post.objects.create(title="t", body="b", author=self.user)
def test_list(self):
res = self.client.get(reverse("post_list"))
self.assertEqual(res.status_code, 200)
self.assertContains(res, "t")
def test_detail(self):
res = self.client.get(reverse("post_detail", args=[self.post.pk]))
self.assertEqual(res.status_code, 200)
self.assertContains(res, "b")