웹사이트에서 사용자들에게 편리한 글목록을 제공하려는 노력들이 많습니다.
기존의 방법은 일반적인 페이지네이션입니다.
페이지네이션은 사용자가 다음페이지로 넘어가는 번호를 눌러줘야한다는 단점이 있습니다. 별 거 아닌 것 같지만, 버튼을 눌렀을 때 페이지가 바뀌는 피로감과 눌러야하는 노동력이 마이너스 요소입니다.
사용자들은 덜 기다리고, 더 편리하게 웹서핑을 하고 싶어합니다.
이런 단점을 극복하기 위해 나온 게 무한 스크롤(infinite scroll)입니다. 위의 사진과 같은 형태지요.
어떨때 무한 스크롤을 써야할까?
굳이 따지자면 무한 스크롤링이 페이지네이션보다 최신 기법이지만 그렇다고 해서 페이지네이션보다 우월하다고 볼 순 없습니다.
무한 스크롤의 단점은 크게 4가지가 있습니다.
- 사용자가 현재의 위치를 알기 힘들고 표류하는 느낌이 납니다.
- 원하는 위치에 있는 자료를 찾기 힘듭니다.
- 웹페이지의 푸터 부분을 볼 수 없습니다.
- 글을 읽고난 후 뒤로가기를 했을 때 원래 위치로 돌아가기 힘듭니다.
대표적으로 인피니트 스크롤을 사용하는 메이저 사이트는 [**트위터**]입니다.
간간히 인터넷을 돌아다니다보면 트위터는 자료를 찾을때 부적합하다며 불평하는 분들이 많습니다.
페이지네이션은 1페이지에서 10페이지로 바로 점프가 가능하지만, 인피니트 스크롤은 불가능합니다.
페이지네이션은 내가 몇 페이지까지 왔는지 알 수 있지만, 인피니트 스크롤은 불가능합니다.
그 점이 자료를 찾을 때 마이너스 요소가 됩니다.
만약 위의 기상청 트위터에서 1년전의 날씨를 스크롤만으로 얻으려면 엄청나게 힘든 과정을 거쳐야 합니다.
따라서 트위터처럼 휘발성 있는 일상 위주의 이야기를 나누는 곳이라거나 쇼핑몰 같이 때론 목적 없이 아이쇼핑을 해야하는 곳에는 인피니트 스크롤이 좋고, 자료 보관을 목적으로 한 곳은 페이지네이션이 낫다는 게 제 의견입니다.
무한 스크롤 구현이론
무한 스크롤의 원리는 간단합니다.
컨텐츠의 끝부분을 만나게 되면, 다음 페이지 부분을 불러와서 더하는 겁니다.
그래서 코드를 요약하면 이렇습니다.
1. 컨텐츠의 끝 부분을 감지했다면
2. 다음 페이지의 부분을 불러와 현재 페이지에 붙여 넣는다.
인피니티 스크롤은 일전에 다룬 레이지로드와 유사한 점도 많고, 제작법도 유사합니다.
크게 2가지 방법으로 만들 수 있다는 점도 동일한데, 전통적인 스크롤 감지 - 그리고 Intersection Observer지요.
이번 시간에는 전통적인 스크롤 방식으로 예제를 짜볼까 합니다.
인피니티 스크롤 만들기 - 예제
제작하기에 앞서 기본적으로 3가지 요소가 필요하다고 생각합니다.
- 모든 내용물을 감싼 공간 (아래의 예제에선 infinite class가 해당)
- 목록의 내용물이 담긴 공간 (아래의 예제에선 list class가 해당)
- 다음 페이지의 링크가 담긴 페이지네이션 (아래의 예제에선 pagination class가 해당)
이를테면 이렇게 지정되어 있어야 합니다.
<div class="infinite">
<div class="list">
내용물
</div>
</div>
<div class="pagination">페이지네이션</div>
그리고 우리의 목표는 list의 내용물의 바닥을 만났을 때 Fetch나 xmlhttprequest나 ajax로 다음페이지의 내용을 불러와 이렇게 만드는 겁니다.
<div class="infinite">
<div class="list">
1페이지 내용물
</div>
<div class="list">
2페이지 내용물
</div>
</div>
<div class="paginaiton">페이지네이션</div>
원리자체는 매우 간단합니다.
그렇다면 실제 예제로 넘어가보겠습니다.
예제1 바닥을 감지하기
굉장히 간단한 자바스크립트로 가능합니다.
function YesScroll () {
const pagination = document.querySelector('.paginaiton'); // 페이지네이션 정보획득
const fullContent = document.querySelector('.infinite'); // 전체를 둘러싼 컨텐츠 정보획득
const screenHeight = screen.height; // 화면 크기
let oneTime = false; // 일회용 글로벌 변수
document.addEventListener('scroll',OnScroll,{passive:true}) // 스크롤 이벤트함수정의
function OnScroll () { //스크롤 이벤트 함수
const fullHeight = fullContent.clientHeight; // infinite 클래스의 높이
const scrollPosition = pageYOffset; // 스크롤 위치
if (fullHeight-screenHeight/2 <= scrollPosition && !oneTime) { // 만약 전체높이-화면높이/2가 스크롤포지션보다 작아진다면, 그리고 oneTime 변수가 거짓이라면
oneTime = true; // oneTime 변수를 true로 변경해주고,
madeBox(); // 컨텐츠를 추가하는 함수를 불러온다.
}
}
}
YesScroll()
코드는 위의 주석으로 다 설명했다고 생각하지만 주의사항을 적어볼까 합니다.
우선 [**oneTime**] 변수를 쓰는 이유는 바닥에 닿고 나서 [**madeBox**]를 딱 한번만 호출하기 위해서입니다.
처음에 거짓으로 설정한 후, 바닥에 닿으면 [**oneTime**]을 true로 바꿔주면 똑같은 것들을 여러번 호출하는 일을 방지해줍니다.
[팁*if 조건문에서 화면의 높이 절반을 빼준 이유는, 유저의 사용 편의성 때문입니다. 바닥을 보기 전에 미리 불러와야 사용자의 편리함이 올라갑니다.*]
또한 [**fullHeight**]의 변수선언을 한 위치도 중요한데, 스크롤을 감지해주는 함수 내에 적은 겁니다.
만약 함수 내에 적지 않는다면 [**fullContent**]의 초기 높이+추가된 컨텐츠의 높이가 업데이트 되지 않아서 첫번째로 바닥에 닿으면 컨텐츠를 무한대로 불러오는 불상사가 벌어집니다.
여기까지 했다면 이제 [**madeBox**]라는 함수를 정의할 때입니다.
이 함수는 현재페이지 기준 다음페이지의 컨텐츠를 추출해내고, 그걸 list 클래스에 더해주는 역할을 할 겁니다.
예제2 컨텐츠를 다음페이지에서 추출해서 더하기
처음 예제에서 페이지네이션의 정보를 querySelector로 얻은 이유가 있습니다.
여러분도 짐작하셨겠듯이 다음페이지의 정보를 얻기 위해서 입니다.
페이지네이션은 사용하시는 페이지에 따라 다르게 되어 있습니다.
디테일한 부분에서 다르겠지만 일반적으로는 아래와 같이 되어 있는 경우가 많습니다.
<a href="1페이지주소" class="prevPage" id="">◀</a>
<span>2</span>
<a href="3페이지주소" class="nextPage" id="">▶</a>
2페이지에 접속한 상태라면 이전 페이지인 1페이지로 이동하는 버튼, 다음 페이지인 3페이지로 이동하는 버튼이 있을 겁니다.
이 때 이전 페이지로 가는 곳엔 [**prevPage**]라는 클래스, 다음 페이지로 가는 데엔 [**nextPage**]라는 클래스가 있습니다.
클래스명이야 웹사이트마다 다르겠지만, 이전/이후 버튼을 식별하는 용도입니다.
const nextLink = pagination.querySelector('.nextPage');
const nextURL = nextLink.getAttribute('href');
위와 같이 [**nextPage**]에서 href를 얻어내면 다음페이지의 주소를 얻어내는 셈입니다.
이제 다음페이지의 주소를 xmlhttprequest로 한 번 불러오겠습니다.
madeBox(){
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 200 || xhr.status === 201) {
const data = xhr.response; // 다음페이지의 정보
const addList = data.querySelector('.list'); // 다음페이지에서 list아이템을 획득
fullContent.appendChild(addList); // infinite에 list를 더해주기
oneTime = false; // oneTime을 다시 false로 돌려서 madeBox를 불러올 수 있게 해주기
} else {
console.error(xhr.response);
}
}
};
xhr.open('GET', nextURL); // 다음페이지의 정보를 get
xhr.send();
xhr.responseType = "document";
}
위와 같이 nextURL에 담긴 다음페이지의 주소를 매개로 xmlhttprequest로 열었습니다.
data로 지정한 변수를 통해서 페이지의 정보를 담았고, 이제 내용물인 list를 얻어낸 후 이것을 infinite에 appendChild로 더해주면 끝입니다.
예제로 표현하자면 위와 같습니다.
스크롤을 쭉 내려보시면 끝없이 콘텐츠가 추가되는 걸 보실 수 있을 겁니다.
다만 codepen에서 xmlrequest를 쓰기엔 여러가지 제약사항이 있어서 madeBox 함수를 직접 element를 만들어 더해주는 방식으로 바꾼 것일 뿐입니다.
고려해봐야할 것들
실제 작업으로 들어갔을 때 고려해야할 것들이 있습니다.
- 사용자가 표류하지 느낌을 느끼지 않게 해주려면 어떻게 해야할 것인가?
- 글을 보고나서 뒤로가기를 했을 때 어떻게 원래 위치로 돌아올 것인가?
- 2페이지로 돌아온 상태에서 1페이지로 가려면 위쪽 감지를 어떻게 할 것인가?
- xmlrequest로 불러오면 딜레이가 생기는데, 그 때 어떻게 해야 사용자에게 로딩 중이라는 것을 알려줄 수 있을까?
- 처음 링크를 불러왔을 땐 그렇다치고, 다다음페이지는 어떻게 불러올 것인가?
- 끝 페이지에 도달했을 때 어떻게 해야 하는가?
차근차근 살펴보겠습니다.
표류하지 않는 느낌을 주려면 이정표를 제공하면 됩니다.
대표적으로 사용되는 방법이 [**버튼**]입니다.
다음 페이지의 정보를 바로 더해주는 게 아니라 바닥에 닿으면 버튼을 추가하고, 그 버튼을 누르면 다음페이지의 정보를 더해주는 방식입니다.
if (fullHeight-screenHeight/2 <= scrollPosition && !oneTime) {
oneTime = true;
madeBox();
} // 기존 방식
if (fullHeight-screenHeight/2 <= scrollPosition && !oneTime) {
oneTime = true;
const button = document.createElement('button');
button.className = "nextButton";
button.textContent = "다음페이지";
fullContent.appendChild(button);
} // 버튼 방식
const nextButton = document.querySelector('.nextButton')l
nextButton.addEventListener('click',nextGO); // 만들어낸 버튼에 장착한 클릭 이벤트 리스너
function nextGO () { // 만약 버튼을 클릭했다면
madeBox();//madeBox를 발동한다
this.parentNode.removeChild(nextButton); // 버튼을 제거한다.
}
핵심은 바닥에 닿았을 때 버튼을 만들고, 그 버튼을 누르면 madeBox 함수를 발동하는 겁니다.
글을 보고나서 뒤로가기를 했을 때 원래 위치로 돌아오려면 history.pushState를 이용하면 됩니다.
history.pushState는 페이지 변경 없이 주소기록을 넣어주는 겁니다.
이를테면 위에서 xmlrequest로 다음 페이지의 정보를 불러왔는데, 그 때 xmlrequest 내부에 아래와 같이 마크업을 해주면 됩니다.
history.pushState(null, null, xhr.responseURL);
그러면 2페이지의 정보를 불러올 때, 주소창은 2페이지의 주소로 바뀝니다.
따라서 2페이지에서 글을 클릭->뒤로가기->2페이지로 돌아오기가 가능합니다.
다만 history.pushState는 100% 의도된 대로 제어하진 못합니다.
만약 사용자가 여러차례 페이지를 살펴보다 뒤로가기로 아예 사이트에서 나가려고 할 때, 누적된 주소들 때문에 표류하다가 짜증이 나실 수도 있습니다.
위 페이지를 감지해야할 경우 else if로 스크롤이 가장 상단에 닿았을 때 발동하는 함수를 만들면 됩니다.
제가 YesScroll이란 함수에서 바닥에 닿으면 madeBox를 발동하게 했는데, 여기에 else if로 경우의 수를 추가하면 됩니다.
if (fullHeight-screenHeight/2 <= scrollPosition && !oneTime) {
oneTime = true;
madeBox();
}
else if ( scrollPosition == 0 ) {
oneTime = true;
madeBox();
}
문제는 madeBox내에서 이게 위에 닿았을 때 온 요청인지, 밑에 닿아서 온 요청인지 구분할 수가 없다는 겁니다.
이럴 땐 변수를 통해서 분기문을 만들고 해결하면 됩니다.
else if ( scrollPosition == 0 ) {
oneTime = true;
upup = true;
madeBox();
}
madeBox내에 변수 upup이 true일 때를 분기문으로 만들어서 위로 올라갈 때의 요청일 때 해야하는 작업들을 넣어두면 됩니다.
[팁* scrollPosition == 0 으로 조건문을 만들어뒀지만, 실제론 infinite 클래스의 천장을 감지해서 조건문으로 만들어야 합니다.*]
물론 위의 과정이 복잡하고 짜증난다면 그냥 속편하게 madeBox2 함수를 만들어서 이전글을 추출하도록 하면 됩니다.
xmlrequest의 딜레이 해결법은 로딩바를 삽입하면 됩니다.
제 블로그에도 전용으로 사용하는 로딩바 이미지가 있습니다.
이미지를 최적화해두면 1kb가 안되는 용량으로 로딩 이미지를 구현할 수 있는데, 이 이미지를 fixed position으로 화면 정중앙이나, 사용자의 시선이 닿는 곳에 오도록 해주면 됩니다.
xmlrequest를 보낼 때 loader를 특정 공간에 더해주고,
xmlrequest에 접속했을 때 loader를 삭제.
이렇게 하면 됩니다.
첫 페이지 불러오기 이후 다다음 페이지를 불러올 때에는 글로벌 변수를 이용하면 됩니다.
이 방법은 어디까지나 초보인 제가 쓰는 방법이고, 훨씬 효율적이고 깔끔한 방법들이 많을 겁니다.
저의 경우엔 글로벌 변수를 만들어서 false로 지정한 후, 처음으로 다음페이지를 불러올 때엔 true로 만들어 딱 1번만 작동하게 만듭니다.
그리고 다른 else if문은 글로벌 변수가 true일 때 다음페이지로 불러왔던 주소를 가져와서 불러올 때 쓰는 겁니다.
마지막으로 끝페이지에 도달해 더이상 불러올 페이지가 없다면, removeEventListener를 이용해 스크롤 감지를 제거해야 합니다.
코드로 나타내면 아래와 같습니다.
addEventListener('scroll',OnScroll,{passive:true});
if (끝페이지에 도달했다는 걸 걸러낼 수 있는 조건) {
removeEventListener('scroll',OnScroll,{passive:true});
}
페이지네이션의 형태마다 끝페이지에 도달했다는 걸 알 수 있는 조건이 너무 다르다보니 구체적으로 적지 않았는데, 저 같은 경우엔 prevPage나 nextPage 클래스에서 href를 추출할 수 없을 때를 끝페이지의 기준으로 삼았습니다.
인피니트 스크롤 라이브러리
이 모든 게 다 귀찮고 다른 사람이 만든 프로그램을 써야겠다 싶으신 분은 라이브러리를 쓰시면 됩니다.
가장 추천하고 싶은 라이브러리는 마샬k님이 만드신 인피니트 스크롤입니다.
기능을 요약하면 아래 사진과 같습니다. (출처, 마샬님 블로그)
MIT 라이센스 기반이라 상업적으로 써도 무료고, 어디에나 적용할 수 있는 범용성 있는 도구입니다.
실제 예제도 페이지에서 경험해볼 수 있습니다.
마샬k님 블로그에 소개된 인피니트 스크롤 라이브러리: https://marshall-ku.com/work/other/infinite-scroll-%ed%94%8c%eb%9f%ac%ea%b7%b8%ec%9d%b8
github주소:https://github.com/marshall-ku/Infinite-Scroll
관련글
'그외 > 유용한 정보' 카테고리의 다른 글
티스토리 블로그에 구글 애드센스 광고가 표시되지 않는 오류 (2) | 2020.10.28 |
---|---|
RTX-3080 종류별 성능 등급표 알아보기 (0) | 2020.10.26 |
코드펜(codepen) 사용법과 저작권을 알아봅시다. (2) | 2020.10.21 |
스크롤 내리면 상단에 고정되는 네비 메뉴 만들기 (4) | 2020.10.20 |
웹폰트를 경량화 후 로딩속도를 최적화해서 적용하는 방법 (2) | 2020.10.12 |