페이징UI와 무한스크롤UI를 비교해보면 모바일 최적화로서 무한스크롤UI를 선호하는 추세이다. 스크롤 됨에 따라 비동기적으로 이미지나 콘텐츠를 가져오는데 일단, 서버통신을 하지 않고 무한스크롤이 어떻게 구현되는지 기본을 공부해보았다.
바닐라JS로 구현방법은 스크롤이벤트, 인터섹션 옵저버를 이용한 두가지 방법을 사용해 봤고 각각의 장단점과 인터섹션 옵저버에 관련하여 내용을 공유하려고 한다.
li
3개를 마련해둔다.li
가 보이면 li
를 추가한다.매우 간단한 계획! 과연 계획대로 될 것인가,,?
[html]
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</body>
[css]
ul {
padding-left: 0;
}
ul li {
background-color: pink;
margin-bottom: 20px;
list-style: none;
text-align: center;
color: #fff;
font-size: 100px;
padding: 100px 0;
}
/* 짝수, 홀수 색상 구분하기 위해 */
li:nth-child(2n) {
background-color: skyblue;
}
첫 번째 방법은 스크롤 이벤트를 사용했다.
스크롤이 마지막 li
면 새로운 순번이 담긴 li
를 추가하도록 하였다. 하지만 조건을 '마지막 li
면'으로 하고 싶었는데 훨씬 까다로워지는 코드 덕에 다른 조건으로 구현했다. 바로 '보이는 브라우저의 높이 + 윈도우의 스크롤 Y값이 해당 콘텐츠(ul
)의 높이보다 같거나 크면'이라는 조건으로 말이다...!
[ js ]
(() => {
const $ul = document.querySelector('ul');
let $li;
let count = $ul.children.length;
document.addEventListener('scroll', () => {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
$li = $ul.appendChild(document.createElement('li'));
$li.textContent = ++count;
}
})
})();
결과적으로 구현은 잘되었다. 또한 코드들이 매우 직관적이다. 스크롤 이벤트 걸어서 원하는 동작을 시키면 되니까.. 하지만 스크롤 이벤트는 스크롤이 되는 순간 이벤트에 걸어두었던 모든 작업들이 실행된다. 요소 한두개이면 상관 없겠지만 백개, 천개를 생각하면 브라우저에 과부화가 걸릴 것이다.
그래서 추천하는 방법이 바로 인터섹션 옵저버이다!
인터섹션 옵저버는 스크롤 되는 순간순간마다 이벤트를 확인하는 것이 아닌 내가 지켜보고자 하는 대상을 등록시켜 그 대상이 상위 요소 혹은 최상위 도큐먼트인 viewport와의 교차 영역에 대한 변화를 감지할 수 있도록 하는 방법이다.
간단하게 문법 설명을 해보자면,
인터섹션 옵저버를 생성한다.
const io = new IntersectionObserver(callback, options);
지켜볼 요소를 매개변수에 넣어 호출한다.
io.observe('요소')
callback함수 내에서 주어진 매개변수로 하고자 하는 일을 구현한다.
끝이다..!
인터섹션 옵저버를 더 잘 사용하기 위해 세부기능을 소개하고자 한다.
하단 내용은 인터섹션 옵저버가 잘 정리되어 있는 HEROPY님의 블로그를 참고했다.
관찰할 대상이 등록되거나 가시성에 변화가 생기면 관찰자는 콜백을 실행한다. 매개변수 (entries, observer)
를 가진다.
const io = new IntersectionObserver((entries, observer) => {}, options)
boundingClientRect
: 관찰 대상의 사각형 정보(DOMRectReadOnly)intersectionRect
: 관찰 대상의 교차한 영역 정보(DOMRectReadOnly)intersectionRatio
: 관찰 대상의 교차한 영역 백분율(intersectionRect 영역에서 boundingClientRect 영역까지 비율, Number)isIntersecting
: 관찰 대상의 교차 상태(Boolean)rootBounds
: 지정한 루트 요소의 사각형 정보(DOMRectReadOnly)target
: 관찰 대상 요소(Element)time
: 변경이 발생한 시간 정보(DOMHighResTimeStamp)콜백이 실행되는 해당 인스턴스를 참조한다.
{root: document.getElementById('my-viewport')}
margin
을 이용해 root범위를 확장하거나 축소 가능{rootMargin: '200px 0px'}
{threshold: 0}
viewport와 타켓이 교차하는 순간{threshold: 0.3}
viewport와 타켓이 30%일 교차했을 때{threshold: 1}
viewport에 타켓이 모두 교차했을 때observe()
unobserve()
disconnect()
그렇게 인터섹션 옵저버를 참고해서 구현해 본 무한스크롤!
[ js ]
(() => {
const $ul = document.querySelector('ul');
let $li = document.querySelector('li:last-child');
let count = $ul.children.length;
// 1. 인터섹션 옵저버 생성
const io = new IntersectionObserver((entry, observer) => {
// 3. 현재 보이는 target 출력
const ioTarget = entry[0].target;
// 4. viewport에 target이 보이면 하는 일
if (entry[0].isIntersecting) {
console.log('현재 보이는 타켓', ioTarget)
// 5. 현재 보이는 target 감시 취소해줘
io.unobserve($li);
// 6. 새로운 li 추가해
$li = $ul.appendChild(document.createElement('li'));
$li.textContent = ++count;
// 7. 새로 추가된 li 감시해!
io.observe($li);
}
}, {
// 8. 타겟이 50% 이상 보이면 해줘!
threshold: 0.5
});
// 2. li감시해!
io.observe($li);
})();
이벤트 스크롤과 다른 점이라면 이벤트 스크롤은 페이지의 길이와 해당 요소의 길이를 비교했다면 인터섹션 옵저버는 추가되는 마지막 요소만을 감지하며 마지막 요소가 50% 교차하면 함수를 실행시켰다.(해당 이미지 스크롤을 잘보면 차이가 난다👀)
스크롤에 따라 계속 실행되는 것이 아니다보니 브라우저 최적화에도 도움이 될 것이다. 다만, 인터넷 익스플로어에서 지원을 안한다.(폴리필을 이용하면 가능)
🧚♀️
처음 접했을 때, 어떻게 접근 해야 하는지 잘 몰랐는데 하나하나 삽질?해보면서 하니 감이 훨씬 빠르게 익혔다. 처음에 리액트 된 무한 스크롤을 따라 해보려니 이리꼬이고 저리꼬였던 기억이 남는다.
오히려 바닐라로 처음부터 개념을 잡는 시간이라 더 좋았던거 같다. 다음번엔 더 활용해서 리액트에서 비동기 통신으로 무한스크롤 구현해봐야겠다.. 희희
📝 Reference
좋은 글 감사합니다~^^
식별자를 $로 시작하게 하는 특별한 이유가 있나요? 변수명이 $로 시작하면 어떤 이점이 있나요?