POKE API에서는 기본적으로 검색 기능을 제공하지 않습니다.
따라서 전체 포켓몬 리스트를 가져와 자체적으로 필터링하여 검색 기능을 구현하기로 했습니다.
검색 기능을 구현하면서, 검색 결과에 대한 페이지네이션, 필터 기능 등을 추가 구현하기 위해 포켓몬 데이터를 여러 컴포넌트에서 재사용해야 하는 상황이 발생했습니다.
상위 루트에 데이터를 정의하고 props로 내려주는 방식보다는, 전역 상태 관리를 통해 구현하는 것이 가독성 면에서 더 좋고 관리하기도 편리할 것이라고 판단하여 Zustand를 사용했습니다.
또한, 기존에 모든 타입 지정과 서버 API 요청 등을 하나의 파일에 작성하던 방식을 개선하여, 다음과 같이 파일을 분리했습니다.
- types.ts: 타입 정의.
- api.ts: 서버 API 요청.
- usePokemonStore.ts: Zustand로 상태 관리.
1. 타입 정의 (types.ts)
포켓몬과 관련된 데이터를 다루기 위해 필요한 타입을 정의했습니다.
// lib/types.ts
export type Pokemon = {
name: string;
url: string;
};
export type PokemonDetail = {
id: number;
name: string;
abilities: AbilitiesType[];
types: PokemonType[];
stats: StatsType[];
height: number;
weight: number;
base_experience: number;
cries: {
legacy: string;
latest: string;
};
sprites: {
front_default: string;
other?: {
home?: {
front_default?: string;
front_shiny?: string;
};
showdown?: {
front_default?: string;
};
};
};
};
type PokemonType = {
type: {
name: string;
url: string;
};
};
type AbilitiesType = {
ability: {
name: string;
url: string;
};
};
type StatsType = {
base_stat: number;
stat: {
name: string;
url: string;
};
};
2. API 요청 (api.ts)
POKE API와 통신하는 모든 함수를 정리했습니다. 이 파일은 API 호출의 책임만을 가지며, 다른 파일과의 의존성을 최소화했습니다.
// lib/api.ts
import { Pokemon, PokemonDetail } from './types';
const pokemonBaseUrl = 'https://pokeapi.co/api/v2';
export async function fetchPokemon(page = 1): Promise<{ count: number; results: Pokemon[] }> {
const offset = (page - 1) * 20;
const response = await fetch(`${pokemonBaseUrl}/pokemon?limit=20&offset=${offset}`);
if (!response.ok) {
throw new Error('포켓몬 목록을 가져오는 데 실패했습니다.');
}
const data = await response.json();
return { count: data.count, results: data.results };
}
export async function fetchAllPokemon(): Promise<Pokemon[]> {
const response = await fetch(`${pokemonBaseUrl}/pokemon?limit=1118`);
if (!response.ok) {
throw new Error('전체 포켓몬 목록을 가져오는 데 실패했습니다.');
}
const data = await response.json();
return data.results;
}
export async function fetchPokemonDetails(pokemonList: { url: string }[]): Promise<PokemonDetail[]> {
const details = await Promise.all(
pokemonList.map(async (pokemon) => {
const response = await fetch(pokemon.url);
if (!response.ok) {
throw new Error(`포켓몬 상세 정보를 가져오는 데 실패했습니다. URL: ${pokemon.url}`);
}
return await response.json();
})
);
return details;
}
export async function fetchPokemonByName(name: string): Promise<PokemonDetail> {
const response = await fetch(`${pokemonBaseUrl}/pokemon/${name}`);
if (!response.ok) {
throw new Error(`포켓몬(${name}) 정보를 가져오는 데 실패했습니다.`);
}
return await response.json();
}
fetchPokemon: 페이지 번호를 기반으로 포켓몬 리스트를 가져옵니다.
fetchAllPokemon: 전체 포켓몬 리스트를 가져옵니다.
fetchPokemonDetails: 포켓몬 상세 정보를 가져옵니다.
fetchPokemonByName: 특정 이름의 포켓몬 정보를 가져옵니다.
3. Zustand로 상태 관리 (usePokemonStore.ts)
Zustand를 활용하여 포켓몬 데이터를 효율적으로 관리했습니다. 각종 데이터 요청 및 검색 로직을 하나의 스토어로 통합했습니다.
// store/usePokemonStore.ts
import { create } from 'zustand';
import { fetchAllPokemon, fetchPokemon, fetchPokemonDetails } from '@/lib/api';
import { Pokemon, PokemonDetail } from '@/lib/types';
type PokemonStore = {
count: number;
loading: boolean;
currentPage: number;
pokemonList: Pokemon[];
allPokemon: Pokemon[];
searchQuery: string;
pokemonListDetails: Record<number | string, PokemonDetail[]>;
fetchPokemon: (page: number) => Promise<void>;
fetchAllPokemon: () => Promise<void>;
fetchPokemonDetails: (page: number) => Promise<void>;
searchPokemon: (query: string) => void;
updateSearchQuery: (query: string) => void;
};
let debounceTimer: NodeJS.Timeout | null = null;
export const usePokemonStore = create<PokemonStore>((set, get) => ({
count: 0,
loading: false,
currentPage: 1,
pokemonList: [],
allPokemon: [],
pokemonListDetails: {},
searchQuery: '',
fetchPokemon: async (page = 1) => {
set({ loading: true });
try {
const data = await fetchPokemon(page);
set({
count: data.count,
pokemonList: data.results,
currentPage: page,
loading: false,
});
} catch (error) {
console.error('Failed to fetch Pokémon list:', error);
set({ loading: false });
}
},
fetchAllPokemon: async () => {
set({ loading: true });
try {
const results = await fetchAllPokemon();
set({ allPokemon: results, loading: false });
} catch (error) {
console.error('Failed to fetch all Pokémon:', error);
set({ loading: false });
}
},
fetchPokemonDetails: async (page = 1) => {
const { pokemonList, pokemonListDetails } = get();
if (pokemonListDetails[page]) return;
set({ loading: true });
try {
const details = await fetchPokemonDetails(
pokemonList.map((pokemon) => ({ url: pokemon.url }))
);
set((state) => ({
pokemonListDetails: {
...state.pokemonListDetails,
[page]: details,
},
loading: false,
}));
} catch (error) {
console.error('Failed to fetch Pokémon details:', error);
set({ loading: false });
}
},
searchPokemon: async (query) => {
const { allPokemon } = get();
if (!query) {
set((state) => ({
pokemonListDetails: {
...state.pokemonListDetails,
search: [],
},
searchQuery: '',
}));
return;
}
set({ loading: true });
try {
const filteredPokemon = allPokemon.filter((pokemon) =>
pokemon.name.toLowerCase().includes(query.toLowerCase())
);
const filteredDetails = await fetchPokemonDetails(
filteredPokemon.map((pokemon) => ({ url: pokemon.url }))
);
set((state) => ({
pokemonListDetails: {
...state.pokemonListDetails,
search: filteredDetails,
},
loading: false,
}));
} catch (error) {
console.error('Error occurred during Pokémon search:', error);
set({ loading: false });
}
},
updateSearchQuery: (query) => {
set({ searchQuery: query });
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const { searchPokemon } = get();
searchPokemon(query);
}, 500);
},
}));
검색 로직
- 전체 포켓몬 데이터를 한 번에 가져와 검색어를 기준으로 필터링하는 방식으로 구현했습니다.
- 검색 시 디바운스를 적용하여 사용자가 입력을 멈춘 후 일정 시간(500ms) 이후에만 API 요청을 보내도록 최적화했습니다.
캐싱 처리
- fetchPokemonDetails 함수에서 각 페이지별로 데이터를 캐싱하여, 사용자가 다른 페이지로 이동했다가 다시 돌아오는 경우 API를 다시 호출하지 않고 캐싱된 데이터를 재사용하도록 했습니다.
'Next.js > Pokemon' 카테고리의 다른 글
MongoDB와 Prisma를 활용한 북마크/좋아요 기능 구현 (0) | 2025.01.01 |
---|---|
PokeAPI와 페이지네이션 기능 구현하기 (0) | 2024.12.30 |
Next.js 15.1.2 -> 14.2.21로 다운그레이드 (0) | 2024.12.27 |
Next.js와 Auth0를 이용한 간단한 로그인 기능 구현하기 (0) | 2024.12.23 |