인스타그램 클론에 리액트를 씌우면서 infinite scroll(스크롤이 내려감에 따라 요소가 계속 추가되는 것)을 구현하고 싶었다. 사실 그동안 click, input, change 같은 이벤트만 주구장창 쓰고 scroll은 한 번도 사용한 적 없기 때문에... 구글링으로 어찌어찌 하긴 했는데😏 내 걸로 만들기 위해서 제대로 해 보는 정리!
페이지 전체를 요소로 잡으면 세 property 모두 document.documentElement.___로 접근할 수 있음!
만약 브라우저 창의 크기를 조정하면 clientHeight는 변화하지만, scrollHeight은 컨텐츠의 높이이기 때문에 변하지 않는다.
그리고 스크롤 이벤트가 발생해도 scrollHeight과 clientHeight은 불변인 반면, scrollTop은 스크롤바가 맨 위에 있다면 0, 맨 아래에 도달하면 (scrollHeight-clientHeight)가 된다. (clientTop과 같은 테두리가 없다고 가정할 때!)
그래서 윈도우에 addEventListener로 스크롤 이벤트를 감지하고, 스크롤이 맨 밑에 도달했을 때, 즉 scrollTop >= scrollHeight-clientHeight이 되면 다시 fetch함수로 추가 데이터를 불러오도록 하면 됨!
처음부터 차근차근 코드로 풀어보자.
- 우선 state에fetch된 총 feed data를 담을 빈 배열(='feedData')과 초기에(컴포넌트가 마운트될 때) fetch될 feed의 수(='items'), 그리고 fetch가 일어날 때마다 누적될 feed의 수(='preItems')를 정의한다.
this.state = { feedData: [], items: 2, preItems: 0, };
- componentDidMount에서는 slice를 이용해 컴포넌트가 마운트될 때 설정한 수만큼 데이터를 fetch해오도록 하고, setState로 feedData에 담아 준다.
그리고 스크롤 이벤트를 감지할 수 있도록 window에 eventListner를 걸어 주고, 이벤트가 발생하면 infiniteScroll이라는 함수를 호출하도록 한다.componentDidMount() { fetch('http://localhost:3000/data/Doeun/articleData.json', { method: 'GET', }) .then(res => res.json()) .then(res => { let dataToAdd = res.slice(this.state.preItems, this.state.items); this.setState({ feedData: [...this.state.feedData, ...dataToAdd], }); }); window.addEventListener('scroll', this.infiniteScroll); }
- 그리고 위에서 설명했던 원리에 따라 infiniteScroll을 정의해 준다. 우선 documentElement의 scrollHeight, scrollTop, clientHeight를 구조분해할당해 주고, scrollTop이 scrollHeight-clientHeight보다 같거나 커질 때(=스크롤이 맨 아래에 도달할 때) preItems를 items로 갱신하고, items에는 새로 추가할 데이터의 개수를 더한 뒤 다시 데이터를 fetch하도록 한다.
infiniteScroll = () => { const { scrollHeight } = document.documentElement; const { scrollTop } = document.documentElement; const { clientHeight } = document.documentElement; if (scrollTop >= scrollHeight - clientHeight) { this.setState({ preItems: this.state.items, items: this.state.items + 1, }); fetch('http://localhost:3000/data/Doeun/articleData.json', { method: 'GET', }) .then(res => res.json()) .then(res => { let dataToAdd = res.slice(this.state.preItems, this.state.items); this.setState({ feedData: [...this.state.feedData, ...dataToAdd], }); }); } };
- 마지막으로 컴포넌트가 언마운트될 때 eventListner를 지워 준다.
componentWillUnmount() { window.removeEventListener('scroll', this.infiniteScroll); }
그런데 위에서 componentDidMount와 infiniteScroll 내부에 fetch부분이 중복되기 때문에! getData라는 함수로 대체해 주면 최종적으로 아래처럼 정리할 수 있다.
componentDidMount() {
this.getData();
window.addEventListener('scroll', this.infiniteScroll);
}
infiniteScroll = () => {
const { scrollHeight } = document.documentElement;
const { scrollTop } = document.documentElement;
const { clientHeight } = document.documentElement;
if (scrollTop >= scrollHeight - clientHeight) {
this.setState({
preItems: this.state.items,
items: this.state.items + 1,
});
this.getData();
};
getData = () => {
fetch('http://localhost:3000/data/Doeun/articleData.json', {
method: 'GET',
})
.then(res => res.json())
.then(res => {
let dataToAdd = res.slice(this.state.preItems, this.state.items);
this.setState({
feedData: [...this.state.feedData, ...dataToAdd],
});
});
}
componentWillUnmount() {
window.removeEventListener('scroll', this.infiniteScroll);
}
사실 페이지 전체를 다루기 위해서는 document.documenElement의 scrollHeigt/scrollTop/clientHeigth만 고려하는 것이 위험하다고 해서 document.body의 값과 비교해 더 큰 값을 사용하도록 하긴 했는데... 내 쪼그만 클론 페이지에서는 이 부분이 딱히 문제가 되지 않아서 뭐가 위험한지 아직 정확히 체감하지 못했다....😶 더 공부가 필요할 것 같다~~~!
덕분에 스크롤 이벤트 구현하는데 많은 도움 되었습니다 도은님~! 👍👍