하루 하나씩 작성하는 TIL #51
use client 사용
)/[id]
)use client 사용 금지
)npx create-next-app@latest
What is your project named? my-app
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use src/ directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/)? Yes
What import alias would you like configured? @/
- ✨ update : 해당 파일에 새로운 기능이 생김
- 🎉 add : 없던 파일을 생성함, 초기 세팅
- 🐛 bugfix : 버그 수정
- ♻ refactor : 코드 리팩토링
- 🩹 fix : 코드 수정
- 🚚 move : 파일 옮김/정리
- 🔥 del : 기능/파일을 삭제
- 🍻 test : 테스트 코드를 작성
- 💄 style : css
- 🙈 gitfix : gitignore 수정
- 💡 comment : 주석 변경
- 🔨 script : package.json 변경(npm 설치 등)
- 📝 chore : 그 외 잡다한 것들
readme 작성해주기.
cd my-app
yarn install
yarn dev
상세가이드
src/app/layout.tsx
에서 metadata
를 선언하고 어플리케이션의 title 과 description 을 설정해줍니다.import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "POkemon", //웹 사이트 제목
description: "POkemon book", //웹 사이트의 기본 설명
};
export default function RootLayout({
children,
}: {
children: React.ReactNode; //children이 React 노드임을 명시
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
// children 렌더링. 레이아웃 컴포넌트가 모든 페이지 컴포넌트를 감싸도록 함.
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "POkemon", //웹 사이트 제목
description: "POkemon book", //웹 사이트의 기본 설명
};
export default function RootLayout({
children,
}: {
children: React.ReactNode; //children이 React 노드임을 명시
}) {
return (
<html lang="en">
<body>
<header className="bg-lime-600 text-white p-4">
<h1 className="text-2xl font-bold">포켓몬 도감</h1>
</header>
<main className="container mx-auto p-4">
{children}
</main>
<footer className="bg-lime-600 text-white p-4 text-center">
<p>© 2024 Pokemon. All rights reserved.</p>
</footer>
</body>
</html>
);
}
// children 렌더링. 레이아웃 컴포넌트가 모든 페이지 컴포넌트를 감싸도록 함.
이렇게 작성 후, 출력 화면 확인을 위해 page.tsx를 대충 밀고
import Image from "next/image";
export default function Home() {
return <h1>가보자고</h1>;
}
위와 같이 작성 후 출력해보면
굉장히 못생겼지만 일단 출력되는 모습을 확인 가능하다.
/pokemons
api 구성src/app/api/pokemons/route.ts
)route.ts
폴더 안에 아래 코드를 붙여넣어주세요.import { NextResponse } from "next/server";
import axios from "axios";
const TOTAL_POKEMON = 151;
export const GET = async (request: Request) => {
try {
const allPokemonPromises = Array.from({ length: TOTAL_POKEMON }, (_, index) => {
const id = index + 1;
return Promise.all([
axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`),
axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`)
]);
});
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" });
}
};
PokemonList
컴포넌트 작성use client
’ 를 사용하여 클라이언트 컴포넌트
로 작성하도록 합니다. (필수 요구사항!)'use client'; //필수 요구사항
import { useEffect, useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
type Pokemon = {
id: number;
name: string; //포켓몬 영어이름
korean_name: string;
height: number;
weight: number;
sprites: { front_default: string }; //이미지 url
types: { type: { name: string; korean_name: string } }[]; //타입
abilities: { ability: { name: string; korean_name: string } }[]; //능력
moves: { move: { name: string; korean_name: string } }[]; //기술
};
const PokemonList = () => {
const [pokemons, setPokemons] = useState<Pokemon[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchPokemons = async () => { //포켓몬 데이터 가져오기
const response = await fetch('/api/pokemons');
const data = await response.json(); //응답 데이터를 json 형식으로 반환
setPokemons(data);
setLoading(false);
};
fetchPokemons();
}, []);
if (loading) {
return <p>Loading...</p>;
}
return (
<div className="grid grid-cols-2 gap-4">
{pokemons.map(pokemon => ( //각 포켓몬 객체에 대해 jsx 반환
<div key={pokemon.id} className="p-4 border rounded-lg">
<Link href={`/pokemon/${pokemon.id}`}>
<div>
<Image
src={pokemon.sprites.front_default}
alt={pokemon.korean_name}
width={96}
height={96}
/>
<h2>{pokemon.korean_name} </h2>
<h2>도감 번호 : {pokemon.id}</h2>
</div>
</Link>
</div>
))}
</div>
);
};
export default PokemonList;
src/app/pokemons/route.ts
PokemonList 컴포넌트는 useEffect 훅을 사용하여 /api/pokemons 엔드포인트에서 데이터를 가져오도록 설정.
이 엔드포인트는 3에서 작성한 Next.js API 라우트를 호출.
=> 외부 API를 직접 호출하는 대신, 내부 API를 통해 데이터를 가져오게 됨.
type Pokemon = {
id: number;
name: string;
korean_name: string;
height: number;
weight: number;
sprites: { front_default: string };
types: { type: { name: string; korean_name: string } }[];
abilities: { ability: { name: string; korean_name: string } }[];
moves: { move: { name: string; korean_name: string } }[];
}
위에서 적용 완료.
import PokemonList from './components/PokemonList';
export default function HomePage() {
return (
<div className="container mx-auto p-4">
<PokemonList />
</div>
);
}
/pokemons/[id]
api 구성src/app/api/pokemons/[id]/route.ts
)route.ts
폴더 안에 아래 코드를 붙여넣어주세요.// src/app/api/pokemons/[id]/route.ts
import { NextResponse } from "next/server";
import axios from "axios";
export const GET = async (
request: Request,
{ params }: { params: { id: string } },
) => {
const { id } = params;
try {
const response = await axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`);
const speciesResponse = await axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`);
const koreanName = speciesResponse.data.names?.find(
(name: any) => name.language.name === "ko",
);
const typesWithKoreanNames = await Promise.all(
response.data.types.map(async (type: any) => {
const typeResponse = await axios.get(type.type.url);
const koreanTypeName =
typeResponse.data.names?.find(
(name: any) => name.language.name === "ko",
)?.name || type.type.name;
return { ...type, type: { ...type.type, korean_name: koreanTypeName } };
}),
);
const abilitiesWithKoreanNames = await Promise.all(
response.data.abilities.map(async (ability: any) => {
const abilityResponse = await axios.get(ability.ability.url);
const koreanAbilityName =
abilityResponse.data.names?.find(
(name: any) => name.language.name === "ko",
)?.name || ability.ability.name;
return {
...ability,
ability: { ...ability.ability, korean_name: koreanAbilityName },
};
}),
);
const movesWithKoreanNames = await Promise.all(
response.data.moves.map(async (move: any) => {
const moveResponse = await axios.get(move.move.url);
const koreanMoveName =
moveResponse.data.names?.find(
(name: any) => name.language.name === "ko",
)?.name || move.move.name;
return { ...move, move: { ...move.move, korean_name: koreanMoveName } };
}),
);
const pokemonData = {
...response.data,
korean_name: koreanName?.name || response.data.name,
types: typesWithKoreanNames,
abilities: abilitiesWithKoreanNames,
moves: movesWithKoreanNames,
};
return NextResponse.json(pokemonData);
} catch (error) {
console.error("Error fetching Pokemon data:", error);
return NextResponse.json({ error: "Failed to fetch data" });
}
};
PokemonDetail
컴포넌트 작성PokemonList
에서 포켓몬을 선택했을 때, 포켓몬에 대한 상세페이지를 보여주는 페이지를 만들어 줍시다. 단! 서버 컴포넌트
로 작성하도록 합니다. - ‘use client
’ 를 사용하지 않습니다! (필수 요구사항!)src/app/pokemons/[id]/route.ts
6번은 ,, 내일 잡다한 에러 해결과 함께 작성하도록 할 예정.
포켓몬 정보를 불러오는데에 404 에러 이슈가 있기 때문. 그 외에는 정상 작동.
그 외 잡다한 css도 내일 수정 예정.