쿠키(Cookie)란 무엇인가?
쿠키는 클라이언트 브라우저에 저장되는 키-값 쌍(key-value pair) 데이터입니다. HTTP 프로토콜은 본질적으로 상태를 유지하지 않기 때문에(stateless), 사용자가 로그인에 성공했을 때 서버는 클라이언트 브라우저에 쿠키를 전송합니다. 이후 클라이언트가 다시 서버에 접속할 때 이 쿠키를 함께 보내면, 서버는 사용자를 식별하여 재로그인 절차 없이 서비스를 제공할 수 있습니다.
쿠키 관리: 생성, 조회, 수정, 삭제
쿠키 조회
Django 뷰(view)에서 클라이언트가 보낸 쿠키는 request.COOKIES 딕셔너리를 통해 접근할 수 있습니다. 특정 쿠키에 안전하게 접근하려면 request.get_signed_cookie() 메서드를 사용하는 것이 좋습니다. 이 메서드는 쿠키 값에 대한 무결성을 검증하며, 필요한 경우 보안을 강화하기 위한 salt 값과 만료 시간을 설정할 수 있습니다.
# 특정 쿠키 값 직접 접근
cookie_value = request.COOKIES.get('my_cookie')
# 서명된 쿠키 값 안전하게 조회 (보안 강화)
# max_age: 쿠키 만료 시간 (초 단위)
# salt: 서명 생성 시 사용된 솔트 값
# default: 쿠키가 없을 경우 반환할 기본값
signed_cookie_value = request.get_signed_cookie('secure_cookie', default=None, salt='my_secret_salt', max_age=3600)
쿠키 설정
HTTP 응답 객체(HttpResponse 또는 render 함수의 반환값)의 set_cookie() 또는 set_signed_cookie() 메서드를 사용하여 쿠키를 클라이언트에 설정할 수 있습니다. 다양한 옵션을 통해 쿠키의 동작을 제어할 수 있습니다.
from django.http import HttpResponse
def set_my_cookie(request):
response = HttpResponse("쿠키가 설정되었습니다.")
# 일반 쿠키 설정
response.set_cookie('username', 'guest', max_age=3600*24*7) # 7일 동안 유효
# 서명된 쿠키 설정 (보안 강화)
response.set_signed_cookie(
'user_id',
'12345',
salt='user_session',
expires='2024-12-31T23:59:59', # 특정 만료 시간 설정 (IE 호환성)
path='/', # 쿠키 적용 경로 (루트 경로)
domain='.example.com', # 쿠키 적용 도메인
secure=True, # HTTPS 통신에서만 쿠키 전송
httponly=True # JavaScript에서 쿠키 접근 차단 (보안 강화)
)
return response
# JavaScript에서 쿠키 설정 예시 (jQuery 사용)
# $.cookie("list_pager_num", 30, { path: '/' });
쿠키 옵션 설명:
key: 쿠키의 이름value: 쿠키의 값max_age: 쿠키의 최대 유효 시간 (초 단위)expires: 쿠키의 만료 시점 (datetime 객체 또는 특정 형식의 문자열)path: 쿠키가 유효한 경로. '/'는 모든 경로에서 유효함을 의미합니다.domain: 쿠키가 유효한 도메인.secure:True로 설정하면 HTTPS 연결에서만 쿠키가 전송됩니다.httponly:True로 설정하면 JavaScript를 통한 쿠키 접근이 차단됩니다. (완벽한 차단은 아니며, 패킷 스니핑 등으로 접근 가능)
쿠키의 역할 및 장점
쿠키는 상태 비저장(stateless)인 HTTP 통신에서 사용자 세션 관리를 돕는 핵심적인 역할을 합니다. 로그인 유지, 사용자 설정 저장 등에 활용됩니다.
세션(Session)이란 무엇인가?
세션은 사용자별로 서버 측에 저장되는 데이터 묶음입니다. 각 사용자는 고유한 세션 ID를 가지며, 이 ID를 통해 서버는 사용자의 상태 정보를 유지합니다. 예를 들어, 로그인한 사용자의 ID, 닉네임 등의 정보를 서버에 저장하여 요청마다 전달하는 번거로움을 줄일 수 있습니다.
세션 데이터는 일반적으로 딕셔너리 형태로 관리되며, 각 세션 ID는 해당 사용자의 데이터에 매핑됩니다. 다음과 같은 형태를 가집니다:
# 예시: 세션 데이터 구조
{
"a1b2c3d4e5f6...": {'user_id': 1, 'username': 'alice', 'email': 'alice@example.com'},
"f6e5d4c3b2a1...": {'user_id': 2, 'username': 'bob', 'email': 'bob@example.com'}
}
세션 설정
Django는 기본적으로 세션 기능을 지원하며, settings.py 파일을 통해 세션 관련 설정을 구성할 수 있습니다.
세션 저장소 엔진
Django는 다양한 세션 저장소 백엔드를 지원합니다. 기본값은 데이터베이스를 사용하는 db 엔진입니다.
SESSION_ENGINE = 'django.contrib.sessions.backends.db': (기본값) 데이터를 데이터베이스의django_session테이블에 저장합니다.SESSION_ENGINE = 'django.contrib.sessions.backends.cache': 데이터를 캐시 시스템(Memcached, Redis 등)에 저장합니다.SESSION_CACHE_ALIAS설정을 통해 사용할 캐시 백엔드를 지정합니다.SESSION_ENGINE = 'django.contrib.sessions.backends.file': 세션 데이터를 파일 시스템에 저장합니다.SESSION_FILE_PATH로 저장 경로를 지정할 수 있습니다.SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db': 캐시와 데이터베이스를 함께 사용합니다. 데이터는 캐시에 우선 저장되고, 캐시에서 누락되면 데이터베이스에서 조회합니다. 성능과 데이터 지속성을 모두 고려한 방식입니다.SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies': 세션 데이터를 암호화하여 클라이언트 쿠키에 직접 저장합니다. 이는 클라이언트가 모든 세션 데이터를 가지고 있으므로 민감한 정보 저장에는 적합하지 않습니다.
주요 설정 항목 (settings.py)
# 세션 쿠키의 이름 (브라우저에 저장되는 쿠키 이름)
SESSION_COOKIE_NAME = "sessionid"
# 세션 쿠키가 유효한 경로
SESSION_COOKIE_PATH = "/"
# 세션 쿠키가 유효한 도메인
SESSION_COOKIE_DOMAIN = None
# HTTPS 연결에서만 세션 쿠키 전송 여부
SESSION_COOKIE_SECURE = False
# JavaScript에서 세션 쿠키 접근 차단 여부
SESSION_COOKIE_HTTPONLY = True
# 세션 쿠키의 기본 만료 시간 (초 단위, 기본값: 2주)
SESSION_COOKIE_AGE = 1209600
# 브라우저 종료 시 세션 만료 여부
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
# 매 요청마다 세션 저장 여부 (기본값: False, 수정 시에만 저장)
SESSION_SAVE_EVERY_REQUEST = False
# 캐시 세션 사용 시 캐시 별칭 (settings.CACHES 설정 참조)
SESSION_CACHE_ALIAS = 'default'
# 파일 세션 사용 시 세션 파일 저장 경로 (None이면 임시 디렉토리 사용)
SESSION_FILE_PATH = None
세션 관리: 생성, 조회, 수정, 삭제
Django 뷰에서는 request.session 객체를 통해 세션 데이터를 쉽게 관리할 수 있습니다. 딕셔너리와 유사한 방식으로 데이터를 다룰 수 있습니다.
def manage_session(request):
# 세션 데이터 생성 또는 수정
request.session['user_id'] = 123
request.session['username'] = 'johndoe'
request.session['preferences'] = {'theme': 'dark', 'language': 'en'}
# 세션 데이터 조회 (get 메서드 사용 권장 - 키가 없을 경우 오류 방지)
user_id = request.session.get('user_id')
user_name = request.session.get('username', 'Guest') # 기본값 설정 가능
# setdefault: 키가 존재하지 않을 때만 값을 설정
request.session.setdefault('last_login', '2023-10-27')
# 세션 데이터 삭제
if 'preferences' in request.session:
del request.session['preferences']
# 모든 세션 키 조회
all_keys = request.session.keys()
# 모든 세션 값 조회
all_values = request.session.values()
# 모든 세션 키-값 쌍 조회
all_items = request.session.items()
# 현재 사용자의 세션 ID 조회
session_key = request.session.session_key
# 만료된 세션 데이터 삭제 (백엔드에서 지원하는 경우)
request.session.clear_expired()
# 특정 세션 키의 존재 여부 확인
exists = request.session.exists("session_key_to_check")
# 현재 사용자의 모든 세션 데이터 삭제
request.session.flush() # 또는 del request.session.session_key
# 세션 만료 시간 재설정
# value가 정수: 해당 초 후에 만료
request.session.set_expiry(300) # 5분 후 만료
# value가 datetime 또는 timedelta 객체: 해당 시점/기간 후에 만료
# from datetime import datetime, timedelta
# request.session.set_expiry(datetime.now() + timedelta(days=1)) # 1일 후 만료
# value가 0: 브라우저 종료 시 만료
request.session.set_expiry(0)
# value가 None: SESSION_COOKIE_AGE 설정에 따름
request.session.set_expiry(None)
return HttpResponse("세션 관리 예시")
세션의 기능, 장점 및 활용
- 기능: 사용자와 서버 간의 상태 정보를 유지하여 웹 애플리케이션의 '대화'를 가능하게 합니다.
- 장점: 민감한 정보(예: 비밀번호, 개인 식별 정보)를 클라이언트가 아닌 서버에 안전하게 보관할 수 있습니다.
- 활용: 사용자 로그인 상태 유지, 장바구니 정보 관리, 사용자 설정 저장 등 다양한 웹 서비스에서 필수적으로 사용됩니다.
예제: 간단한 매칭 웹사이트
사용자 로그인(세션 기반), 남녀 회원 정보 관리, 그리고 서로 관련된 이성 목록을 보여주는 간단한 매칭 웹사이트를 구현하는 시나리오입니다.
요구사항
- 로그인: 사용자 이름, 비밀번호, 성별을 입력받고, '일주일 동안 로그인 유지' 옵션을 제공합니다. 로그인 성공 시 세션에 사용자 ID와 성별을 저장합니다.
- 데이터 모델: 남자 회원, 여자 회원, 그리고 이 둘 간의 관계를 저장하는 테이블을 정의합니다.
- 기능:
- 로그인 페이지: 사용자 정보 입력 및 로그인 처리
- 메인 페이지: 로그인한 사용자의 성별에 따라 반대 성별 회원의 목록을 보여줍니다.
- 관계 목록 페이지: 현재 사용자와 관련된 이성 회원의 목록을 보여줍니다.
구현 코드 예시
1. URL 설정 (urls.py)
from django.urls import path
from app01.views import account, main
urlpatterns = [
path('admin/', admin.site.urls),
path('login/', account.login_view, name='login'),
path('logout/', account.logout_view, name='logout'),
path('index/', main.index_view, name='index'),
path('partners/', main.partners_view, name='partners'),
]
2. 모델 정의 (models.py)
from django.db import models
class Man(models.Model):
username = models.CharField(max_length=50, unique=True)
password = models.CharField(max_length=128) # 비밀번호는 해싱하여 저장해야 합니다.
nickname = models.CharField(max_length=50)
def __str__(self):
return self.nickname
class Woman(models.Model):
username = models.CharField(max_length=50, unique=True)
password = models.CharField(max_length=128) # 비밀번호는 해싱하여 저장해야 합니다.
nickname = models.CharField(max_length=50)
def __str__(self):
return self.nickname
class Connection(models.Model):
man = models.ForeignKey(Man, on_delete=models.CASCADE)
woman = models.ForeignKey(Woman, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.man.nickname} & {self.woman.nickname}"
3. 계정 관련 뷰 (views/account.py)
from django.shortcuts import render, redirect, HttpResponse
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout
from app01.models import Man, Woman
def login_view(request):
if request.method == 'GET':
return render(request, 'login.html')
username = request.POST.get('username')
password = request.POST.get('password')
gender = request.POST.get('gender') # '1' for Man, '0' for Woman
remember_me = request.POST.get('remember_me')
user_obj = None
if gender == '1':
user_obj = Man.objects.filter(username=username, password=password).first()
else:
user_obj = Woman.objects.filter(username=username, password=password).first()
if not user_obj:
return render(request, 'login.html', {'error_message': 'Invalid username or password'})
else:
# 세션에 사용자 정보 저장
request.session['user_info'] = {
'user_id': user_obj.id,
'username': user_obj.username,
'nickname': user_obj.nickname,
'gender': gender
}
# 'Remember Me' 옵션 처리 (세션 만료 시간 연장)
if remember_me:
request.session.set_expiry(60 * 60 * 24 * 7) # 1주일
else:
request.session.set_expiry(0) # 브라우저 종료 시 만료
return redirect('index')
def logout_view(request):
request.session.flush() # 모든 세션 데이터 삭제
return redirect('login')
4. 메인 기능 뷰 (views/main.py)
from django.shortcuts import render, redirect
from app01.models import Man, Woman, Connection
def index_view(request):
if 'user_info' not in request.session:
return redirect('login')
user_info = request.session['user_info']
gender = user_info.get('gender')
partners_list = []
if gender == '1': # 현재 사용자가 남자일 경우
partners_list = Woman.objects.all()
else: # 현재 사용자가 여자일 경우
partners_list = Man.objects.all()
# 페이지네이션 로직 추가 (예: PageInfo 클래스 사용)
# total_partners = partners_list.count()
# page_info = PageInfo(request.GET.get('page'), total_partners, 10, '/index/', 11)
# paginated_partners = partners_list[page_info.start():page_info.end()]
return render(request, 'index.html', {
'partners': partners_list,
# 'page_info': page_info
})
def partners_view(request):
if 'user_info' not in request.session:
return redirect('login')
user_info = request.session['user_info']
current_user_id = user_info.get('user_id')
gender = user_info.get('gender')
related_partners = []
if gender == '1': # 현재 사용자가 남자
# 남자 ID와 연결된 여자들의 닉네임 조회
connections = Connection.objects.filter(man_id=current_user_id).select_related('woman')
related_partners = [conn.woman.nickname for conn in connections]
else: # 현재 사용자가 여자
# 여자 ID와 연결된 남자들의 닉네임 조회
connections = Connection.objects.filter(woman_id=current_user_id).select_related('man')
related_partners = [conn.man.nickname for conn in connections]
return render(request, 'partners.html', {'related_partners': related_partners})
5. 템플릿 (templates/login.html, templates/index.html, templates/partners.html)
각 HTML 템플릿 파일은 사용자 인터페이스를 렌더링하며, Django 템플릿 언어를 사용하여 동적 데이터를 표시합니다. user_header.html과 같은 부분 템플릿을 포함하여 코드 중복을 줄일 수 있습니다.
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
{% include 'user_header.html' %}
<h1>Login</h1>
{% if error_message %}<p style="color:red;">{{ error_message }}</p>{% endif %}
<form method="post">
{% csrf_token %}
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<div>
<label>Gender:</label>
<input type="radio" name="gender" value="1" checked> Man
<input type="radio" name="gender" value="0"> Woman
</div>
<div>
<input type="checkbox" name="remember_me" value="1"> Remember me
</div>
<button type="submit">Login</button>
</form>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Partner List</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
</head>
<body>
{% include 'user_header.html' %}
<h3>Available Partners</h3>
<a href="{% url 'partners' %}">View Related Partners</a>
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Nickname</th>
</tr>
</thead>
<tbody>
{% for partner in partners %}
<tr>
<td>{{ partner.id }}</td>
<td>{{ partner.nickname }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{# Pagination will be added here if implemented #}
{# <nav><ul class="pagination">{{ page_info.pager|safe }}</ul></nav> #}
</body>
</html>
partners.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Related Partners</title>
</head>
<body>
{% include 'user_header.html' %}
<h1>Your Related Partners</h1>
<ul>
{% for partner_nickname in related_partners %}
<li>{{ partner_nickname }}</li>
{% empty %}
<li>No related partners found.</li>
{% endfor %}
</ul>
</body>
</html>
user_header.html
<div>
{% if request.session.user_info %}
<h2>Welcome, {{ request.session.user_info.nickname }}!</h2>
<a href="{% url 'logout' %}">Logout</a>
{% endif %}
</div>