이번 포스트에서는 리액트에서 무한스크롤 이벤트를 구현해보겠다.
IntersectionObserver이란?
타겟엘리먼트와 타겟의 부모 혹은 상위엘리먼트의 뷰포트가 교차되는 부분을 비동기적으로 관찰하는 API
먼저 리액트 앱을 설치한다.
$ npx create-react-app [프로젝트 명]
1. App.js 파일을 수정해서 Item을 여러개와 Item 요소들이 들어갈 Itemlist를 만들어준다.
App.js
import "./App.css";
import styled from "styled-components";
import { useState } from "react";
const ItemWrap = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
align-items: center;
.Item {
width: 350px;
height: 300px;
display: flex;
flex-direction: column;
background-color: #ffffff;
margin: 1rem;
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
border-radius: 6px;
}
`;
function App() {
const [itemList, setItemList] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
return (
<div className="App">
<ItemWrap>
{itemList.map((item, index) => (
<div className="Item" key={index}>{index+1}</div>
))}
</ItemWrap>
</div>
);
}
export default App;
이런 화면이 될 것이다.
2. 데이터를 받아오는 중에 로딩컴포넌트를 보여주기위해 다음을 설치하고 코드에 추가해준다.
또한 target State와 observer의 관찰 대상이 될 target Element를 최하단에 생성해준다.
$ npm i react-loading
App.js
import ReactLoading from "react-loading";
...
const LoaderWrap = styled.div`
width: 100%;
height: 80%;
display: flex;
justify-content: center;
text-align: center;
align-items: center;
`;
...
function App() {
const [itemList, setItemList] = useState([1, 2, 3, 4, 5]); // ItemList
const [target, setTarget] = useState(""); // target
const [isLoding, setIsLoding] = useState(false); // isloding
return (
<div className="App">
<ItemWrap>
{itemList.map((item, index) => (
<div className="Item" key={index}>
{index + 1}
</div>
))}
</ItemWrap>
{isLoding ? (
<LoaderWrap>
<ReactLoading type="spin" color="#A593E0" />
</LoaderWrap>
) : (
""
)}
<div ref={setTarget}></div>
</div>
);
}
export default App;
3. 이제 Intersection Observer를 사용하기위해 useEffect를 선언하고 Intersection Oberver의 인자로 쓰일 함수를 선언하며, option을 지정해준다.
App.js
function App() {
const [itemList, setItemList] = useState([1, 2, 3, 4, 5]); // ItemList
const [target, setTarget] = useState(""); // target
const [isLoding, setIsLoding] = useState(false); // isloding
const onIntersect = async ([entry], observer) => {
if (entry.isIntersecting && !isLoding) {
observer.unobserve(entry.target);
setIsLoding(true);
// 데이터를 가져오는 부분
setIsLoding(false);
observer.observe(entry.target);
}
};
useEffect(() => {
let observer;
if (target) {
// callback 함수, option
observer = new IntersectionObserver(onIntersect, {
threshold: 0.4,
});
observer.observe(target); // 타겟 엘리먼트 지정
}
return () => observer && observer.disconnect();
}, [target]);
return (
<div className="App">
<ItemWrap>
{itemList.map((item, index) => (
<div className="Item" key={index}>
{index + 1}
</div>
))}
</ItemWrap>
{isLoding ? (
<LoaderWrap>
<ReactLoading type="spin" color="#A593E0" />
</LoaderWrap>
) : (
""
)}
<div ref={setTarget}></div>
</div>
);
}
useEffect 함수 안에 보면 위에 선언한 callback함수와 option을 deps로 넣어준 것을 볼 수 있는데,
조금 더 자세히 알아보면
※ Intersection Oberver 첫 번째 deps : callback()
- callback : 타겟 엘리먼트가 교차되었을 때 실행할 함수
※ Intersection Oberver 두 번째 deps : root, rootMargin, threshold
- root
- default : null, 브라우저의 viewport
- 교차영역의 기준이 될 root 엘리먼트. observer의 대상으로 등록할 엘리먼트는 반드시 root의 하위 엘리먼트여야 한다.
- rootMargin
- default : '0px 0px 0px 0px'
- root 엘리먼트의 margin.
- threshold
- default : 0
- 0.0부터 1.0 사이의 숫자 혹은 이 숫자들로 이루어진 배열로, 타겟 엘리먼트에 대한 교차 영역 비율을 의미한다.
- 0.0의 경우 타겟 엘리먼트가 교차영역에 진입했을 시점이고, 1.0의 경우 타겟 엘리먼트 전체가 교차영역에 들어왔을 때의 observer를 실행하는 것을 의미한다.
다시 한 번 코드를 살펴보면
useEffect(() => {
let observer;
if (target) {
// callback 함수, option
observer = new IntersectionObserver(onIntersect, {
threshold: 0.4,
});
observer.observe(target); // 타겟 엘리먼트 지정
}
return () => observer && observer.disconnect();
}, [target]);
target 엘리먼트로 지정한 target State가 첫 렌더링 때 생성될 것이고, 첫 렌더링 때와 이 target의 변경이 감지될 때 useEffect가 실행된다. callback 함수로는 위에 선언한 onIntersect 함수이고, option으로 threshold : 0.4를 지정했다.
const onIntersect = async ([entry], observer) => {
if (entry.isIntersecting && !isLoding) {
observer.unobserve(entry.target);
setIsLoding(true);
// 데이터를 가져오는 부분
setIsLoding(false);
observer.observe(entry.target);
}
};
useEffect가 실행되고 callback함수인 onIntersection이 실행된다. target 엘리먼트가 교차지점에 들어오고
isLoding이 false일 때 target 지정을 철회하고 데이터를 가져올 때 동안 isLoding State를 true로 만들어준다.
데이터가 정상적으로 들어오면 isLoding State를 다시 false로 바꿔주고, targetElement를 재지정한다.
데이터를 임의로 가져왔다고 예상하고 itemList에 추가하는 로직을 구현해보자.
const onIntersect = async ([entry], observer) => {
if (entry.isIntersecting && !isLoding) {
observer.unobserve(entry.target);
setIsLoding(true);
// 데이터를 가져오는 부분
await new Promise((resolve) => setTimeout(resolve, 2000));
let Items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
setItemList((itemLists) => itemLists.concat(Items));
setIsLoding(false);
observer.observe(entry.target);
}
};
DB를 조회할 땐 그 만큼의 시간이 걸릴테지만 지금은 로딩컴포넌트를 보여주기위해 Timeout을 2초 설정했다.
- target Element가 첫 렌더링 될 때와 설정한 교차지점에 진입·들어왔을 때 observer 객체가 새로 생성된다.
- observer 객체가 callback함수와 option을 가진 채로 생성된다.
- observer 객체가 새로 생성될 때 callback 함수가 실행되어 데이터를 가져오거나 하는 로직이 실행된다.
- Itemlist가 수정되어 더 많아지게되고, target Element가 재지정되어 다시 최 하단으로 설정된다.
전체코드
App.js
import "./App.css";
import styled from "styled-components";
import { useState } from "react";
import { useEffect } from "react";
import ReactLoading from "react-loading";
import axios from "axios";
const LoaderWrap = styled.div`
width: 100%;
height: 80%;
display: flex;
justify-content: center;
text-align: center;
align-items: center;
`;
const ItemWrap = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
align-items: center;
.Item {
width: 350px;
height: 300px;
display: flex;
flex-direction: column;
background-color: #ffffff;
margin: 1rem;
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
border-radius: 6px;
}
`;
function App() {
const [itemList, setItemList] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); // ItemList
const [target, setTarget] = useState(""); // target
const [isLoding, setIsLoding] = useState(false); // isloding
const onIntersect = async ([entry], observer) => {
if (entry.isIntersecting && !isLoding) {
observer.unobserve(entry.target);
setIsLoding(true);
// 데이터를 가져오는 부분
await new Promise((resolve) => setTimeout(resolve, 2000));
let Items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
setItemList((itemLists) => itemLists.concat(Items));
setIsLoding(false);
observer.observe(entry.target);
}
};
useEffect(() => {
let observer;
if (target) {
// callback 함수, option
observer = new IntersectionObserver(onIntersect, {
threshold: 0.4,
});
observer.observe(target); // 타겟 엘리먼트 지정
}
return () => observer && observer.disconnect();
}, [target]);
return (
<div className="App">
<ItemWrap>
{itemList.map((item, index) => (
<div className="Item" key={index}>
{index + 1}
</div>
))}
</ItemWrap>
{isLoding ? (
<LoaderWrap>
<ReactLoading type="spin" color="#A593E0" />
</LoaderWrap>
) : (
""
)}
<div ref={setTarget}></div>
</div>
);
}
export default App;
실행 화면
Next.js typescript 에서는 타겟 엘리먼트를 다음과 같이 선언해주자.
<div ref={(e: any) => { setTarget(e) }}></div>
'Front-End > React.js' 카테고리의 다른 글
React.js | regeneratorRuntime is not defined 에러 (0) | 2021.11.25 |
---|---|
React.js | React-Day-Picker (0) | 2021.11.23 |
React.js | CSS, SASS, SCSS (0) | 2021.11.02 |
React.js | API 연동 | 마지막 (0) | 2021.10.08 |
React.js | API 연동 | Context 와 함께 사용하기 (0) | 2021.10.07 |