[TIL] 240703 (Next.js 개인과제 useInfiniteQuery 무한 스크롤 구현)

·2024년 7월 3일

TIL

목록 보기
88/268
post-thumbnail

🥞 오늘 한 일

  • 넥스트 개인과제
    • 무한 스크롤 구현 (useInfiniteQuery 사용)
    • 스타일 변경
      • 각 카드 호버 시 색 변경
      • 로딩 메세지 화면 가운데로 위치 수정
  • 스탠다드반 수업
    • Next.js 복습#1 - Next.js 기초

🍽️ 트러블 슈팅

넥스트 개인과제

무한 스크롤 구현

메인 페이지에서 무한 스크롤을 구현했다. 어제 잠깐 시도해봤다가 포기했는데, 오늘은 포기하지 않고 끝까지 물고 늘어져서 겨우내 해낼 수 있었다. 그러나 마구잡이로 수정을 시도하다보니, 나조차도 내 코드에 대해서 아직 제대로 이해를 하지 못한 것 같아서(특히 useEffect 부분), 아직 이 부분에 대해서는 trouble로 남아있는 상태이다.

// PokemonList.tsx
"use client";

import { QueryFunctionContext, useInfiniteQuery } from "@tanstack/react-query";
import axios, { AxiosError } from "axios";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React, { useEffect, useRef } from "react";

const PokemonList = (): React.JSX.Element => {
  const router = useRouter();
  const loadMoreRef = useRef<HTMLDivElement | null>(null);

  const {
    data: pokemonList,
    isPending,
    isError,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery<Pokemon[], AxiosError>({
    queryKey: ["pokemonList"],
    queryFn: async ({
      pageParam = 0,
    }: QueryFunctionContext): Promise<Pokemon[]> => {
      const { data } = await axios.get<Pokemon[]>(
        `http://localhost:3000/api/pokemons`,
        {
          params: { offset: pageParam, limit: 48 },
        }
      );
      return data;
    },
    getNextPageParam: (lastPage, allPages) => {
      if (lastPage.length < 48) {
        return undefined; // 마지막 페이지에 도달하면 더 이상 요청하지 않음
      }
      return allPages.length * 48; // 다음 페이지의 offset 계산
    },
    staleTime: 600000,
    gcTime: 600000,
    initialPageParam: 0,
  });

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage) {
          fetchNextPage();
        }
      },
      { threshold: 1.0 }
    );

    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current);
    }

    return () => {
      if (loadMoreRef.current) {
        observer.unobserve(loadMoreRef.current);
      }
    };
  }, [fetchNextPage, hasNextPage]);

  if (isPending || !pokemonList) {
    return <div className="text-center">포켓몬을 데려오는 중입니다...</div>;
  }

  return (
    <>
      <ul className="grid grid-cols-6 gap-3 p-[30px]">
        {pokemonList.pages.map((page, pageIndex) => (
          <React.Fragment key={pageIndex}>
            {page.map((pokemon: Pokemon) => {
              return (
                <Link key={pokemon.id} href={`/${pokemon.id}`}>
                  <li
                    key={pokemon.id}
                    className="text-center cursor-pointer box-border bg-white text-black p-2 rounded-2xl hover:bg-gray-400 transition"
                  >
                    <Image
                      src={pokemon.sprites.front_default}
                      width={100}
                      height={100}
                      alt={pokemon.korean_name}
                      className="mx-auto"
                    />
                    <p className="flex justify-center items-center gap-1">
                      <span className="bg-black text-white rounded px-1 text-xs">
                        {String(pokemon.id).padStart(4, "0")}
                      </span>{" "}
                      <span className="font-bold">
                        {pokemon.korean_name
                          ? pokemon.korean_name
                          : pokemon.name}
                      </span>
                    </p>
                  </li>
                </Link>
              );
            })}
          </React.Fragment>
        ))}
      </ul>
      <div className="w-full text-center pb-[30px] " ref={loadMoreRef}>
        {isFetchingNextPage
          ? "더 많은 포켓몬을 데려오는 중입니다..."
          : hasNextPage
          ? "포켓몬을 더 데려올 수 있습니다."
          : "모든 포켓몬을 데려왔습니다!"}
      </div>
    </>
  );
};

export default PokemonList;
// route.ts
import { NextResponse } from "next/server";
import axios, { AxiosResponse } from "axios";

const TOTAL_POKEMON = 1025;

export const GET = async (request: Request) => {
  const { searchParams } = new URL(request.url);
  const offset = parseInt(searchParams.get("offset") ?? "0", 10);
  const limit = parseInt(searchParams.get("limit") ?? "48", 10);

  try {
    const allPokemonPromises = Array.from({ length: limit }, (_, index) => {
      const id = offset + index + 1;
      if (id <= TOTAL_POKEMON) {
        return Promise.all([
          axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`),
          axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`),
        ]);
      } else {
        return null;
      }
    }).filter(Boolean) as Promise<[AxiosResponse, AxiosResponse]>[];

    const allPokemonResponses = await Promise.all(allPokemonPromises);

    const allPokemonData = allPokemonResponses.map(
      ([response, speciesResponse], index) => {
        const koreanName = speciesResponse.data.names.find(
          (name: any) => name.language.name === "ko"
        );
        return { ...response.data, korean_name: koreanName?.name || null };
      }
    );
    return NextResponse.json(allPokemonData);
  } catch (error) {
    return NextResponse.json({ error: "Failed to fetch data" });
  }
};

🍴 돌아보기

뿌듯하면서도 아쉬움이 많이 남는 날이었다. 그저 기능을 구현하는 것 자체도 중요하긴 하지만, 더 중요한 것은 내가 이 코드를 이해하면서 작성했는가라고 생각하기 때문에, 사실 내 코드라는 생각이 들지는 않는다. 복습하고 이해하여 반드시 내 것으로 만들어야겠다.

🍳 내일 목표

  • 넥스트 개인과제
    • 해설 기반 리팩토링
    • 추가적으로 원하는 기능 제작
    • 과제 제출
profile
웹 프론트엔드 개발자

0개의 댓글