
메인 페이지에서 무한 스크롤을 구현했다. 어제 잠깐 시도해봤다가 포기했는데, 오늘은 포기하지 않고 끝까지 물고 늘어져서 겨우내 해낼 수 있었다. 그러나 마구잡이로 수정을 시도하다보니, 나조차도 내 코드에 대해서 아직 제대로 이해를 하지 못한 것 같아서(특히 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" });
}
};
뿌듯하면서도 아쉬움이 많이 남는 날이었다. 그저 기능을 구현하는 것 자체도 중요하긴 하지만, 더 중요한 것은 내가 이 코드를 이해하면서 작성했는가라고 생각하기 때문에, 사실 내 코드라는 생각이 들지는 않는다. 복습하고 이해하여 반드시 내 것으로 만들어야겠다.