๋ฆฌ์กํธ ์์์ Intersection Observer API๋ฅผ ์ฌ์ฉํ์ฌ Infinite Scroll ๊ธฐ๋ฅ ๊ตฌํ
Infinite Scroll ๊ธฐ๋ฅ์ ํ์์ฑ
์๋๋ ์งํํ๋ ํ๋ก์ ํธ์ ๋ฉ์ธ ํ์ด์ง ๋ด์์ ์ถ๊ฐ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์์ ๊ฐ์ ธ์ ๋๋๋งํ๋ ๋ฐฉ์์ Carousel Slide ์์ง๋ง ๊ด๋ จ ๋์์ธ ์์์ด ๋ํญ ์์ ๋์๋ค.
์ด์ ๋ฐ๋ผ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ฐฉ์์ด ํน์ ์ง์ ์ ๋๋ฌํ์ ๋, ์ด๋ฅผ ์ธ์ํด ์๋ฒ์์ ์ถ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ Infinite Scroll ๋ฐฉ์์ ์ฌ์ฉํ๊ธฐ๋ก ํ์๋ค.
์ฒ์์๋ ํน์ ์ง์ ์ ๊ณ์ฐํ๊ธฐ ์ํด ScrollingElement ๋ด์ scrollTop๊ฐ์ ์ฌ์ฉํ์์ง๋ง, ์คํฌ๋กค์ด ๋๋ ์๋์ ๋ฐ๋ผ ๊ฐ์ด ํญ์ ๋ฐ๋๋ ์ด์์ scroll event ์์ฒด์ ์ฑ๋ฅ ์ ํ ๋ฌธ์ (๋๊ธฐ์ ์ผ๋ก ์ด๋ฒคํธ ๋ฐ์ํ ๋๋ง๋ค ํธ์ถ๋๋ ์ฝ๋ฐฑ ํจ์)๋ก ์ธํด Intersection Observer API๋ฅผ ์ฌ์ฉํด ๋ณด๊ธฐ๋ก ํ๋ค.
Intersection Observer API
https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API
Intersection Observer API - Web API | MDN
Intersection Observer API๋ ํ๊ฒ ์์์ ์์ ์์ ๋๋ ์ต์์ document ์ viewport ์ฌ์ด์ intersection ๋ด์ ๋ณํ๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ๊ด์ฐฐํ๋ ๋ฐฉ๋ฒ์ ๋๋ค.Intersection Observer API๋ ํ๊ฒ ์์์ ์์ ์์ ๋๋
developer.mozilla.org
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
Intersection Observer API ๋ ๋ฃจํธ ์์์ ํ๊ฒ ์์์ ๊ต์ฐจ์ ์ ๊ด์ฐฐํ๊ณ ํ๊ฒ ์์๊ฐ ๋ฃจํธ ์์์ ๊ต์ฐจํ๋์ง ์๋์ง๋ฅผ ๊ตฌ๋ณํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
๋ณธ ํ๋ก์ ํธ์์๋ ๋ ๋จ์ ๋๋ฌํ์ ๊ฒฝ์ฐ์๋ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ์ด๋ฏ๋ก, ํ๊ฒ์ ํด๋นํ๋ ์์๋ฅผ mainForm ์ปดํฌ๋ํธ์ ์ถ๊ฐํ๋ค.
<div ref={target} className="targetElement">{isLoaded && <Loader />}</div>
- useRef๋ฅผ ํตํด ํด๋น ์์๋ฅผ ํ๊ฒํ ํ์ฌ ์ดํ useEffect Hook์์ ์ปดํฌ๋ํธ ๋ง์ดํธ ๋ฐ ์ ๋ฐ์ดํธ ์ Intersection Observer API ์ธ์คํด์ค ์์ฑ
- Intersection Observer API ์ธ์คํด์ค ์์ฑ ์ ์ธ์ ๊ฐ์ผ๋ก ๋๊ฒจ์ฃผ๊ธฐ ์ํด ํด๋์ค๋ช ์ง์
- ํน์ ์ง์ ์ ๋ฟ์ ๊ฒฝ์ฐ, ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ฉฐ ์ด๋ ๋น๋๊ธฐ๋ก ์ผ์ด๋๊ธฐ ๋๋ฌธ์ ์๊ฐ์ด ์์๋ ์ ์๊ณ ์ด๋ฅผ ์ํด ์ฌ์ฉ์์๊ฒ ๋ก๋ฉ UI๋ฅผ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด isLoaded state๊ฐ์ด true์ผ ๊ฒฝ์ฐ์ Loader ์ปดํฌ๋ํธ๋ฅผ ๋ณด์ฌ์ฃผ๋๋ก ์ค์
Loader ์ปดํฌ๋ํธ๋ ํ๋ก์ ํธ ์ ๋ฐ์ ์ฐ์ด๊ธฐ ๋๋ฌธ์ react-loading์ ์ฌ์ฉํ์ฌ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๋์ ๋ชจ๋๋ก ์์ฑํด๋์๋ค.
https://www.npmjs.com/package/react-loading
react-loading
React loading component
www.npmjs.com
import { memo } from "react";
import ReactLoading from "react-loading";
import styled from "styled-components";
const Loader = () => {
return (
<Container>
<ReactLoading type="spin" color="#262f6a" /> //๋ก๋ฉ ํ์
๊ณผ ์์์ props๋ก ์ง์
</Container>
);
};
const Container = styled.div`
width: 100%;
height: 80%;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
`;
export default memo(Loader);
๋ค์์ผ๋ก ํด๋น ํ๊ฒ ์์์ ๋ํ Intersection Observer API ์ธ์คํด์ค๋ฅผ ์์ฑํด์ฃผ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ์๋ค.
useEffect(() => {
if (isIntersect && isLoaded) return;
let observer;
if (target.current) {
observer = new IntersectionObserver(onIntersect);
observer.observe(target.current);
}
return () => observer && observer.disconnect();
}, [target.current, isIntersect, isLoaded]);
Container Component์ธ mainFormContainer ๋ด์์ ์ ๋ฌ ๋ฐ์ isLoaded state๊ฐ๊ณผ isIntersect ๊ฐ์ด true์ผ ๊ฒฝ์ฐ์๋ Oberver Intersection API ์ธ์คํด์ค๋ฅผ ์์ฑํ๊ณ observer ๋ฉ์๋๊ฐ ์คํ๋๋ ์ฐ์ฐ ๊ณผ์ ์ ๋ฐฉ์งํจ์ผ๋ก์จ, ๊ต์ฐจ์ ์ ๋ฟ์์ ๊ฒฝ์ฐ์ ํ์ ๊ณผ์ ์ด ์ฐ๋ฌ์ ์ผ์ด๋์ง ์๋๋ก ํ์๋ค.
(target.current๋ฅผ ์์กด์ฑ ๋ฐฐ์ด์ ๋ฃ์์ ๊ฒฝ์ฐ์, ์ฝ์์ waring message๋ก ์์์ ์ผ๋ก ๋ฐ๋๋ ๊ฐ์ด๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ์ฌ์ฉํ์ง ๋ง๋ผ๊ณ ๋ฌ๋ค. ์ดํ ๋ฆฌํฉํ ๋ง ์ ํด๋น ๋ถ๋ถ ๋ํ ๋ฆฌํฉํ ๋งํ ์๊ฐ์ด๋ค)
๊ทธ๋ฆฌ๊ณ ์ธ์คํด์ค ์์ฑ ์ ํ ๋นํด์ค ์ฝ๋ฐฑ ํจ์์ธ onIntersect๋ ์ค์ redux-saga๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์ญํ ์ ํ๋ container component์ธ mainFormContainer์ ์ ์ธํด ์ฃผ์๋ค.
const onIntersect = async ([{ isIntersecting, target }], observer) => {
if (isIntersecting) {
observer.unobserve(target);
dispatch(increaseOffset());
setIntersect(true);
}
};
entries ๋ IntersectionObserEntry ์ธ์คํด์ค๋ฅผ ๋ด์ ๋ฐฐ์ด์ด๋ฉฐ ์์ callback func์ ์ฒซ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ์ด ๋๋ค.
ํด๋น ํค ๊ฐ ์ค ๊ต์ฐจ์ ์ด ๋์๋์ง ์๋ณํ ์ ์๋ ๊ฐ์ธ isIntersecting๊ณผ ํ๊ฒ ์์๋ฅผ ๋์คํธ๋ญ์ณ๋ง ํ ๋น์ ํตํด ํ์์ ๋ง๊ฒ ๊ฐ์ ธ์๋ค.
๋ง์ฝ์ isIntersection ๊ฐ์ด true์ผ ๊ฒฝ์ฐ์๋ ์ฐ์ ํ๊ฒ ์์๋ฅผ unobserver ๋ฉ์๋๋ฅผ ํตํด ํ ๋น์ ํด์ ํด์ฃผ์ด ์ค๋ณต์ ๋ฐฉ์งํ์๋ค.
๊ทธ๋ฆฌ๊ณ dispatch()๋ฅผ ํตํด ์ก์ ์์ฑ ํจ์๋ฅผ ๋ฐ์์์ผ currentOffset ๊ฐ์ ๊ฐ์ฐํด ์ฃผ์๋ค.
useSelector๋ฅผ ํตํด redux-store ๋ด์์ currentOffset๊ฐ์ ์ฐธ์กฐํ๊ณ ํด๋น ๊ฐ์ด ์ ๋ฐ์ดํธ ๋์์ ๊ฒฝ์ฐ, ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋๋ก useEffect ๋ด์ ๊ด๋ จ ํจ์๋ฅผ ์ถ๊ฐํ์๋ค.
const currentOffset = useSelector(({ mainForm }) => mainForm.currentOffset);
(...)
useEffect(() => {
async function getMoreContents() {
if (!isIntersect) return;
dispatch(isLoaded());
await new Promise(resolve => setTimeout(resolve, 1500));
if (sort === "friends") {
batch(() => {
dispatch(friends(currentOffset));
});
} else {
batch(() => {
dispatch(content({ sort, currentOffset }));
dispatch(rooms({ sort, currentOffset }));
});
}
dispatch(isLoaded());
setIntersect(false);
}
getMoreContents();
}, [isIntersect, currentOffset]);
๊ฒฐ๊ณผํ๋ฉด
์ฐธ๊ณ ํฌ์คํ
Infinite Scroll
Question 1. ์ต์ด์๋ 20๊ฐ์ ๋ชฉ๋ก์ ๋ถ๋ฌ์จ๋ค. 2. ์คํฌ๋กค์ ์ตํ๋จ์ผ๋ก ์ด๋์ 'loading' ์ํ ํ์๊ฐ ๋ํ๋๋ฉฐ, ์ดํ์ 20๊ฐ ๋ชฉ๋ก์ ๋ ๊ฐ์ ธ์จ๋ค. 3. ๋ก๋ฉ ์๋ฃ์ 'loading'ํ์๊ฐ ์ฌ๋ผ์ง๋ฉฐ, ๊ฐ์ ธ์จ ๋ชฉ๋ก
choi95.tistory.com
https://choi95.tistory.com/178
ํน์ ๊ฐ์ Entry ์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ_Infinite Scroll
๊ตฌํ ๊ฒฐ๊ณผ ๋ค์๊ณผ ๊ฐ์ด Scroll๊ฐ์ด ํ๋ฉด ์์ ๋ ์ง์ ์ ๋๋ฌํ์ ๊ฒฝ์ฐ์ ์ถ๊ฐ์ ์ผ๋ก ์๋ฒ์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ์ฌ ํ๋ฉด์ ๋๋๋ง๋๋๋ก ๊ธฐ๋ฅ ๊ตฌํํ์๋ค. ๋ฆฌ์กํธ ์์์ ์ ์ญ ๊ฐ์ฒด(Window)์ ์คํฌ๋กค
choi95.tistory.com
Debounce ํจ์๋ก ์ด๋ฒคํธ ์ฒ๋ฆฌ ์ต์ ํ
https://choi95.tistory.com/56 Infinite Scroll Question 1. ์ต์ด์๋ 20๊ฐ์ ๋ชฉ๋ก์ ๋ถ๋ฌ์จ๋ค. 2. ์คํฌ๋กค์ ์ตํ๋จ์ผ๋ก ์ด๋์ 'loading' ์ํ ํ์๊ฐ ๋ํ๋๋ฉฐ, ์ดํ์ 20๊ฐ ๋ชฉ๋ก์ ๋ ๊ฐ์ ธ์จ๋ค. 3. ๋ก๋ฉ ์๋ฃ..
choi95.tistory.com