Django
Chapter 10-2 Django에서 LoginRequiredMixin 완전 정복
Chansman
2025. 5. 15. 15:51
🔐 Django에서 LoginRequiredMixin 완전 정복
LoginRequiredMixin은 Django의 클래스 기반 뷰(CBV)에서 로그인한 사용자만 접근 가능하도록 제한할 때 사용하는 믹스인입니다.
1️⃣ LoginRequiredMixin이란?
- ✅ 로그인하지 않은 사용자가 접근하면 자동으로 로그인 페이지로 리디렉션됩니다.
- ✅ CBV(Class-Based View)에서 사용하며, FBV(Function-Based View)에서는 @login_required 데코레이터를 사용합니다.
- ✅ 로그인 후 리디렉션 URL은 settings.LOGIN_URL로 설정된 경로를 따릅니다.
2️⃣ 실전 사용 예시
# post/comment_views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic import CreateView
from post.forms import CommentForm
from post.models import Comment, Post
class CommentCreateView(LoginRequiredMixin, CreateView):
model = Comment
form_class = CommentForm
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
post = Post.objects.get(pk=self.kwargs.get('post_pk'))
self.object.post = post
self.object.save()
return HttpResponseRedirect(reverse('main'))
post_list_content.html
fontawesome 에서 comment,like 검색해서 나온것 삽입
추가적으로 comment 가저오기 전체적인 위치 조정
{# templates/post/post_list_content.html #}
{% load static %}
{% for post in object_list %}
<div class="border-bottom my-4 pb-2 infinite-item">
<div class="mb-2">
<span class="p-2 border rounded-circle me-2">
<i class="fa-solid fa-user" style="width: 16px; padding-left: 3px;"></i>
</span>
{{ post.user.nickname }} {{ post.id }}
{% if post.user == request.user %}
<a href="{% url 'update' post.pk %}" class="btn btn-warning btn-sm float-end">수정</a>
{% endif %}
</div>
<div class="swiper" style="max-height: 500px;">
<div class="border-1 swiper-wrapper">
{% for post_image in post.images.all %}
<div class="swiper-slide">
<img class="img-fluid post-image" src="{{ post_image.image.url }}" alt="">
</div>
{% endfor %}
</div>
<div class="swiper-pagination"></div>
</div>
<div class="my-2">
{{ post.content|linebreaksbr }}
</div>
<div>
<i class="fa-solid fa-comment"></i>
<i class="fa-regular fa-heart"></i>
<div>
<button class="add-comment">댓글 작성</button>
</div>
</div>
<div class="comment-form d-none">
{% if request.user.is_authenticated %}
<form action="{% url 'comment:create' post.pk %}" method="post">
{% csrf_token %}
{{ comment_form.as_p }}
<button class="btn btn-primary btn-sm">생성</button>
</form>
{% endif %}
</div>
<div class="mt-2">
{% for comment in post.comments.all %}
<p>
<span class="px-1 py-0 border rounded-circle me-2">
<i class="fa-solid fa-user fa-xs" style="width: 8px; padding-left: 1px;"></i>
</span>
<strong>{{ comment.user }}</strong> {{ comment.content | linebreaksbr }}
</p>
{% endfor %}
</div>
</div>
{% endfor %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="infinite-more-link"></a>
<div id="scroll-sentinel" style="height:1px;"></div>
{% endif %}
poast/list.html JS 추가
{% block js %}
<!-- ✅ [추가] jQuery를 가장 먼저 불러오기 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- 기존 swiper 스크립트 유지 -->
<script src="https://cdn.jsdelivr.net/npm/swiper/swiper-bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
let loading = false;
// Swiper 초기화
function initSwipers() {
document.querySelectorAll('.swiper').forEach(el => {
new Swiper(el, {
direction: 'horizontal',
loop: false,
pagination: { el: el.querySelector('.swiper-pagination') },
});
});
}
initSwipers();
async function loadNextPage() {
if (loading) return;
const link = document.querySelector('.infinite-more-link');
if (!link) return;
loading = true;
try {
const res = await fetch(link.href, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const html = await res.text();
const tmp = document.createElement('div');
tmp.innerHTML = html;
tmp.querySelectorAll('.infinite-item').forEach(item =>
document.querySelector('.infinite-container').appendChild(item)
);
document.querySelector('.infinite-more-link').remove();
document.getElementById('scroll-sentinel').remove();
const newLink = tmp.querySelector('.infinite-more-link');
if (newLink) {
document.querySelector('.infinite-container').appendChild(newLink);
const newSentinel = document.createElement('div');
newSentinel.id = 'scroll-sentinel';
document.querySelector('.infinite-container').appendChild(newSentinel);
observer.observe(newSentinel);
}
initSwipers();
} catch (err) {
console.error('페이지 로드 실패:', err);
} finally {
loading = false;
}
}
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadNextPage();
}
});
}, {
root: null,
rootMargin: '0px 0px 200px 0px',
threshold: 0
});
const sentinel = document.getElementById('scroll-sentinel');
if (sentinel) observer.observe(sentinel);
});
// ✅ [이벤트 등록은 그대로 유지]
$(document).ready(function(){
$(document).on('click', '.add-comment', function(){
$(this).closest('.infinite-item').find('.comment-form').toggleClass('d-none');
});
});
</script>
{% endblock %}
post/forms.py Comment form 작성
class CommentForm(BootstrapModelForm):
class Meta:
model = Comment
fields = {'content', }
post/views.py context_data get_context_data 함수 작성 역참조 'comment' 추가
class PostListView(ListView):
model = Post
queryset = Post.objects.all().select_related('user').prefetch_related('images', 'comments')
template_name = 'post/list.html' # 전체 레이아웃용 템플릿
context_object_name = 'object_list'
paginate_by = 5
ordering = ('-created_at', )
def get_context_data(self, *args, **kwargs):
data = super().get_context_data(*args, **kwargs)
data['comment_form'] = CommentForm()
return data
post/comment.urls 추가
from django.urls import path
from . import comment_views as views
app_name= 'comment'
urlpatterns = [
path('create/<int:post_pk>', views.CommentCreateView.as_view(), name='create'),
]
config/urls.py에 include 추가
# include
path('comment/', include('post.comment_urls'))
🔍 코드 설명
- LoginRequiredMixin: 로그인하지 않은 사용자는 접근 불가
- form_valid: 폼이 유효할 경우 실행
- post = Post.objects.get(...): URL에서 post_pk를 가져와 연결
- reverse('main'): 댓글 작성 후 메인 페이지로 이동
3️⃣ reverse vs reverse_lazy
항목 reverse reverse_lazy
평가 시점 | 즉시 평가 | 지연 평가 (lazy evaluation) |
사용 위치 | 함수 내부, 뷰에서 직접 호출 | CBV 속성 (예: success_url)에서 사용 |
사용 예 | HttpResponseRedirect(reverse('main')) | success_url = reverse_lazy('main') |
📌 요약
- reverse: 즉시 문자열로 URL을 반환 → 함수 안에서 사용
- reverse_lazy: 필요한 시점까지 기다렸다가 평가 → CBV 클래스 속성에서 사용
🧠 팁과 주의사항
- LoginRequiredMixin은 왼쪽에 먼저 작성해야 합니다. 예:
class MyView(LoginRequiredMixin, ListView):
...
- URL 리디렉션 경로를 커스터마이징하고 싶다면 settings.LOGIN_REDIRECT_URL 또는 next 파라미터 사용
- CBV에서 reverse_lazy를 쓰지 않고 reverse를 쓰면 AppRegistryNotReady 등의 초기화 오류 발생 가능
✅ 결론 정리
목적 사용 도구
로그인 사용자만 접근 | LoginRequiredMixin or @login_required |
URL 생성 (즉시) | reverse() |
URL 생성 (지연) | reverse_lazy() |
로그인 제어와 리디렉션 URL 처리는 Django 프로젝트에서 보안성과 사용자 경험을 동시에 챙길 수 있는 중요한 포인트입니다 🔐