들어가기
이전에는 최대한 CSS를 사용해서 캐러셀 스크롤을 기능을 만들었다면 이번에는 직업 스크롤 기능 자체를 구현해보았다. 이전 구현에는 동작 자체는 문제가 없지만 스크롤링 동작에서 부드럽지 못한 부분이 있었는데 이 부분을 직접 구현해서 해결해보았다. 조금 복잡할지는 모르지만 최대한 간단하게 작성하도록 노력하였다.
작성자: http://ospace.tistory.com/ (ospace114@empal.com)
기본 HTML 구성
먼저, 이전에 구현했던 부분을 가져왔고 필요없는 곳을 제거했다. 먼저 HTML 페이지는 크게 변경되는 부분이 없이 그대로 사용했다. 단순히 목록 태그를 사용해서 10개 아이템을 추가했다.
<div class="controller">
<ul class="container">
<li>Item 01</li>
<li>Item 02</li>
<li>Item 03</li>
<li>Item 04</li>
<li>Item 05</li>
<li>Item 06</li>
<li>Item 07</li>
<li>Item 08</li>
<li>Item 09</li>
<li>Item 10</li>
</ul>
</div>
다음으로 CSS 부분이다. controller와 container에 대한 스타일을 지정하고 목록 태그에 있는 아이템에 대해서도 적절하게 스타일을 적용했다.
.controller {
height: 160px;
margin: 10px;
overflow-y: hidden;
border: 1px solid black;
}
.container {
list-style: none;
margin: 0;
padding: 0;
}
.container > li {
padding: 12px 16px;
border: 1px solid #f0f0f0;
}
이전에 적용했던 스냅(snap) 관련 속성은 제거 했다. 그리고 overflow-y 속성은 hidden으로 설정하여 controller 영역을 넘치는 container 부분을 숨겨서 표시되지 않도록 했다. 이는 나중에 직접 container 위치를 변경해서 스크롤처럼 보이도록 처리할 예정이다.
무한 스크롤 기능 구현
Javascript을 사용해서 구현하는 부분이다. 스크롤 동작과 무한 스크롤 및 스냅 기능을 구현하였다.
기본 스크롤 기능
앞의 HTML 코드만으로는 스크롤이 동작하지 않는다. 먼저 간단하게 스크롤부터 구현해보자. 스크롤 동작은 마우스 휠에 의해 스크롤링이 된다. 마우스 휠 이벤트를 사용해서 스크롤 동작을 구현한다.
const controller = document.querySelector(".controller");
let currentPos = 0;
setScrollPos(currentPos);
function setScrollPos(pos) {
container.style.transform = `translateY(${-pos}px)`;
}
controller.addEventListener("wheel", ({ deltaY }) => {
const pos = currentPos + deltaY * 0.1;
setScrollPos(pos);
currentPos = pos;
});
간단하게 코드를 설명하면 현재 위치를 setScrollPos()에 의해 초기화한다. wheel 이벤트에서 deltaY값을 추출해서 현재 스크롤 위치를 변경한다. 그리고 currentPos에 현재 위치를 저장해둔다. 추후 스크롤 위치 변경시 사용된다.
setScrollPos()에서 transform을 사용하는 이유는 화면 렌더링 성능을 위해서 선택했다. wheel 이벤트에서 wheelDelta는 deprecated이기 때문에 deltaY를 사용하여 currentPos를 변경해서 이동할 스크롤 위치를 지정해야한다.
무한 스크롤 사전 준비
이제 무한 스크롤을 위해서 스크롤되는 아이템 묶음인 페이지를 복제해서 추가한다. 총 3개 페이지가 추가되고 중간 페이지로 기본 위치를 이동한다. 그러면 상하방향으로 연속적인 이동을 시작할 수 있다.
const controller = document.querySelector(".controller");
const container = controller.querySelector(".container");
const items = Array.from(container.querySelectorAll("li"));
container.append(...items.map((it) => it.cloneNode(true)));
container.append(...items.map((it) => it.cloneNode(true)));
const pageHeight = items.reduce((prev, it) => prev + it.offsetHeight, 0);
let currentPos = pageHeight;
setScrollPos(currentPos);
// 중략
코드를 보면 컨테이너에 있는 아이템들을 복제해서 넣는다. 그리고 items에서 모든 요소의 높이 합인 페이지 높이를 pageHeight에 저장하고 currentPos에서 페이지 높이 만큼 저장하여 위치를 이동한다.
무한 스크롤 구현
마지막으로 무한 스크롤를 구현해보자. 이 부분은 이전에 캐러셀 스크롤 구현보다 간단하다. 개념은 현재 컨테이너 영역에서 상단과 하단 일정 영역으로 이동할 경우 페이지 뫂이 만큼 아래로 또는 위로 위치를 이동하면 된다.
const itemsHeight = pageHeight * 3;
const upper = itemsHeight * 0.1;
const lower = itemsHeight * 0.8;
function setScrollPos(pos) {
if (upper > pos) {
pos += pageHeight;
} else if (lower < pos) {
pos -= pageHeight;
}
container.style.transform = `translateY(${-pos}px)`;
return pos;
}
itemsHeight에 컨테이너 전체 높이를 저장하고, upper와 lower에 상단과 하단에 적당한 경계 위치를 구한다. 그리고 setScrollPos()에서 위치를 지정할 때에 상단과 하단 경계를 넘어가는지 확인해서 pageHeight 만큼 더하거나 뻬주면 된다. 한가지 setScrollPos()에서 위치를 수정하기 때만이 위치를 리턴해주었다. 그리고 whell 이벤트 핸들러에서 변경된 위치를 처리해준다.
controller.addEventListener("wheel", ({ deltaY }) => {
const pos = currentPos + deltaY;
currentPos = setScrollPos(pos);
});
setScrollPos()에 변경된 위치를 다시 currentPos인 현재 위치에 저장한다.
테스트하면 상하로 무한 스크롤되면서 부드럽게 위치가 이동된다.
부드럽게 움직이기
마우스 휠을 사용해서 스크롤을 할 경우 페이지가 끈기듯 움직인다. 터치패드 지원하는 일부 모바일 기기에서 두 손가락으로 수직으로 스와이프(Swipe)으로 스크롤하는 기능이 있다. 이때에는 브라우저에서 자동으로 관성 모멘텀이 적용된 스크롤 페이지 위치를 wheel 이벤트로 발생한다. 마우스 휠에서도 사용할 수 있게 좀더 부드러운 움직임을 위해서 직접 구현해보자.
let posDelta = 0;
function goScrollPos(delta) {
posDelta += delta;
function run() {
let delta = posDelta * 0.3;
if (1e-4 > Math.abs(delta)) {
posDelta = 0;
return;
}
currentPos = setScrollPos(currentPos + delta);
posDelta -= delta;
requestAnimationFrame(run);
}
requestAnimationFrame(run);
}
goScrollPos()은 currentPos에서 delta만큼 위치 이동을 부드럽게 애니메이션 처리한다. 실제 wheel 이벤트에 적용해보자.
controller.addEventListener("wheel", ({ deltaY }) => {
goScrollPos(deltaY);
});
테스트해보면 마우스 휠에 의해서 좀더 부드럽게 이동하는 모습을 볼 수 있다.
아이템 스냅
마지막으로 스크롤하다보면 controller의 위쪽에 표시되는 아이템이 중간에 잘려서 보인다. 이를 아이템 상단에 스냅되서 표시하고 싶다. 구현은 간단하다. 이동할 위치에서 가장 가까운 아이템을 찾아서 이동할 위치를 갱신하면 된다.
let posDelta = 0;
function goScrollPos(delta) {
posDelta += delta;
function run() {
const cnt = Math.round((currentPos + posDelta) / itemHeight);
const delta = (cnt * itemHeight - currentPos) * 0.3;
if (1e-4 > Math.abs(delta)) {
posDelta = 0;
return;
}
currentPos = setScrollPos(currentPos + delta);
posDelta -= delta;
requestAnimationFrame(run);
}
requestAnimationFrame(run);
}
현재 위치에서 delta만큼 이동할 위치을 기준으로 가장 가까운 아이템 위치를 찾아서 새로운 delta 값을 계산해서 부드러운 움직임을 구현했다.
아이템 스냅까지 구현하면서 좀더 완성도 있는 무한 스크롤 기능이 구현된듯 하다.
데모
See the Pen Carousel Scroll 2 by ospace (@ospace) on CodePen.
결론
위의 구현에서 부드럽게 움직이게 만드는 부분이 최대한 간략하게 구현했지만 생각보다 시간이 걸렸다. 추가로 예외 처리 및 개선할 부분이 많지만 구조보다 동작에 대해서 집중적으로 다루었다.
지금까지 무한 스크롤하는 기능을 구현해보았다. 물론 직접 구현하는 방식이기에 부가적인 기능들이 추가되어야 한다. 예를 들어, 키보드에서 의한 페이지 업과 다운, 마우스 드레그, 터치 스와프에 의한 스크롤되지 않는다. 현재는 수직방향으로 스크롤만 다루었지만 수평방향 스크롤도 크게 다르지 않을 것이다. 이런 부분은 앞의 구현된 내용을 조금만 응용하면 쉽게 구현할 수 있을 거라 생각한다.
부족한 글이지만 여러분에게 도움이되었으면 하네요. 모두 즐프하세요. ospace.
참조
[1] 간단한 캐러셀 스크롤(Carousel Scroll) 만들기, https://ospace.tistory.com/1009
[2] Element: wheel event, mdn, https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
[3] translateY(), mdn, https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/translateY
'3.구현 > HTML5&Javascript' 카테고리의 다른 글
| 간단한 캐러셀 스크롤(Carousel Scroll) 만들기 (0) | 2025.11.04 |
|---|---|
| [CSS] Flex 사용하기 (2) | 2024.04.02 |
| [javascript] 이미지 동적 로딩 (0) | 2024.03.21 |
| [javascript] VR Panoramic 360 video player 사용 (0) | 2024.03.20 |
| [jquery] 단순 DOM 데이터 바인딩 (0) | 2024.03.19 |
