๐ฉ๐ป๐ป Today I Learned ๐
โญ๋ค์ด๊ฐ๋ฉฐ
์ค์ ํ๋ก์ ํธ์ ๋ค์ด๊ฐ๋ ๊ธฐ๋ฅ๊ณผ ๋์์ธ์ ์ดํด๋ณด๋ค๊ฐ ๋น์ฐํ๊ฒ ๋ฌดํ ์คํฌ๋กค์ ์จ์ผํ๋ค๋ ์๊ฒฌ์ด ๋์๋ค. ๊ฐ์๋ฅผ ๋ณด๋ฉฐ ๊ตฌํํด๋ณธ ์ ๋ ์๊ณ ์ด์ ๋ฏธ๋ ํ๋ก์ ํธ๋ฅผ ํ๋ฉด์ ๊ตฌํ์ ์ง์ ํด๋ณด์ง๋ ์์์ง๋ง ์ฝ๋๋ฅผ ํจ๊ป ๋ดค๋ ์ ์ด ์์๋ค. ํ์ง๋ง ๋ณธ ๊ฒ๊ณผ ์ง์ ๊ตฌํํ๋ ๊ฒ์ ์ฒ์ง ์ฐจ์ด๋ผ๋ ์ .
๋ฉ์ธ ํ์ด์ง ๊ฒ์๊ธ์ ์กฐํํ๋ ๋ถ๋ถ์์ ํ์ํ ๊ธฐ๋ฅ์ธ๋ฐ ๋ด๊ฐ ๋งก๊ฒ ๋ ํ์ด์ง๋ ์๋์ง๋ง ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํด๋ณด๊ณ ์ถ์ด์ ๋ฐ๋ก ๊ตฌํํ๋ค.
โญ react-infinite-scroll-component
๋ฆฌ์กํธ์์ ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํ ์ ์๋ ๋ฐฉ๋ฒ์ด ๋ช ๊ฐ์ง๊ฐ ์๋๋ฐ,
1) ๋ธ๋ผ์ฐ์ ์ onScroll์ ์ด์ฉํด์ scrollHeight ๊ฐ์ ์ฌ์ฉํ๋ ๊ฑฐ๋ค.
2) Intersection Observer API ์ ์ด์ฉํ๋ ๋ฐฉ๋ฒ์ด ๋์ค์ ์ธ๊ฒ ๊ฐ๋ค. ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ์ฐ๋ฉด ์งง์ ์๊ฐ์ ๋ง์ ์์ ํธ์ถ์ ํ ์ ์๊ณ , ๋๊ธฐ์ ์ผ๋ก ์คํ๋๊ธฐ ๋๋ฌธ์ ์ด๋ฒคํธ๊ฐ ๋์์์ด ํธ์ถ๋๋ค๋ ์ ์ด ์๋ค. ์ด๋ถ๋ถ์ ํด๊ฒฐํ๊ธฐ ์ํด์ ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ํธ๋ค๋ง ํ ์ ์๋ intersection observer API๋ฅผ ์ด๋ค๊ณ ํ๋ค. ํ๊ฒ ๊ฐ์ ๊ณ์ ์ฃผ์ํ๋ฉด์ ๊ทธ ๊ฐ์ด ๋ทฐํฌํธ์ ํ๊ฒ์์๊ฐ ๋ค์ด์์ ๋ ์คํํ ํจ์๋ฅผ ๋ฑ๋กํด์ ์ฌ์ฉํ๋ค.
์ด๋ฒ์ ๊ตฌํ ํ ๋ ์ด ๋ฐฉ๋ฒ์ react-infinite-scroll-component ๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด๊ฑฐ์๋ค.
react-infinite-scroll-component github
์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง๋ ๊ณต์ ๊นํ์ ๋ค์ด๊ฐ๋ฉด ์น์ ํ๊ฒ ๋์์๋ค. ๋ค์ํ ์์ฑ๋ค์ด ์๊ธฐ ๋๋ฌธ์ ํ์ํ ๊ฒ ๋ค์ ๋ฃ์ด์ ๊ตฌํํ๋ฉด ๋๋ค.
โญ ๋ฌธ์ ์ ๋ฐ๊ฒฌ – Redux๋ก ํด๊ฒฐ
์ด๋ฏธ์ง๋ฅผ 5๊ฐ์ฉ ๋ฐ์์ค๋ ๊ฒ์ผ๋ก ํด์ ์ ๊ตฌํ์ด ๋์๋ค. ๊ทธ ๋์ ๊ฐ์๋ฅผ ๋ณด๋ฉฐ ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํ ๋ ์ดํด๊ฐ ์ ์๋๊ณ ์ด๋ ต๊ฒ๋ง ๋๊ปด์ก๋๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ ๊ตฌํํ๋ ์ ๋ง ํ ์ ๋ค์ด๊ณ ์ฝ๊ฒ ๊ตฌํํ ๋๋์ด์์ง๋ง ๋ฌธ์ ์ ์ด ๋ฐ๊ฒฌ๋์๋ค.
๋ด๊ฐ ๋ง๋๋ ํ๋ก์ ํธ๋ ํ์ฐธ ๊ฒ์๋ฌผ์ ์กฐํํ๋ค๊ฐ ๋ด ํ์ด์ง์ ๊ฐ๋ค๊ฐ ๋ค์ ๋์์ค๋ฉด ๋ด๊ฐ ๋ณด๋ ํ์ด์ง๊ฐ ๋์ด์ผํ๋๋ฐ, ํ์ฌ ๊ตฌํ๋ ์ฝ๋๋ ๋ค๋ก๊ฐ๊ธฐ๋ฅผ ๋๋ฌ์ ๋์๊ฐ๋ฉด ๋ค์ ์ฒซ ํ์ด์ง๋ถํฐ ๋ก๋ฉ์ด ๋๋ ๊ฒ์ด ์๋๊ฐ?
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ redux๋ฅผ ์ฌ์ฉํ๋ค. ์ด ๋ถ๋ถ์ด ์ ๋ง ๊น๋ค๋กญ๊ฒ ๋๊ปด์ก๋ค.
main.js
import React, { useState, useEffect } from "react";
import { history } from "./redux/configStore";
import { useDispatch, useSelector } from "react-redux";
import { actionCreators as postActions } from "./redux/modules/post";
import InfiniteScroll from "react-infinite-scroll-component";
import axios from "axios";
const Main = () => {
const dispatch = useDispatch();
const post_data = useSelector((state) => state.post);
const [hasMore, sethasMore] = useState(true);
const [page, setpage] = useState(post_data.page);
useEffect(() => {
//์ฒซ ๋ก๋ฉ์ ๋ถ๋ฌ์ค๋ ๋ฐ์ดํฐ
dispatch(postActions.getPostAction(page));
}, [page]);
//scroll event
//์คํฌ๋กค์ ๋ค์ํ์ด์ง๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฒ
const getData = () => {
let pages = page + 1;
axios
.get(`https://picsum.photos/v2/list?page=${pages}&limit=5`)
.then((res) => {
//๋ฐ์ดํฐ๊ฐ ์ฌ์ด์ฆ๋ณด๋ค ์์ ๊ฒฝ์ฐ
if (data.length === 0 || data.length < 5) {
sethasMore(false);
} else {
//๋ฐ์ดํฐ๊ฐ ์ฌ์ด์ฆ๋งํผ ๋์ด์์ ๋
setpage(pages + 1);
}
});
};
return (
<>
{/* ๋ค๋ฅธ ํ์ด์ง๋ก ๋์ด๊ฐ๋ค๊ฐ ๋ค์ ๋์์์ ๋ ํ์ด์ง๊ฐ ํ์ด์ง๊ฐ ๊ทธ๋๋ก์ธ์ง ํ์ธํ๊ธฐ ์ํ ๋ฒํผ */}
<button onClick={() => history.push("/page")}>
Move to another page
</button>
<InfiniteScroll
dataLength={post_data.posts.length} //This is important field to render the next data
next={getData}
hasMore={hasMore}
>
{post_data.posts.map((item, idx) => {
return (
<>
<div className="imgbox" key={item.id}>
<img src={item.download_url} alt="photo" />
</div>
</>
);
})}
</InfiniteScroll>
</>
);
};
export default Main;
์ด๋ฏธ์ง ์คํฌ๋กค์ด ์ผ์ด๋๋ ํ์ด์ง์ด๋ค. ๋ค๋ฅธ ํ์ด์ง๋ก ์ด๋ํ๋ค๊ฐ ๋์์์ ๋๋ฅผ ํ์ธํ๊ธฐ ์ํด์ ๋ฒํผ์ ์์ ํ๋ ๋ฃ์ด์คฌ๋ค.
์๋ useEffect ๋ถ๋ถ์ ์ฒซ ๋ก๋ฉ์ ๋ค์ด์ค๋ ํ์ด์ง๋ฅผ 1๋ก ํ์ํด์ ๋ฃ์ด๋๊ณ , getData()์ infinite Scroll ์์ฑ ์ค์ next์ ๋ฃ์ด์ ๋ค์ ํ์ด์ง๋ฅผ ๋ฐ์ต๋๋ค. next๋ผ๋ ์์ฑ์ ๋ฐ๋ฅ์ ๋ฟ์ ํ์ ๋ฐ์ดํฐ๋ฅผ fetchํด์ ๊ฐ์ ธ์ค๋๋ฐ ์ด๋ ์ด์ ๋ฐ์ดํฐ๋ ํฌํจ๋์ ๋์ด์จ๋ค.
useEffect์ ๋ค์ด๊ฐ ๋ถ๋ถ์ด ์ฒซ ๋ก๋ฉ์์ 1๋ก ๋์ด์์ผ๋ ๊ณ์ํด์ ์ฒซ ํ์ด์ง๋ถํฐ ๋ก๋ฉ์ด ๋๋๊ฑฐ๋ค. ๊ทธ๋์ ๋ฆฌ๋์ค๋ฅผ ์ฌ์ฉํด์ ๋๋ฉ์ ๋ฐ์์จ ๋ฆฌ์คํธ๋ฅผ ์ ์ฅํด๋๊ณ useSelector๋ฅผ ์ฌ์ฉํด์ ๋ฆฌ์คํธ๋ฅผ ๊ฐ์ ธ์์ ์ฌ์ฉํ๋ค.
post.js
import { createAction, handleActions } from "redux-actions";
import { produce } from "immer";
import axios from "axios";
const GET_POST = "GET_POST";
const initialState = {
posts: [],
page: 0,
has_next: false,
};
const getPosts = createAction(GET_POST, (post_data) => ({ post_data }));
// middlewares
const getPostAction = (page) => {
return async (dispatch, getState, { history }) => {
axios
.get(`https://picsum.photos/v2/list?page=${page}&limit=5`)
.then((res) => {
let is_next = null;
if (res.data.length < 5) {
is_next = false;
} else {
is_next = true;
}
let post_data = {
posts: res.data,
page: page + 1,
next: is_next,
};
dispatch(getPosts(post_data));
});
};
};
// reducer
export default handleActions(
{
[GET_POST]: (state, action) =>
produce(state, (draft) => {
draft.posts.push(...action.payload.post_data.posts);
draft.has_next = action.payload.post_data.next;
draft.page = action.payload.post_data.page;
}),
},
initialState
);
const actionCreators = {
getPostAction,
};
export { actionCreators };
dispatch๋ก ๋ฆฌ๋์ค ํจ์๋ฅผ ์ธ ๋ page ๊ฐ์ ๋๊ฒจ์ค์ผํ๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฐ์์ค๋ ๊ฐ์ ํ์ด์ง์ ๋ฆฌ์คํธ, ๊ทธ ๋ค์ ํ์ด์ง๋ก ๋์ด๊ฐ๋ ๋๋์ง์ ๋ํ ์ ๋ณด๊ฐ ๋ด๊ฒจ์๋ค. ๊ทธ๋ฌ์ง ์์ผ๋ฉด ๋ค์ ํ์ด์ง๊ฐ ๋ก๋ฉ๋์ง ์๋๋ค. getData๋ก๋ ํ์ด์ง ๊ฐ๋ง ๋ณ๊ฒฝํด์ฃผ๊ณ ๋ฐ๋ก ๋ฆฌ์คํธ๋ฅผ ๋ฐ์์ค์ง๋ ์์๋ค. ๋ฆฌ๋์ค์์ ๋ฐ์ ๋ด์ฉ์ผ๋ก๋ ์คํฌ๋กค์ด ๋ฐ์ํ๋ค. ๊ทธ๋ฆฌ๊ณ main.js์์๋ page๊ฐ ๋ณํ ๋๋ง๋ค ๋๋ฉ์ด ๋๊ฒ ์์กด๊ฐ์ ๋ฃ์ด์ค๋ค.
์ฒ์์๋ ์ดํด๋ฅผ ๋ชปํด์ redux์ ์ ์ฅํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ ์ฐ๋๋ฐ, ๋ฌดํ ์คํฌ๋กค ๋์์ ์ํด์ ๊ณ์ํด์ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๊ณ ๊ทธ๊ฑธ ๋ useState๋ก ๊ด๋ฆฌ๋ฅผ ํด์ฃผ๊ณ ์์๋ค. ์ฌ์ค ๊ทธ๋ด ํ์๊ฐ ์๋๊ฑฐ ์๋๋ฐ ๋ง์ด๋ค. ์ค์ค๋ก ์ฝ๋๋ฅผ ๋ณด๋ฉด์ ์ด๊ฒ ์๋์ด ์ ๋๋๊ฑด์ง๋ฅผ ์ดํด๋ฅผ ๋ชป ํ๋ค. ์๋๋ฉด ๋ฆฌ๋์ค๋ ๋ฆฌ๋์ค๋๋ก ์์ง์ด๊ณ , ์คํฌ๋กค ๋ฐ์ดํฐ๋ ๋ ์คํฌ๋กค ๋ฐ์ดํฐ๋๋ก ์์ง์ด๊ณ …๋์ด ๋ฐ๋ก๋๊ณ ์๋๋ฐ…๋ญ์ง?๋ญ์ง๋ผ๋ฉฐ ๐ณ
๋๋ฒ์งธ ๋ฌธ์ ์ ! infinite Scroll์ ์์ฑ์ผ๋ก ๋ฃ์ด์ฃผ๋ getData์ page๊ฐ์ด useEffect์ ๋ค์ด๊ฐ๋ page๊ฐ์ด ๋์ผํ๋ฉด ๊ทธ ๋์ผํ ๊ฐ์ ํ์ด์ง๊ฐ ๋๋ฒ ๋ถ๋ฌ์์ง๋ค. ์ฒซ๋ฒ์งธ ํ์ด์ง๊ฐ ๋ ๋ฒ ๋์ค๊ณ ๊ทธ๋ค์ ํ์ด์ง๊ฐ ๋์ค๋ ๊ฒ์ด๋ค. ์ด ๋ถ๋ถ๋ ์ดํด๋ฅผ ๋ชป ํ๋ค. ์๋๋ฉด ๋ฆฌ๋์ค๋ก ๋ฐ์ดํฐ ๋ฐ๋๊ฑฐ๋ ์คํฌ๋กค๋ก ๋ฐ๋ ๊ฐ์ ๋ฐ๋ก ์์ง์ด๋๊น, ์์ง? ๋ผ๋ ์๊ฐ์ด ๋ค์๊ธฐ ๋๋ฌธ. ๋ฐ๋ก ๋ถ๋ฌ์ค๋ ๋ฐ์ดํฐ๋ฅผ ์ํ๊ด๋ฆฌ ์ํด๋์ฃผ๋๋ฐ ๋ฌด์จ ์๊ด์ด์ง ํ๋๋ฐ,…
getData์ ๋ค์ด๊ฐ๋ ํ์ฌ page๊ฐ์ + 1์ ํด์ ๊ทธ ๋ค์ ํ์ด์ง๋ถํฐ ๋์ค๊ฒํ๋ค. ์ด๊ฒ ์๋๋ฉด next์์ฑ์ ๋ค์ด๊ฐ๋ ํจ์๋ ๊ทธ ์ด์ ์ ๋ฐ์ดํฐ๋ ์ํํด์ ์ค๊ธฐ ๋๋ฌธ์ ์๋ง๋ ํ์ฌ page๊ฐ์ด ์ด์ ๊บผ๋ก ๋์ด๊ฐ๋ ๋ฏ ํ๋ค. ๊ทธ๋์ ๊ฐ์ ํ์ด์ง๊ฐ ๋ฐ๋ณตํด์ ๋์ค๋ ๊ฒ!
โญ ์์ฑ
โญ ์ฐธ๊ณ ํ ๋์์ & ๋ธ๋ก๊ทธ
- Blog
https://velog.io/@euneun/%EC%A1%B8%EC%97%85%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-k162xd7y
- Youtube
https://youtu.be/bBUOMy6Tugw