export interface Country {
name: {
common: string;
};
capital: string[];
flags: {
svg: string;
};
}
export interface CountryInfo {
id: string;
name: string;
capital: string;
isFavorite: boolean;
flagUrl: string;
}
위에 interface Country
는 API에서 받아오는 데이터를 위한 type
이고, interface CountryInfo
는 useQuery
에서 queryFn
이 select
옵션을 거친 후 return되는 데이터를 위한 type
이다.
// api.countries.ts
export const getData = async () => {
const response = await fetch("https://restcountries.com/v3.1/all");
const data = await response.json();
return data;
};
// App.jsx
useEffect(() => {
const getCountryList = async () => {
const data: Country[] = await getData();
const countries: CountryInfo[] = data.map((country) => ({
id: crypto.randomUUID(),
name: country.name.common,
capital: country.capital?.[0],
flagUrl: country.flags.svg,
isFavorite: false,
}));
setCountries(countries);
};
getCountryList();
}, []);
// App.tsx
const { data, isLoading } = useQuery<Country[], Error, CountryInfo[]>({
queryKey: ["countries"],
queryFn: getData,
select: (data: Country[]) => {
return data.map((country) => ({
id: crypto.randomUUID(),
name: country.name.common,
capital: country.capital?.[0],
flagUrl: country.flags.svg,
isFavorite: false,
}));
},
});
useEffect(() => {
if (data) setCountries(data);
}, [isLoading]);
useQuery
의 Generic
에는 네 개의 타입 파라미터를 넣을 수 있다!
첫 번째 타입 파라미터로는 queryFn
에서 return되는 데이터의 타입을 넣어주고, 두 번째 타입 파라미터로는 Error
의 타입을 넣어준다! 이 두 개가 필수인 듯 하다!
세 번째는 useQuery
에서 select
옵션을 사용하면 select
에서 반환되는 return 타입을 별도로 적어주는 듯 하다!
나는 queryFn
에서 return되는 자료를 그대로 쓰지 않고 가공해서 쓰고 싶었다. 그래서 useQuery
에서 select
옵션을 쓰고 있었기 때문에 세 번째 제네릭 타입까지 추가했다!
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import "./App.css";
import { getData } from "./api/api.countries";
import CountryCardList from "./components/CountryCardList";
import { Country, CountryInfo } from "./types/country";
function App() {
const [countries, setCountries] = useState<CountryInfo[]>([]);
const handleToggleIsFavorite = (id: CountryInfo["id"]) =>
setCountries((prev) =>
prev.map((country) =>
country.id === id
? { ...country, isFavorite: !country.isFavorite }
: country
)
);
const { data, isLoading } = useQuery<Country[], Error, CountryInfo[]>({
queryKey: ["countries"],
queryFn: getData,
select: (data: Country[]) => {
return data.map((country) => ({
id: crypto.randomUUID(),
name: country.name.common,
capital: country.capital?.[0],
flagUrl: country.flags.svg,
isFavorite: false,
}));
},
});
useEffect(() => {
if (data) setCountries(data);
}, [isLoading]);
if (isLoading) return <>Loading...</>;
return (
<>
<CountryCardList
listTitle="My Favorite Countries"
countries={countries}
handleToggleIsFavorite={handleToggleIsFavorite}
isFavorite={true}
/>
<CountryCardList
listTitle="All Countries"
countries={countries}
handleToggleIsFavorite={handleToggleIsFavorite}
isFavorite={false}
/>
</>
);
}
export default App;
import { CountryInfo } from "../../types/country";
import CountryCard from "./CountryCard";
import ListTitle from "./ListTitle";
type CountryCardListProps = {
countries: CountryInfo[];
handleToggleIsFavorite: (id: string) => void;
isFavorite: boolean;
listTitle: string;
};
function CountryCardList({
countries,
handleToggleIsFavorite,
isFavorite,
listTitle,
}: CountryCardListProps) {
return (
<>
<ListTitle>{listTitle}</ListTitle>
<ul className="grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
{countries
.filter((item) => item.isFavorite === isFavorite)
.map((item, i) => {
return (
<li key={i}>
<CountryCard onClick={handleToggleIsFavorite} {...item} />
</li>
);
})}
</ul>
</>
);
}
export default CountryCardList;
import { CountryInfo } from "../../types/country";
interface CountryCardProps extends CountryInfo {
onClick: (id: string) => void;
}
function CountryCard({
id,
onClick,
name,
flagUrl,
capital,
}: CountryCardProps) {
return (
<div
onClick={() => onClick(id)}
className={`box-border rounded-xl flex flex-col items-center p-4 w-full h-full gap-4 shadow-md hover:shadow-lg bg-[#FCFDFF]`}
>
<img className="object-cover w-24 " src={flagUrl} alt="국기" />
<h3 className="self-start text-lg font-semibold ">{name}</h3>
<p className="self-start text-md">{capital}</p>
</div>
);
}
export default CountryCard;