일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 게임
- jest
- nestjs
- AWS
- MongoDB
- JavaScript
- 정렬
- Queue
- OCR
- 자료구조
- MySQL
- typeORM
- dfs
- Bull
- GIT
- Python
- cookie
- class
- Sequelize
- flask
- react
- TypeScript
- nodejs
- mongoose
- Express
- 공룡게임
- Dinosaur
- Nest.js
- game
- Today
- Total
포시코딩
[IntersectionObserver] 무한 스크롤 (Infinite Scroll) - React 리액트 버전 본문
개요
React에서 무한스크롤 구현할 일이 있어
여기저기 검색해봤는데
나오는 블로그마다 죄다 어렵게만 설명하고 있어서
답답해가지고 내가 직접 간단하게 구현해본 방법을 공유해본다.
fetcher, React query, Throttle 이딴거 죄다 필요없다.
JavaScript에서 기본 제공하는 Intersection Observer API를 통해 구현하는 법을 알아보자
React가 아닌 일반 JS에서 구현하는 방법을 아래 참고
구현 방법
// ...기타 import 생략
import { useEffect, useRef, useState } from 'react';
const Test = () => {
const target = useRef(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
observer.observe(target.current);
}, []);
function 실행할함수() {
setLoading(true);
console.log('실행');
setLoading(false);
}
// const observer = new IntersectionObserver(callback, options); // option을 줄 수도 있지만 필요성을 못느껴 생략
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return; // entry가 interscting 중이 아니라면 함수를 실행하지 않음
if (loading) return; // 현재 page가 불러오는 중임을 나타내는 flag를 통해 불러오는 중이면 함수를 실행하지 않음
// 실행할 함수
실행할함수();
});
});
return (
<>
// ...생략
<div style={{ height: "20px", backgroundColor: "red" }} ref={target}></div>
</>
)
}
export default Test;
작동하는걸 알아보기 쉽게 옵저버에 감지될 div에 style을 먹여줬다.
매커니즘은 간단하니 자세한 건 직접 주석을 살펴보면 이해할 것이다.
특이사항으로는 실행할 함수에 대해 앞뒤로 setLoading을 먹여
loading이 true라면 observer에 등록된 함수가 중복실행이 되지 않도록 하고 있는데
이 부분은 추후 lodash에서 제공하는 throttle을 활용하면 빼도 되지 않을까 싶다.
결과 확인
데이터를 추가로 붙이는 것 보단 스크롤을 내렸을 때 함수가 실행된다는 걸 보여주고 싶어서
위와 같이 console.log('실행')을 실행하게끔 결과 화면을 만들어봤다.
보다시피 빨간 박스가 보일 때 마다 실행이 출력되고 있으며
박스의 높이가 상당함에도 내리는 순간 여러번 실행되지 않고 한번만 실행되는 것을 확인할 수 있을 것이다.
시행 착오
막상 page를 먹이면서 적용해보려니 문제가 발생했다.
page number를 하나씩 올리며 계속 특정 함수를 불러와야 하는데 (page에 따른 새 게시글 목록들)
setPage를 통해 갱신이 되지 않고 있었다.
코드 1
import { useEffect, useRef, useState } from 'react';
const SampleList = () => {
const target = useRef(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
useEffect(() => {
observer.observe(target.current);
}, []);
useEffect(() => {
실행할함수();
}, [page]);
function 실행할함수() {
setLoading(true);
console.log('page: ', page);
setLoading(false);
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
if (loading) return;
setPage(page + 1);
});
});
return (
<>
<div style={{ height: "1000px", backgroundColor: "green" }} ></div>
<div id="scrollEnd" style={{ height: "20px", backgroundColor: "red" }} ref={target}></div>
</>
)
};
export default SampleList;
코드 2
import { useEffect, useRef, useState } from 'react';
const SampleList = () => {
const target = useRef(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
useEffect(() => {
infiniteScroll();
}, []);
useEffect(() => {
실행할함수();
}, [page]);
function 실행할함수() {
setLoading(true);
console.log('page: ', page);
setLoading(false);
}
const infiniteScroll = () => {
const scrollEnd = document.querySelector('#scrollEnd');
const io = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
if (loading) return;
observer.observe(scrollEnd);
setPage(page + 1);
});
});
io.observe(scrollEnd);
};
return (
<>
<div style={{ height: "1000px", backgroundColor: "green" }} ></div>
<div id="scrollEnd" style={{ height: "20px", backgroundColor: "red" }} ref={target}></div>
</>
)
};
export default SampleList;
코드가 살짝 다르긴 한데 둘 다 위 이미지처럼 처음 1 page에서 2 page로 올라간 이후로
동작하지 않고 있다.
정상작동이라면 끝도 없이 올라갔어야 되는데..
https://velog.io/@woohm402/why-my-state-doesnt-change
https://velog.io/@cada/React의-setState가-잘못된-값을-주는-이유
위 두 블로그 글에 따르면 안바뀌는 이유가 state를 쓰기 때문에인 것으로 나오는듯 하다.
글을 보고 redux로 모든 변수 값들을 넘겨 테스트해봤지만 결과는 마찬가지였다.
애초에 react에 대해 내가 잘못 알고 있었던 부분이 있는 것 같아
위 글을 정독하며 문제를 해결해보았다.
해결 방법
일단 1차적으로 React Hook에 대해 잘 모르고 있어서 해결하는데 오래 걸렸다.
https://devkkiri.com/post/927ae150-7627-4e25-b29f-2d0293b82332
위 블로그 글을 통해 아주 간단하게 해결할 수 있었는데
겪은 문제에 대해 아래와 같이 파악했다.
- page를 state로 둔게 문제. state를 변경하면 react는 렌더를 새로 하기 때문에 계속 state가 1인 상태가 됨
- 때문에 page를 useRef를 통해 렌더에 상관없게 유지되게 함
틀린게 있으면 지적 바란다.
아무튼 이렇게 이론적으로 파악을 했으니 실제 코드를 수정해보았다.
코드
import { useEffect, useRef, useState } from 'react';
const SampleList = () => {
const target = useRef(null);
const [loading, setLoading] = useState(false);
// const [page, setPage] = useState(1); // page는 state가 아닌 아래 useRef를 통해 사용
const page = useRef(1);
useEffect(() => {
observer.observe(target.current); // observer를 등록하는건 동일
}, []);
function 실행할함수() {
setLoading(true);
console.log('page: ', page.current);
setLoading(false);
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
if (loading) return;
// setPage(page + 1);
실행할함수();
page.current += 1; // 이렇게 해줘야 page 숫자가 올라간다.
});
});
return (
<>
<div style={{ height: "1000px", backgroundColor: "green" }} ></div>
<div id="scrollEnd" style={{ height: "20px", backgroundColor: "red" }} ref={target}></div>
</>
)
};
export default SampleList;
결과
이렇게 사진과 같이 원하던 결과를 얻을 수 있었다.
여기에 console.log() 대신 목록을 불러오는 함수를 실행하게 하여
동일하게 page.current를 넣어주면 될 것이다. (이후 실제로 해봤고 잘 됨)
정리
여전히 throttle을 안써도 무방해보이지만
나중에 좀 더 react를 잘 다룰 수 있게 되면
https://tech.kakaoenterprise.com/149
여기서 알려주는 방법대로
여러 Custom Hook을 사용하는 방법도 해보고 싶은 생각이다. 언제가 될진 모르겠지만..
당장은 위에서 해결한 방법대로 구현하고 백엔드에 집중하자.
'React' 카테고리의 다른 글
Redux 꼭 사용해야 할까? - 작성중 (0) | 2023.04.03 |
---|---|
onKeyUp, onKeyDown 한글 입력 시 두번 실행되는 오류 (2) | 2023.03.22 |
Loading Spinner 만들기 (0) | 2023.03.03 |
[object Object] 문제 해결 (0) | 2022.12.03 |
CRA (create-react-app) dotenv (0) | 2022.07.21 |