사용자 인증 시스템 구성
Django 웹 애플리케이션에서 사용자 관리 기능은 보안과 신뢰성을 확보하는 데 필수적입니다. 외부 라이브러리에 의존하지 않고 Django 내장 인증 시스템을 활용하여 등록, 로그인, 로그아웃 기능을 구현해 보겠습니다. 이를 위해 별도의 accounts 앱을 생성하고 기존 프로젝트 설정에 통합합니다.
앱 생성 및 설정 적용
프로젝트 루트 디렉토리에서 터미널을 실행하여 새 앱 초기화 명령어를 입력합니다.
python manage.py startapp membership
생성된 앱은 settings.py 파일의 INSTALLED_APPS 목록에 추가되어야 활성화됩니다. 또한 프로젝트 최상위 urls.py 에 해당 경로를 포함시켜 라우팅을 가능하게 합니다.
INSTALLED_APPS = [
# 기존 앱들...
'membership',
]
# urls.py 수정 예시
path('auth/', include('membership.urls')),
회원가입 뷰 구현
기본 제공되는 폼 클래스를 상속받아 커스텀 폼을 작성하면 UI 일관성과 유효성 검사를 쉽게 관리할 수 있습니다. forms.py 에 다음 내용을 정의합니다.
from django.contrib.auth.forms import UserCreationForm
class RegistrationForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_name in ['username', 'password1', 'password2']:
self.fields[field_name].help_text = None
self.fields[field_name].widget.attrs.update({'class': 'form-control'})
이후 views.py 에서 폼 제출을 처리하는 함수를 작성하며, 성공 시 홈 페이지로, 실패 시 에러가 포함된 폼을 다시 렌더링하도록 설계합니다.
from django.shortcuts import render, redirect
from .forms import RegistrationForm
def user_register(request):
context = {'page_title': '회원가입'}
if request.method == 'POST':
form_instance = RegistrationForm(request.POST)
if form_instance.is_valid():
form_instance.save()
return redirect('home.main')
else:
form_instance = RegistrationForm()
context['registration_form'] = form_instance
return render(request, 'membership/register.html', context)
로그인 및 세션 관리
사용자 식별은 HTTP 의 무상태적 특성을 보완하기 위해 세션을 사용합니다. 로그인은 직접적인 HTML 폼을 만들어 처리함으로써 템플릿 제어력을 높일 수 있습니다.
from django.contrib.auth import authenticate, login
# views.py 내부
def sign_in(request):
error_msg = ''
if request.method == 'POST':
user_obj = authenticate(
username=request.POST.get('username'),
password=request.POST.get('pass_word')
)
if user_obj:
login(request, user_obj)
return redirect('home.main')
else:
error_msg = '정보 불일치'
return render(request, 'membership/login.html', {'error_message': error_msg})
콘텐츠 상호작용: 리뷰 시스템
인증이 완료되면 사용자가 영화에 대해 의견을 남길 수 있어야 합니다. 이는 기본적인 CRUD(생성, 조회, 업데이트, 삭제) 패턴을 따르며, 데이터 무결성을 위해 외키 관계를 정의합니다.
리뷰 모델 설계
movies 앱 내에 새로운 모델을 추가합니다. 각 리뷰는 특정 영화와 작성자와의 연결 고리를 가져야 하므로 ForeignKey 를 활용합니다.
from django.db import models
from django.contrib.auth.models import User
class FilmCritique(models.Model):
critique_text = models.CharField(max_length=500)
created_at = models.DateTimeField(auto_now_add=True)
target_movie = models.ForeignKey('Movie', on_delete=models.CASCADE)
written_by = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return f"{self.target_movie.name} - {self.written_by.username}"
모델 변경 사항을 반영하기 위해 데이터베이스 마이그레이션을 수행해야 합니다.
python manage.py makemigrations
python manage.py migrate
리뷰 생성 로직
영화 상세 페이지에서 인증된 사용자만 리뷰 제출 폼을 보이도록 템플릿 조건문을 적용하고, 뷰 함수에서는 권한 확인을 엄격히 합니다.
@login_required
def submit_critique(request, movie_id):
film = get_object_or_404(Movie, pk=movie_id)
if request.method == 'POST':
new_critique = FilmCritique.objects.create(
critique_text=request.POST.get('critique'),
target_movie=film,
written_by=request.user
)
return redirect('films.details', pk=movie_id)
return redirect('films.details', pk=movie_id)
기존 리뷰 수정 및 삭제
작성자 본인에게만 수정 및 삭제 권한을 부여하기 위해 뷰 내부에서 소유권 체크를 수행합니다. get_object_or_404 를 사용하여 잘못된 ID 요청 시 안전함을 보장합니다.
@login_required
def edit_critique(request, movie_id, critique_id):
item = get_object_or_404(FilmCritique, id=critique_id, written_by=request.user)
if request.method == 'POST':
item.critique_text = request.POST.get('text')
item.save()
return redirect('films.details', pk=movie_id)
쇼핑 카트 및 세션 활용
장바구니 기능은 사용자의 상태를 지속 유지해야 하는 대표적인 사례입니다. 이때 Django 의 세션 메커니즘이 유용하게 쓰이며, 쿠키를 통해 클라이언트와 서버 간 데이터를 동기화합니다.
카트 앱 구조
쇼핑 관련 로직은 전용 앱으로 분리하여 관리성을 높입니다. 세션 딕셔너리를 사용하여 담겨있는 상품 정보를 임시 저장합니다.
# cart/views.py
from django.shortcuts import redirect
from movies.models import Film
def add_to_bag(request, film_id):
film_obj = get_object_or_404(Film, id=film_id)
session_bag = request.session.get('basket_items', {})
quantity = int(request.POST.get('qty', 1))
session_bag[str(film_id)] = quantity
request.session['basket_items'] = session_bag
return redirect('shop.basket_view')
총액 계산 유틸리티
복잡한 계산을 뷰에서 분리하여 재사용성을 높이려면 유틸리티 파일을 사용하는 것이 좋습니다. 각 아이템의 가격을 양과 곱하여 합산합니다.
# cart/utils.py
def compute_bag_amount(items_dict, films_list):
grand_total = 0
for film in films_list:
count = items_dict.get(str(film.id), 0)
grand_total += film.price * int(count)
return grand_total
세션을 통한 상태 추적
웹 브라우저의 개발자 도구를 통해 sessionid 쿠키가 어떻게 교환되는지 확인할 수 있습니다. 이 코드는 백엔드에서 사용자 액션 기록을 추적하고, 결제 전까지 데이터를 보유하는 역할을 합니다.
주문 처리 및 영수증 모델
결제 버튼 클릭 시 메모리에 있는 카트 데이터를 영구적인 주문 레코드로 변환해야 합니다. 이를 위해 거래 내역 (Transaction) 과 항목 (TransactionItem) 모델을 정의합니다.
데이터 모델 정제
주문 발생 시점의 정보 (구매자, 총금액, 날짜) 와 개별 구매 항목 (상품, 단가, 수량) 을 구분하여 저장합니다.
from django.db import models
class SalesRecord(models.Model):
customer = models.ForeignKey(User, on_delete=models.CASCADE)
total_price = models.DecimalField(max_digits=10, decimal_places=2)
purchase_date = models.DateTimeField(auto_now_add=True)
class SaleDetail(models.Model):
record = models.ForeignKey(SalesRecord, on_delete=models.CASCADE, related_name='details')
product = models.ForeignKey(Film, on_delete=models.SET_NULL, null=True)
amount = models.IntegerField(default=1)
unit_cost = models.DecimalField(max_digits=10, decimal_places=2)
결제 프로세스 실행
주문 뷰는 로그인 여부와 빈 카트를 먼저 검증한 뒤, 실제로는 데이터베이스 트랜잭션을 일으키는 작업을 수행합니다. 구매가 확정되면 세션 카트는 즉시 비워집니다.
@login_required
def complete_purchase(request):
basket = request.session.get('basket_items', {})
if not basket:
return redirect('shop.basket_view')
films = list(Film.objects.filter(id__in=list(basket.keys())))
total_amt = compute_bag_amount(basket, films)
order = SalesRecord.objects.create(customer=request.user, total_price=total_amt)
for film in films:
SaleDetail.objects.create(
record=order,
product=film,
amount=basket.get(str(film.id)),
unit_cost=film.price
)
request.session['basket_items'] = {}
return render(request, 'cart/confirmation.html', {'record_id': order.id})
주문 내역 조회
사용자는 본인의 과거 구매 내역을 확인할 수 있어야 합니다. ForeignKey 관계의 역방향 접근 (user.salesrecord_set) 을 이용하여 특정 고객에게 해당하는 모든 주문을 필터링합니다.
@login_required
def my_records(request):
history = request.user.salesrecord_set.all().order_by('-purchase_date')
return render(request, 'membership/orders.html', {'transactions': history})