๐ŸŒต React์—์„œ ๋ฌดํ•œ์Šคํฌ๋กค ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

ํžˆ๋‹ˆยท2025๋…„ 8์›” 10์ผ

React

๋ชฉ๋ก ๋ณด๊ธฐ
1/2
post-thumbnail

React๋กœ ๋ฌดํ•œ ์Šคํฌ๋กค(Infinite Scroll) ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿš€

์š”์ฆ˜ ์†Œ์…œ ๋ฏธ๋””์–ด๋‚˜ ๋‰ด์Šค ํ”ผ๋“œ๋ฅผ ๋ณด๋ฉด ์ฝ˜ํ…์ธ ๊ฐ€ ๋์—†์ด ์ด์–ด์ง€๋Š” ๊ฒฝํ—˜์„ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ์•„๋ž˜๋กœ ์Šคํฌ๋กคํ•˜๋ฉด ์ƒˆ๋กœ์šด ์ฝ˜ํ…์ธ ๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋กœ๋“œ๋˜๋Š” ๊ฒƒ, ์ด๊ฒƒ์ด ๋ฐ”๋กœ ๋ฌดํ•œ ์Šคํฌ๋กค(Infinite Scroll) ์ž…๋‹ˆ๋‹ค.

์ด๋ฒˆ ํฌ์ŠคํŠธ์—์„œ๋Š” ์™œ ๋ฌดํ•œ ์Šคํฌ๋กค์ด ํ•„์š”ํ•˜๋ฉฐ, React ํ™˜๊ฒฝ์—์„œ react-intersection-observer ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ์–ผ๋งˆ๋‚˜ ์‰ฝ๊ณ  ํšจ์œจ์ ์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๐Ÿค” ์™œ ํ•„์š”ํ• ๊นŒ์š”? ํ•œ ๋ฒˆ์— ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ฉด ์•ˆ ๋˜๋‚˜์š”?

๋งŒ์•ฝ ์ˆ˜๋ฐฑ, ์ˆ˜์ฒœ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ํŽ˜์ด์ง€์— ๋ชจ๋‘ ํ‘œ์‹œํ•˜๋ ค๊ณ  ํ•˜๋ฉด ์—ฌ๋Ÿฌ ์„ฑ๋Šฅ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๋А๋ฆฐ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„: ์‚ฌ์šฉ์ž๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ ๋‹ค์šด๋กœ๋“œํ•  ๋•Œ๊นŒ์ง€ ๊ธด ์‹œ๊ฐ„ ๋™์•ˆ ๋นˆ ํ™”๋ฉด์ด๋‚˜ ๋กœ๋”ฉ ํ™”๋ฉด์„ ๋ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ธŒ๋ผ์šฐ์ € ๊ณผ๋ถ€ํ•˜: ํ•œ ๋ฒˆ์— ๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ๊ทธ๋ฆฌ๋ ค๊ณ  ํ•˜๋ฉด(๋ Œ๋”๋ง) ๋ธŒ๋ผ์šฐ์ €์˜ ๋ Œ๋”๋ง ์—”์ง„์— ํฐ ๋ถ€๋‹ด์„ ์ค๋‹ˆ๋‹ค. ์ด๋Š” ์‹ฌ๊ฐํ•œ UI ๋Š๊น€(Jank) ํ˜„์ƒ์ด๋‚˜ ๋ธŒ๋ผ์šฐ์ € ๋ฉˆ์ถค์„ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋Œ€ํ‘œ์ ์ธ ๋ฐฉ๋ฒ•์ด ๋ฐ”๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜(Pagination)๊ณผ ๋ฌดํ•œ ์Šคํฌ๋กค(Infinite Scroll)์ž…๋‹ˆ๋‹ค.

๐Ÿง ํŽ˜์ด์ง€๋„ค์ด์…˜ vs. ๋ฌดํ•œ ์Šคํฌ๋กค

๋‘ ๋ฐฉ์‹์€ UX ๊ด€์ ์—์„œ ๋ช…ํ™•ํ•œ ์ฐจ์ด๊ฐ€ ์žˆ์œผ๋ฉฐ, ์„œ๋น„์Šค์˜ ๋ชฉ์ ์— ๋งž๊ฒŒ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํŽ˜์ด์ง€๋„ค์ด์…˜: ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ •ํ•œ ๋ชฉ์ ์„ ๊ฐ€์ง€๊ณ  ์ •๋ณด๋ฅผ ์ฐพ์„ ๋•Œ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. (์˜ˆ: ๊ตฌ๊ธ€ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ, ์ด์ปค๋จธ์Šค ์ƒํ’ˆ ๋ชฉ๋ก)

๋ฌดํ•œ ์Šคํฌ๋กค: ์‚ฌ์šฉ์ž๊ฐ€ ๋šœ๋ ทํ•œ ๋ชฉ์  ์—†์ด ์ฝ˜ํ…์ธ ๋ฅผ ํƒ์ƒ‰ํ•˜๊ณ  ๋ฐœ๊ฒฌํ•˜๋Š” ๊ฒƒ์„ ์ฆ๊ธธ ๋•Œ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. (์˜ˆ: ์ธ์Šคํƒ€๊ทธ๋žจ ํ”ผ๋“œ, ํ•€ํ„ฐ๋ ˆ์ŠคํŠธ)

์˜ค๋Š˜์€ ์ด ์ค‘์—์„œ '๋ฌดํ•œ ์Šคํฌ๋กค' ๊ธฐ๋Šฅ ๊ตฌํ˜„์— ๋Œ€ํ•ด ์ง‘์ค‘์ ์œผ๋กœ ๋‹ค๋ค„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

โš™๏ธ React์—์„œ ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„ํ•˜๊ธฐ

react-intersection-observer๋Š” ๋ธŒ๋ผ์šฐ์ €์˜ Intersection Observer API๋ฅผ React์—์„œ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ์Šคํฌ๋กค ์ด๋ฒคํŠธ๋ฅผ ์ง์ ‘ ๊ฐ์ง€ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ํšจ์œจ์ ์ด๊ณ  ์„ฑ๋Šฅ์ด ๋›ฐ์–ด๋‚ฉ๋‹ˆ๋‹ค.

1. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

๋จผ์ € ํ”„๋กœ์ ํŠธ์— ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.


npm install react-intersection-observer

2. ํ•ต์‹ฌ ๋กœ์ง ๋ฐ ์‚ฌ์šฉ ์˜ˆ์‹œ

์ด์ œ ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•œ ์ „์ฒด ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


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;

3. ์ฝ”๋“œ ๋™์ž‘ ์›๋ฆฌ

์œ„ ์ฝ”๋“œ์˜ ๋™์ž‘ ์›๋ฆฌ๋Š” ์ƒ๊ฐ๋ณด๋‹ค ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.

๊ฐ์‹œ ๋Œ€์ƒ ์ง€์ • (ref): useInView ํ›…์ด ๋ฐ˜ํ™˜ํ•˜๋Š” ref๋ฅผ ๋ฆฌ์ŠคํŠธ์˜ ๋งจ ๋งˆ์ง€๋ง‰์— ์žˆ๋Š”

์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ด div๊ฐ€ ๋ฐ”๋กœ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ• ์ง€ ๋ง์ง€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” '๊ฐ์‹œ ์„ผ์„œ'๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

ํ™”๋ฉด ๊ฐ์ง€ (inView): ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ์Šคํฌ๋กคํ•ด์„œ ref๊ฐ€ ๋‹ฌ๋ฆฐ div๊ฐ€ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚˜๋ฉด, useInView๋Š” inView ๊ฐ’์„ true๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ์š”์ฒญ ํŠธ๋ฆฌ๊ฑฐ (useEffect): useEffect๋Š” inView ๊ฐ’์ด true๋กœ ๋ฐ”๋€ ๊ฒƒ์„ ๊ฐ์ง€ํ•˜๊ณ , fetchMovies ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋‹ค์Œ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„์— ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.

์ƒํƒœ ์—…๋ฐ์ดํŠธ: ๋ฐ์ดํ„ฐ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ค๋ฉด nowData์— ์ƒˆ๋กœ์šด ์˜ํ™” ๋ชฉ๋ก์„ ์ถ”๊ฐ€ํ•˜๊ณ , page ๋ฒˆํ˜ธ๋ฅผ 1 ์ฆ๊ฐ€์‹œ์ผœ ๋‹ค์Œ ์š”์ฒญ์„ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋ณต: ์ƒˆ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ Œ๋”๋ง๋˜๋ฉด ๋ฆฌ์ŠคํŠธ๊ฐ€ ๊ธธ์–ด์ง€๊ณ  '๊ฐ์‹œ ์„ผ์„œ' div๋Š” ๋” ์•„๋ž˜๋กœ ๋‚ด๋ ค๊ฐ‘๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กค์„ ๊ณ„์† ๋‚ด๋ฆฌ๋ฉด ์ด ๊ณผ์ •์ด ์ž๋™์œผ๋กœ ๋ฐ˜๋ณต๋ฉ๋‹ˆ๋‹ค.

โœ… ๋งˆ๋ฌด๋ฆฌ

react-intersection-observer๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋ถˆํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„ ์—†์ด, ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ๋ถ€๋“œ๋Ÿฌ์šด ์Šคํฌ๋กค ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ํšจ์œจ์ ์ธ ๋ฌดํ•œ ์Šคํฌ๋กค์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„๋ฅผ ๊ฐœ์„ ํ•˜๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œ์ผœ ๋ณด์„ธ์š”.

profile
์•ˆ๋…•ํ•˜์„ธ์š”

0๊ฐœ์˜ ๋Œ“๊ธ€