🧩

018 무한 스크롤

 
이번 시간에는 무한 스크롤을 만들어보도록 하겠습니다. 무한 스크롤이란 포스팅이 많아지면 Ajax 통신을 이용해 페이지를 계속 로드하는 것을 말합니다.
 
post_list.html에서 'page'를 검색하시면 'page'라는 id를 가진 input 태그가 하나 있습니다. 여기에다가 Ajax 통신을 이용해서 article을 계속해서 추가할 것인데요. input 박스를 article 아래 넣도록 하겠습니다.
 
파일명 : post/templates/post/post_list.html
{% for post in posts %} <article class="contents"> ... </article> {% endfor %} <div id="post_list_ajax"></div> <!--post를 계속해 추가하는 코드--> <input type="hidden" id="page" value="2">
 

1. Ajax 코드 수정

Ajax 통신을 하는 script_ajax.html을 수정하겠습니다. 프로트엔드 수업을 듣고 오셔서 알고 계시겠지만 전체 페이지에 두 가지의 JavaScript 기능이 들어가 있습니다. 사이트의 사이즈가 줄어들었을때 사이드 박스의 위치를 잡아주는 부분, 스크롤을 내렸을 때 헤더부분 로고가 없어지는 부분입니다.두기능을 먼저 작성하고 스크롤에서 Ajax 통신을 시도하겠습니다.
 
파일명 : post/templates/post/script_ajax.html
<script type="text/javascript"> (function(){ const delegation = document.querySelector('.contents_box'); const side_box = document.querySelector('.side_box'); const header = document.querySelector('#header'); function delegationFunc(e){ ... } function resizefunc(){ console.log('리사이즈') if(pageYOffset >= 10){ let calcWidth = (window.innerWidth * 0.5) + 167; if(side_box){ side_box.style.left = calcWidth + "px"; } } } function scrollfunc(){ var scrollHeight = pageYOffset + window.innerHeight; var documentHeight = document.body.scrollHeight; console.log(pageYOffset); console.log('scrollHeight:'+scrollHeight); console.log('documentHeight:'+documentHeight); if (pageYOffset >= 10){ header.classList.add('on'); resizefunc(); if(side_box){ side_box.classList.add('on'); } } else { header.classList.remove('on'); if(side_box){ side_box.classList.remove('on'); side_box.removeAttribute('style'); } } } // 뷰의 크기가 변할 때 resizefunc 함수 실행 window.addEventListener('resize', resizefunc); // 스크롤이 이동했을 때 scrollfunc함수를 실행 window.addEventListener('scroll', scrollfunc); delegation.addEventListener('click',delegationFunc); })() </script>
 
전체 페이지의 크기를 줄이시면 개발자 도구 콘솔 창에 '리사이즈'라고 출력하는 것을 보실 수 있습니다.
notion imagenotion image
 
스크롤을 내리시면 개발자 도구 콘솔 창에 scrollHeight, documentHeight가 출력되시는 것을 확인할 수 있습니다.
notion imagenotion image
 
notion imagenotion image
 
그리고 스크롤을 내릴 때, 헤더가 줄어드는 애니메이션이 잘 작동하는 것을 볼 수 있습니다.
notion imagenotion image
 
사이즈를 줄여도 위치가 잘 고정이 되고 히든박스도 잘 작동이 됩니다. 이제 스크롤이 끝에 닿았을 때 글이 더 있다면 그 글을 Ajax 통신으로 받아오는 부분을 작성하겠습니다.
 
script_ajax.htmlscrollfunc 함수에 다음 코드를 추가합니다.
 
파일명 : post/templates/post/script_ajax.html
function scrollfunc(){ ... if (pageYOffset >= 10){ ... } else { ... } if(scrollHeight >= documentHeight){ var page = document.querySelector('#page').value; console.log(page); var end_page = {{ posts.paginator.num_pages }} // 해당 내용은 views.py에서 작성할 것입니다. if(page > end_page){ return; } document.querySelector('#page').value = parseInt(page) + 1; callMorePostAjax(page); } }
scrollfunc 아래에 callMorePostAjax 함수를 만들도록 하겠습니다.
 
파일명 : post/templates/post/script_ajax.html
function scrollfunc(){ ... } function callMorePostAjax(page) { var end_page = {{ posts.paginator.num_pages }}; <!--post의 페이지 수를 불러오는 작업--> if(page > end_page){ return; } $.ajax({ type: 'POST', url: "{% url 'post:post_list' %}", data: { 'page': page, 'csrfmiddlewaretoken': '{{ csrf_token }}' }, dataType: 'html', success: addMorePostAjax, error: function(request, status, error){ alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error); } }); }
 
csrftoken은 누가 요청할 때 서버에서 암호화 된 토큰 값을 받습니다. 그것을 다시 서버로 전송해서 두개의 값을 비교해 우리가 보낸 토큰 값이 맞는지 확인하는 중간에 해킹 시도를 막아주는 기능을 합니다.
 
addMorePostAjax 함수도 작성해 보도록 하겠습니다.
 
파일명 : post/templates/post/script_ajax.html
function scrollfunc(){ ... } function callMorePostAjax(page) { ... } function addMorePostAjax(data, textStatus, jqXHR) { let post = document.querySelector('#post_list_ajax'); post.insertAdjacentHTML("beforeend", data); }
 
새로 받은 포스트 리스트 html을 post 뒤에다가 계속 누적해서 붙이는 함수입니다. script_ajax.html에서 변경하실 부분은 전부 변경이 되었습니다.
 

2. post_list 함수 수정

이제 Ajax 요청을 받아 처리하는 뷰 함수를 수정하겠습니다. post_list URL로 요청을 보내서 html 파일을 받을 것입니다. 이 내용을 post/views.pypost_list 함수에 작성해 보도록 하겠습니다.
 
파일명 : post/views.py
... from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage def post_list(request, tag=None): ... paginator = Paginator(post_list, 3) page_num = request.POST.get('page') try: posts = paginator.page(page_num) except PageNotAnInteger: #page 파라미터가 int가 아닌 값이 들어오게 되면 page(1)로 바꿔주세요 posts = paginator.page(1) except EmptyPage: #page가 페이지를 넘어서면 마지막 페이지를 넘겨준다 posts = paginator(page(paginator.num_pages)) ... if request.user.is_authenticated: ... else: ...
 
post_list 함수는 Ajax 통신에만 사용하는 것은 아닙니다. 일반적으로 메인페이지 요청 시 post를 뿌려주고 있습니다. Ajax로 들어왔을때 동작을 다시 정의해줄 수 있습니다.
 
파일명 : post/views.py
def post_list(request, tag=None): ... if request.is_ajax(): return render(request, 'post/post_list_ajax.html',{ 'posts':posts, 'comment_form':comment_form, }) ... if request.user.is_authenticated: ... else: ...
 
아랫부분에 인증된 사용자라면 render에서 보내는 부분이 있었습니다. 우리가 Paginator를 사용했기 때문에 post의 내용, post_listpaginator로 담아서 posts 변수에 최종적으로 담았습니다.
그렇기 때문에 템플릿에 넘겨줄 변수 post_listposts로 변경을 하도록 하겠습니다.
 
파일명 : post/views.py
def post_list(request, tag=None): ... if request.user.is_authenticated: ... return render(request, 'post/post_list.html', { 'user_profile': user_profile, 'posts': posts, 'comment_form': comment_form, 'following_post_list': following_post_list, }) else: return render(request, 'post/post_list.html', { 'posts': posts, 'comment_form': comment_form, })
 
이제 추가되는 글의 템플릿으로 사용할 html 하나를 추가하도록 하겠습니다. post/templates/post /post_list_ajax.html을 만들고 내용을 다음과 같이 작성합니다. post_list.htmlarticle.contents 태그 부분과 똑같습니다. post_list.html에서 복사해 오셔도 됩니다.
 
파일명 : post/templates/post/post_list_ajax.html
{% load static %} {% for post in posts %} <article class="contents"> <header class="top"> <div class="user_container"> <div class="profile_img"> {% if post.author.profile.picture %} <img src="{{ post.author.profile.picture.url }}" alt="프로필이미지"> {% else %} <img src="{% static 'imgs/thumb.jpeg'%}" alt="프로필이미지"> {% endif %} </div> <div class="user_name"> <div class="nick_name m_text">{{ post.author.profile.nickname }} {{ post.id }}</div> <div class="country s_text">Seoul, South Korea</div> </div> <div> <form action="{% url 'post:post_delete' post.pk %}" method="post"> {% csrf_token %} <input type="submit" value="삭제"> </form> </div> </div> <div class="sprite_more_icon" data-name="more"> <ul class="toggle_box"> <li> {% if user.profile in post.author.profile.get_follower %} <input type="submit" class="follow" value="팔로잉" data-name="follow" name="{{ post.author.profile.id }}"> {% else %} <input type="submit" class="follow" value="팔로우" data-name="follow" name="{{ post.author.profile.id }}"> {% endif %} </li> {% if post.author == user %} <li> <a class="post-edit" href="{% url 'post:post_edit' post.pk %}">수정</a> </li> <li> <form class="post-delete-form" action="{% url 'post:post_delete' post.pk %}" method="post"> {% csrf_token %} <input type="submit" class="post-delete" value="삭제"> </form> </li> {% endif %} </ul> </div> </header> <div class="img_section"> <div class="trans_inner"> <div><img src="{{ post.photo.url }}" alt="visual01"></div> </div> </div> <div class="bottom_icons"> <div class="left_icons"> <div class="heart_btn"> {% if user in post.like_user_set.all %} <div class="sprite_heart_icon_outline on" name="{{ post.id }}" data-name="heartbeat"></div> {% else %} <div class="sprite_heart_icon_outline" name="{{ post.id }}" data-name="heartbeat"></div> {% endif %} </div> <div class="sprite_bubble_icon"></div> <div class="sprite_share_icon" data-name="share"></div> </div> <div class="right_icon"> {% if user in post.bookmark_user_set.all %} <div class="sprite_bookmark_outline on" name="{{ post.id }}" data-name="bookmark"></div> {% else %} <div class="sprite_bookmark_outline" name="{{ post.id }}" data-name="bookmark"></div> {% endif%} </div> </div> <div class="likes m_text"> <span id="like-count-{{ post.id }}">좋아요{{ post.like_count }}개</span> <span id="bookmark-count-{{ post.id }}">북마크{{ post.bookmark_count}}개</span> </div> <div class="comment_container"> <div class="comment" id="comment-list-ajax-post{{post.id}}"> {% for comment in post.comment_set.all %} <div class="comment-detail" id="comment{{ comment.id }}"> <div class="nick_name m_text">{{ comment.author.profile.nickname}}</div> <div>{{comment.content}}</div> {% if user == comment.author %} <input type="button" class="del-comment" data-name="comment_delete" value="삭제" name="{{ comment.id }}"> {% endif %} </div> {% endfor %} </div> <div class="small_heart"> <div class="sprite_small_heart_icon_outline"></div> </div> </div> <div class="timer">{{ post.created_at|timesince }}</div> <div class="comment_field" id="add-comment-post{{post.id}}"> {% if user.is_authenticated %} {{ comment_form }} <input type="text" placeholder="댓글달기..."> <div class="upload_btn m_text" name="{{post.id}}" data-name="comment">게시</div> {% else %} {{ comment_form }} <input type="text" placeholder="댓글달기..."> <div class="upload_btn m_text" name="{{post.id}}" data-name="comment" onclick="alert('댓글을 작성하려면 로그인이 필요합니다')">게시</div> {% endif%} </div> </article> {% endfor %}
 
이제 글을 4개 이상 만들고 메인 페이지에서 스크롤 해봅니다. 처음에는 게시글이 3개만 보이지만 스크롤이 끝에 닿는 순간 게시글이 더 추가되는 것을 확인할 수 있습니다.
이렇게 해서 무한스크롤 기능을 만들어 보았습니다. 포스트를 계속 추가해도 3개씩 계속해서 추가가 됩니다. Ajax 통신 전에는 paginator로 3개만 보여주는 것을 확인하실 수 있습니다.