장고 중급 #5 메시지 / 세션 / 쿠키

6 분 소요

HTTP는 stateless입니다. 한 요청과 다음 요청이 서로를 모르는 게 기본. 그 위에 “같은 사용자” 라는 개념을 얹기 위해 세 가지 도구가 있습니다.

  • 메시지 — 한 요청에서 다음 요청으로 잠깐 전달 (flash)
  • 세션 — 한 사용자의 여러 요청에 걸친 상태
  • 쿠키 — 위 두 가지를 가능하게 하는 가장 밑단의 매체

이 글은 위에서 아래로 — 메시지부터 시작해 쿠키 보안까지 한 호흡에 봅니다.

#3에서 본 MessageMiddleware, SessionMiddleware가 동작의 근간입니다.

메시지 — flash 패턴 #

**“저장되었습니다”**같이 다음 페이지로 넘어가서 한 번만 보여주는 알림. 이걸 직접 만들면 세션을 만지는 코드를 매번 적어야 하는데, 장고는 messages 프레임워크로 풀어줍니다.

셋업 (보통 자동) #

startproject로 만든 프로젝트는 이미 들어 있습니다.

settings.py — 확인
INSTALLED_APPS = [
    ...
    "django.contrib.messages",
]

MIDDLEWARE = [
    ...
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
]

TEMPLATES = [{
    ...
    "OPTIONS": {
        "context_processors": [
            ...
            "django.contrib.messages.context_processors.messages",
        ],
    },
}]

뷰에서 메시지 추가 #

blog/views.py
from django.contrib import messages
from django.shortcuts import redirect

def post_create(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save()
            messages.success(request, "글이 저장되었습니다.")
            return redirect("post_detail", pk=post.pk)
        messages.error(request, "입력값을 다시 확인해주세요.")
    else:
        form = PostForm()
    return render(request, "blog/post_form.html", {"form": form})

레벨별 헬퍼:

함수레벨쓰임
messages.debugDEBUG개발 디버깅
messages.infoINFO단순 안내
messages.successSUCCESS성공 알림
messages.warningWARNING주의
messages.errorERROR실패

DEBUG는 기본적으로 표시되지 않습니다. MESSAGE_LEVEL = messages.DEBUG로 켤 수 있습니다.

CBV에서 — SuccessMessageMixin #

CBV
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.edit import CreateView

class PostCreateView(SuccessMessageMixin, CreateView):
    model = Post
    form_class = PostForm
    success_message = "%(title)s 글이 저장되었습니다."

%(field)s 형태로 모델 필드를 메시지에 끼워 넣습니다. (#1 CBV의 Mixin 패턴.)

템플릿에서 렌더 #

base.html — 한곳에서 모든 메시지
{% if messages %}
  <ul class="messages">
    {% for message in messages %}
      <li class="message message--{{ message.tags }}">
        {{ message }}
      </li>
    {% endfor %}
  </ul>
{% endif %}
  • message.tagssuccess, error 등 (CSS 클래스로 활용)
  • 한 번 렌더되면 자동으로 사라집니다 — 다음 요청엔 다시 보이지 않음

메시지 백엔드 #

기본 백엔드는 FallbackStorage — 세션을 시도하고 안 되면 쿠키로 폴백합니다.

settings.py — 백엔드 변경 가능
MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"
# 또는 "...cookie.CookieStorage"

쿠키 백엔드는 헤더가 커질 수 있고, 세션 백엔드는 미들웨어 순서가 중요. 기본 FallbackStorage가 보통 답 입니다.

Session — 여러 요청에 걸친 상태 #

세션은 사용자별로 서버 측에 보관하는 dict 같은 저장소입니다. 클라이언트는 세션 ID만 쿠키로 가지고, 실제 데이터는 서버에 있습니다 (기본 백엔드의 경우).

기본 사용 #

블로그 — 최근 본 글
def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)

    recent = request.session.get("recent_posts", [])
    if pk in recent:
        recent.remove(pk)
    recent.insert(0, pk)
    recent = recent[:10]
    request.session["recent_posts"] = recent

    return render(request, "blog/post_detail.html", {"post": post})

API는 보통의 dict와 비슷합니다.

세션 API
request.session["key"] = "value"        # 저장
request.session.get("key")              # 조회 (없으면 None)
request.session.get("key", default)     # 기본값
del request.session["key"]              # 삭제
"key" in request.session                # 존재 검사

request.session.flush()                  # 세션 전체 비우고 새 세션 키 생성
request.session.cycle_key()              # 키만 새로 (데이터 유지) — 권한 변경 후 권장

세션 만료 — set_expiry #

만료 제어
request.session.set_expiry(60 * 60)     # 1시간 (초)
request.session.set_expiry(0)           # 브라우저 닫으면 만료
request.session.set_expiry(None)        # 기본값 (settings.SESSION_COOKIE_AGE)

기본 만료는 SESSION_COOKIE_AGE = 1209600 (2주). “로그인 유지” 체크박스 같은 경우엔 set_expiry(0) 또는 긴 값으로 분기.

세션 백엔드 — 어디에 저장하나 #

settings.py
SESSION_ENGINE = "django.contrib.sessions.backends.db"  # 기본
백엔드저장 위치쓰임
db (기본)DB (django_session 테이블)거의 모든 경우 — 기본값으로 잘 동작
cache캐시 (Redis 등)매우 빠름. 캐시 비우면 사라짐 (주의)
cached_db캐시 → DB 폴백DB의 영속성 + 캐시의 속도
file파일 시스템단일 서버, 작은 트래픽
signed_cookies쿠키 자체에 (서명)서버 stateless. 데이터 양 제한, 매 요청마다 전송

운영에서 보통 답: db 또는 cached_db. Redis가 이미 있다면 cache 또는 cached_db로 가속.

signed_cookies는 서버를 완전히 stateless 하게 만들 수 있지만, 모든 요청에 세션 데이터가 전송되니 데이터를 작게 유지해야 합니다. 비밀 정보를 넣으면 안 됩니다 (서명만 되어 있을 뿐 암호화 X).

만료된 세션 청소 #

db 백엔드는 만료된 행이 자동으로 사라지지 않습니다. 주기적으로 정리:

크론으로 주기 실행
python manage.py clearsessions

쿠키 — 가장 밑단 #

세션 ID, CSRF 토큰, 로케일 같은 작은 값은 쿠키로 직접 다루기도 합니다.

쿠키 읽기/쓰기 #

쿠키 직접 다루기
def view(request):
    visited = request.COOKIES.get("visited", "0")
    visited = str(int(visited) + 1)

    response = render(request, "page.html", {"visited": visited})
    response.set_cookie(
        "visited",
        visited,
        max_age=60 * 60 * 24 * 30,   # 30일
        httponly=True,
        secure=True,
        samesite="Lax",
    )
    return response

def logout_view(request):
    response = redirect("home")
    response.delete_cookie("visited")
    return response

request.COOKIESdict 형태로 모든 쿠키가 들어 있는 읽기 전용 객체입니다. 쓰기는 응답 객체에.

쿠키 보안 — HttpOnly / Secure / SameSite #

쿠키의 보안 속성 셋은 반드시 알아야 하는 기본 입니다.

HttpOnly #

Set-Cookie: sessionid=abc; HttpOnly

JavaScript의 document.cookie로 읽을 수 없도록 막습니다. XSS가 일어나도 세션 쿠키를 도난당하지 않습니다.

세션 쿠키, 인증 쿠키는 반드시 HttpOnly. 장고의 SESSION_COOKIE_HTTPONLYCSRF_COOKIE_HTTPONLY의 기본값:

기본의미
SESSION_COOKIE_HTTPONLYTrue세션 쿠키는 JS 접근 차단
CSRF_COOKIE_HTTPONLYFalseJS가 토큰을 헤더에 넣어 보내야 하므로 읽을 수 있어야 함

CSRF 쿠키만 예외인 이유 — fetch 요청에 X-CSRFToken 헤더로 토큰을 넣어 보내야 하기 때문입니다.

Secure #

Set-Cookie: sessionid=abc; Secure

HTTPS 연결에서만 전송됩니다. HTTP로는 쿠키가 안 갑니다. 운영에선 무조건 켜야 합니다.

settings.py — 운영
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True              # HTTP → HTTPS 자동 리다이렉트
SECURE_HSTS_SECONDS = 31536000          # 1년 HSTS
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

SecurityMiddleware (#3)가 이 설정들을 강제합니다. 자세한 운영 보안은 고급 #7.

SameSite #

쿠키가 다른 도메인에서 시작된 요청에 같이 전송될지를 제어합니다. CSRF의 가장 강한 방어선.

의미
Strict같은 사이트의 요청에만 (외부 링크 클릭 시도 안 보냄)
Lax기본 권장. GET 같은 안전한 탑레벨 네비게이션엔 보냄, POST 등엔 안 보냄
None모든 요청에 (Secure도 같이 켜야 함)
settings.py
SESSION_COOKIE_SAMESITE = "Lax"     # 기본
CSRF_COOKIE_SAMESITE = "Lax"

Strict는 외부 링크로 사이트에 들어왔을 때 로그인이 풀려 보일 수 있어 UX가 어색합니다. Lax가 보통 답입니다.

CSRF와 쿠키의 관계 — 짧게 #

CSRF (Cross-Site Request Forgery) — 다른 사이트가 사용자의 인증 쿠키를 편승해서 요청을 위조하는 공격.

장고의 방어:

  1. 폼에 {% csrf_token %} → 숨김 input으로 토큰 삽입
  2. 쿠키에 같은 토큰 저장 (CSRF 쿠키)
  3. POST/PUT/DELETE 요청에서 두 값을 비교 (CsrfViewMiddleware)
  4. 쿠키와 폼 값이 다르면 403 반환

JavaScript fetch 라면 X-CSRFToken 헤더로 토큰을 같이 보냅니다.

fetch + CSRF 토큰
function getCookie(name) {
  const m = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
  return m ? m[2] : null;
}

await fetch("/api/posts/", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-CSRFToken": getCookie("csrftoken"),
  },
  body: JSON.stringify(data),
});

SameSite=Lax가 켜져 있으면 단순 POST 위조는 이미 막힙니다. CSRF 토큰은 이중 방어 수단입니다.

작은 실전 — 비로그인 장바구니 #

세션을 쓰는 흔한 경우 — 로그인 안 한 사용자의 장바구니를 세션에 임시로 보관하는 패턴입니다.

cart/views.py
def add_to_cart(request, product_id):
    cart = request.session.get("cart", {})
    cart[str(product_id)] = cart.get(str(product_id), 0) + 1
    request.session["cart"] = cart
    request.session.modified = True   # dict 내부 변경은 자동 감지 안 됨!
    return redirect("cart_detail")

def cart_detail(request):
    cart = request.session.get("cart", {})
    products = Product.objects.filter(pk__in=cart.keys())
    items = [(p, cart[str(p.pk)]) for p in products]
    return render(request, "cart/detail.html", {"items": items})

함정 한 줄: dict 내부의 값을 바꿨을 때 장고가 자동으로 감지하지 못합니다. request.session.modified = True로 명시해야 저장됩니다. 또는 위 코드처럼 cart 자체를 다시 request.session["cart"] = cart로 대입하는 방법도 있습니다.

정리 #

이번 글에서 잡은 것:

  • 메시지messages.success/info/warning/error/debug, 템플릿 {% for message in messages %}
  • CBV는 SuccessMessageMixin
  • 메시지 백엔드 — 기본 FallbackStorage가 보통 답
  • 세션request.session["key"], get, flush, cycle_key, set_expiry
  • 세션 백엔드: db (기본), cache, cached_db, file, signed_cookies
  • 만료된 세션 청소: python manage.py clearsessions
  • 쿠키request.COOKIES, response.set_cookie, response.delete_cookie
  • 보안 속성:
    • HttpOnly — JS 접근 차단 (세션 쿠키엔 필수)
    • Secure — HTTPS에서만 전송 (운영 필수)
    • SameSite=Lax — CSRF 강한 방어선
  • 운영 설정: SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURE, SECURE_SSL_REDIRECT, HSTS
  • CSRF 토큰 + 쿠키 비교, fetch는 X-CSRFToken 헤더
  • 세션의 dict 내부 변경은 request.session.modified = True

다음 글(#6 Static/Media 운영)에서는 기초 #5에서 첫인사한 정적 파일을, 운영 관점으로 다시 봅니다 — collectstatic, MEDIA_*, S3 / WhiteNoise / Storage 백엔드까지 다룹니다.

X