
์์ฆ ์์ ๋ฏธ๋์ด๋ ๋ด์ค ํผ๋๋ฅผ ๋ณด๋ฉด ์ฝํ ์ธ ๊ฐ ๋์์ด ์ด์ด์ง๋ ๊ฒฝํ์ ์ฝ๊ฒ ํ ์ ์์ต๋๋ค. ์ฌ์ฉ์๊ฐ ํ์ด์ง๋ฅผ ์๋๋ก ์คํฌ๋กคํ๋ฉด ์๋ก์ด ์ฝํ ์ธ ๊ฐ ์์ฐ์ค๋ฝ๊ฒ ๋ก๋๋๋ ๊ฒ, ์ด๊ฒ์ด ๋ฐ๋ก ๋ฌดํ ์คํฌ๋กค(Infinite Scroll) ์ ๋๋ค.
์ด๋ฒ ํฌ์คํธ์์๋ ์ ๋ฌดํ ์คํฌ๋กค์ด ํ์ํ๋ฉฐ, React ํ๊ฒฝ์์ react-intersection-observer ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด ์ผ๋ง๋ ์ฝ๊ณ ํจ์จ์ ์ผ๋ก ๊ตฌํํ ์ ์๋์ง ์์๋ณด๊ฒ ์ต๋๋ค.
๋ง์ฝ ์๋ฐฑ, ์์ฒ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ํ ํ์ด์ง์ ๋ชจ๋ ํ์ํ๋ ค๊ณ ํ๋ฉด ์ฌ๋ฌ ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
๋๋ฆฐ ์ด๊ธฐ ๋ก๋ฉ ์๋: ์ฌ์ฉ์๋ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๋ค์ด๋ก๋ํ ๋๊น์ง ๊ธด ์๊ฐ ๋์ ๋น ํ๋ฉด์ด๋ ๋ก๋ฉ ํ๋ฉด์ ๋ด์ผ ํฉ๋๋ค.
๋ธ๋ผ์ฐ์ ๊ณผ๋ถํ: ํ ๋ฒ์ ๋๋ฌด ๋ง์ ๋ฐ์ดํฐ๋ฅผ ํ๋ฉด์ ๊ทธ๋ฆฌ๋ ค๊ณ ํ๋ฉด(๋ ๋๋ง) ๋ธ๋ผ์ฐ์ ์ ๋ ๋๋ง ์์ง์ ํฐ ๋ถ๋ด์ ์ค๋๋ค. ์ด๋ ์ฌ๊ฐํ UI ๋๊น(Jank) ํ์์ด๋ ๋ธ๋ผ์ฐ์ ๋ฉ์ถค์ ์ ๋ฐํ ์ ์์ต๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ํ์ ์ธ ๋ฐฉ๋ฒ์ด ๋ฐ๋ก ํ์ด์ง๋ค์ด์ (Pagination)๊ณผ ๋ฌดํ ์คํฌ๋กค(Infinite Scroll)์ ๋๋ค.
๋ ๋ฐฉ์์ UX ๊ด์ ์์ ๋ช ํํ ์ฐจ์ด๊ฐ ์์ผ๋ฉฐ, ์๋น์ค์ ๋ชฉ์ ์ ๋ง๊ฒ ์ ํํด์ผ ํฉ๋๋ค.
ํ์ด์ง๋ค์ด์ : ์ฌ์ฉ์๊ฐ ํน์ ํ ๋ชฉ์ ์ ๊ฐ์ง๊ณ ์ ๋ณด๋ฅผ ์ฐพ์ ๋ ์ ํฉํฉ๋๋ค. (์: ๊ตฌ๊ธ ๊ฒ์ ๊ฒฐ๊ณผ, ์ด์ปค๋จธ์ค ์ํ ๋ชฉ๋ก)
๋ฌดํ ์คํฌ๋กค: ์ฌ์ฉ์๊ฐ ๋๋ ทํ ๋ชฉ์ ์์ด ์ฝํ ์ธ ๋ฅผ ํ์ํ๊ณ ๋ฐ๊ฒฌํ๋ ๊ฒ์ ์ฆ๊ธธ ๋ ์ ํฉํฉ๋๋ค. (์: ์ธ์คํ๊ทธ๋จ ํผ๋, ํํฐ๋ ์คํธ)
์ค๋์ ์ด ์ค์์ '๋ฌดํ ์คํฌ๋กค' ๊ธฐ๋ฅ ๊ตฌํ์ ๋ํด ์ง์ค์ ์ผ๋ก ๋ค๋ค๋ณด๊ฒ ์ต๋๋ค.
react-intersection-observer๋ ๋ธ๋ผ์ฐ์ ์ Intersection Observer API๋ฅผ React์์ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก ๋์์ฃผ๋ ๊ฐ๋ ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ์ง์ ๊ฐ์งํ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ํจ์จ์ ์ด๊ณ ์ฑ๋ฅ์ด ๋ฐ์ด๋ฉ๋๋ค.
๋จผ์ ํ๋ก์ ํธ์ ํด๋น ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํฉ๋๋ค.
npm install react-intersection-observer
์ด์ ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํ ์ ์ฒด ์ฝ๋๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
import React, { useEffect, useState } from 'react';
import MovieHeader from "./MovieHeader";
import MovieMain from "./MovieMain";
import MovieList from "./MovieList";
import axiosInstant from "../../api/axios";
import { useInView } from "react-intersection-observer";
function Movie() {
const [nowData, setNowData] = useState([]);
const [nowLoading, setNowLoading] = useState(false);
const [nowError, setNowError] = useState(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const { ref, inView } = useInView({
threshold: 0.5,
});
// ๋ฐ์ดํฐ ๋ก๋ฉ ํจ์
const fetchMovies = async () => {
// ์ด๋ฏธ ๋ก๋ฉ ์ค์ด๊ฑฐ๋ ๋ ์ด์ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ์คํํ์ง ์์
if (nowLoading || !hasMore) return;
setNowLoading(true);
try {
const response = await axiosInstant.get(`now_playing?page=${page}`);
const { results, total_pages } = response.data;
setNowData(prev => [...prev, ...results]);
setHasMore(total_pages > page);
setPage(prevPage => prevPage + 1); // ๋ค์ ํ์ด์ง๋ฅผ ์ค๋น
} catch (e) {
setNowError(e);
} finally {
setNowLoading(false);
}
};
// `inView` ๊ฐ์ด true๋ก ๋ณ๊ฒฝ๋ ๋ ๋ค์ ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ต๋๋ค.
useEffect(() => {
if (inView) {
fetchMovies();
}
}, [inView]);
return (
<>
<MovieHeader />
<MovieMain />
<MovieList title="Now Playing" movies={nowData} error={nowError} />
{/* ์ด div๊ฐ ํ๋ฉด์ ๋ณด์ด๋ฉด ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํฉ๋๋ค. */}
<div ref={ref}></div>
{nowLoading && <p>Loading...</p>}
{nowError && <p>Error!</p>}
</>
);
}
export default Movie;
์ ์ฝ๋์ ๋์ ์๋ฆฌ๋ ์๊ฐ๋ณด๋ค ๊ฐ๋จํฉ๋๋ค.
๊ฐ์ ๋์ ์ง์ (ref): useInView ํ ์ด ๋ฐํํ๋ ref๋ฅผ ๋ฆฌ์คํธ์ ๋งจ ๋ง์ง๋ง์ ์๋
ํ๋ฉด ๊ฐ์ง (inView): ์ฌ์ฉ์๊ฐ ํ์ด์ง๋ฅผ ์คํฌ๋กคํด์ ref๊ฐ ๋ฌ๋ฆฐ div๊ฐ ํ๋ฉด์ ๋ํ๋๋ฉด, useInView๋ inView ๊ฐ์ true๋ก ๋ณ๊ฒฝํฉ๋๋ค.
๋ฐ์ดํฐ ์์ฒญ ํธ๋ฆฌ๊ฑฐ (useEffect): useEffect๋ inView ๊ฐ์ด true๋ก ๋ฐ๋ ๊ฒ์ ๊ฐ์งํ๊ณ , fetchMovies ํจ์๋ฅผ ํธ์ถํ์ฌ ๋ค์ ํ์ด์ง์ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์ ์์ฒญํฉ๋๋ค.
์ํ ์ ๋ฐ์ดํธ: ๋ฐ์ดํฐ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๋ถ๋ฌ์ค๋ฉด nowData์ ์๋ก์ด ์ํ ๋ชฉ๋ก์ ์ถ๊ฐํ๊ณ , page ๋ฒํธ๋ฅผ 1 ์ฆ๊ฐ์์ผ ๋ค์ ์์ฒญ์ ์ค๋นํฉ๋๋ค.
๋ฐ๋ณต: ์ ๋ฐ์ดํฐ๊ฐ ๋ ๋๋ง๋๋ฉด ๋ฆฌ์คํธ๊ฐ ๊ธธ์ด์ง๊ณ '๊ฐ์ ์ผ์' div๋ ๋ ์๋๋ก ๋ด๋ ค๊ฐ๋๋ค. ์ฌ์ฉ์๊ฐ ์คํฌ๋กค์ ๊ณ์ ๋ด๋ฆฌ๋ฉด ์ด ๊ณผ์ ์ด ์๋์ผ๋ก ๋ฐ๋ณต๋ฉ๋๋ค.
react-intersection-observer๋ฅผ ํ์ฉํ๋ฉด ๋ถํ์ํ ๋ฆฌ์์ค ๋ญ๋น ์์ด, ์ฌ์ฉ์์๊ฒ๋ ๋ถ๋๋ฌ์ด ์คํฌ๋กค ๊ฒฝํ์ ์ ๊ณตํ๋ ํจ์จ์ ์ธ ๋ฌดํ ์คํฌ๋กค์ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ์ด๊ธฐ ๋ก๋ฉ ์๋๋ฅผ ๊ฐ์ ํ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ํฌ๊ฒ ํฅ์์์ผ ๋ณด์ธ์.