무한 스크롤이란?
- 컨텐츠를 페이징하는 기법 중 하나.
- 아래로 스크롤하다 컨텐츠의 마지막 요소를 볼 즈음 다음 컨텐츠가 있으면 불러오는 방식.
- 페이지 로딩 시간을 줄이기 위함.
- 페북, 트위터, 인스타 등의 SNS에서 주로 사용됨.
구현 방식
- window의 scroll 이벤트를 통해 스크롤링이 일어날 때마다 화면 전체의 height와 스크롤 위치를 통해 스크롤이 컨텐츠 끝 즈음에 다다랐는지 체크해서 처리하는 방식.
new
intersection observer 방식 - 매번 스크롤 감지가 아닌 지정한 DOM 객체가 시야에 닿았는지를 확인.
왜 App.js와 main.js를 분리할까?
→ 컴포넌트를 불러올 root가 필요함. ex) 여러 개의 투두리스트를 만들 경우 app을 여러 개 불러줘야 함.
→ 컴포넌트를 선언하는 부분과 만들어서 실행하는 부분의 분리한 것임.
기본 사항

- App - 데이터를 불러오고
photoList
를 생성함.
- photoList - app에서 받은 데이터로 스크롤에 닿았으면
onScrollEnded
호출. 데이터를 요청함.
- 이 둘이 반복되며 무한 스크롤 생성.
- 사용할 API 구조
- limit - 한 번에 가져올 개수.
- start : 어디서부터 가져올지.
흐름
스크롤을 이용해 이미지 컨텐츠에 끝에 닿는지 체크하는 로직 구현 → onScrollEnded 호출 → 다음 데이터 호출 → 불러온 데이터와 원래 있던 데이터를 합침 → 합쳐진 데이터를 PhotoList로 내림.
isLoading으로 버튼 중복 클릭 방지
- 데이터 전송이 느린 환경에서 중복으로 load 버튼 클릭을 방지하기 위함.
- App.js에서 this.state의 조건으로 false를 지정.
- 컴포넌트 생성자를 만들 때와 setState 메소드에
isLoading
을 추가함.
fetchPhotos
를 호출할 때 this.setState로isLoading
을 true, 완료 후 false로 다시 지정함.
- PhotoList.js에서 addEventListener에 조건으로 추가함.
방식 1 - scroll 이벤트를 이용해 계산
scroll 이벤트
document.body.offsetHeight
: 전체 렌더링된 높이를 구할 수 있음.
innerHeight
(현재 높이) +scrollY
: 전체 컨텐츠에서 스크롤이 얼마나 됐는지.
window.innerHeight + window.scrollY >= document.body.offsetHeight
로 스크롤이 화면 끝에 닿았는지를 알 수 있음.
- 다만 상단 코드는 무조건 끝에 닿았을 경우만 true 값을 주기 때문에
(window.innerHeight + window.scrollY) + 100 >= document.body.offsetHeight
로 100px정도 추가해주면 좋음.
수식이 들어가 있는 값은 변수로 빼면 조건문에서 파악하기 쉬움.
ex) const isScrollEnded = ~~~, if(isScrollEnded) { ~~~
무한 스크롤 vs 인터렉션을 통한 로딩
→ 무한 스크롤 시 footer의 접근이 어렵기 때문에 상황에 따라 버튼 클릭으로 스크롤 이벤트를 발생시켜야 함. 두 방식 모두 주체만 다를 뿐 방식은 같음.
이벤트 지연
- 주로 스크롤 이벤트에서 많이 사용함.
- 사용하면 퍼포먼스가 향상됨.
- debouncing은 이벤트가 발생된 것을 지연시키는 반면, 스로틀링(Throttling)은 최초 발생 이벤트 이후 일정 시간동안 들어온 같은 이벤트는 무시하는 방법임.
- debouncing은 마지막 페이지에서 계속 스크롤 요청시 방어하기 좋음.
화면 끝에 닿았을 때 API 호출이 계속되는 문제
- 컨텐츠의 수가 무한대면 큰 의미가 없을 수도 있지만, 개수가 적은 경우 꼭 처리를 해야 함.
- 어떻게 체크하지?
- 5개씩 불러온 데이터에서 다음 데이터가 5개보다 작은 경우 없다고 판단하는 것(마지막 데이터가 딱 5개인 경우 체크할 수 없다는 문제점 발생).
- 컨텐츠 전체 개수를 불러오는 API와 현재 내가 불러온 API 개수를 비교하는 방법.
- App.js에서 this.state의 조건으로
totalCount:0
을 지정.
- 컴포넌트 생성자를 만들 때
totalCount
를 추가함.
- API에서 전달해주는 값이기 때문에
request
를 요청해 반환받은 값으로 setState를 함.
const init = async () => { const totalCount = await request("/cat-photos/count"); this.setState({ ...this.state, totalCount, }); await fetchPhotos(); }; init();
- PhotoList.js에서 scroll 이벤트에
photos.length < totalCount
라는 조건을 추가하여 해당 조건을 충족할 경우에만onScrollEnded
를 실행.
방식 2 -intersection observer
- observer를 이용해 가장 마지막에 있는 li가 화면에 들어왔을 때 감지하여 다음 페이지를 불러옴.
const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { //isLoading 방어코드 잊지말기! if (entry.isIntersecting && !this.state.isLoading) { console.log("end of Page", entry); onScrollEnded(); } }); }, { threshold: 0, } );
- threshold는 비율을 계산에 어느 정도 비율에 왔을 때 observer를 실행할지 판단함. 0.5로 넣으면 사진의 절반에 다다랐을 때 함수를 실행함.
- li가 붙어서 불러오기 때문에 스크롤이 끝난 것으로 인식할 수 있음. 따라서
min-height:500px;
같은 style 값을 붙여야 함. 이때 사진의 크기가 규칙하다면 계산, 사진의 크기가 불규칙하다면 이미지 사이즈에 대한 api를 받음. 아니면 image holder를 넣어 이미지 크기를 바꾸는 방식으로 코드를 짜는 경우도 있음.
- 마지막 li가 뭔지 파악하기 위해
let $lastLi = null;
이란 변수를 생성하고
- render 함수 마지막에 하단 코드를 삽입함.
const $nextLi = $photos.querySelector("li:last-child"); if ($nextLi !== null) { //방어 코드 if ($lastLi !== null) { observer.unobserve($lastLi); } $lastLi = $nextLi; observer.observe($lastLi); } };
- 화면 끝에 닿지 않아도 화면 끝으로 인식하는 경우가 생김. observer로 삽입했던 $lastLi에 대한 값이 남아 있기 때문에
unobserve
로 값을 빼줘야 함.
리펙토링 (실패함)
const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting && !this.state.isLoading) { observer.unobserve(entry.target); onScrollEnded(); } }); }, { threshold: 0.5, } ); //render 함수 마지막 부분에 위치 const $lastLi = $photos.querySelector("li:last-child"); if ($lastLi !== null) { observer.observe($lastLi); }
- isLoading이랑 겹쳐 버그가 발생할 수 있음.