TIL #51 플러스주차 개인과제 (NextJS)

DO YEON KIM·2024년 7월 2일
1

부트캠프

목록 보기
51/72

하루 하나씩 작성하는 TIL #51



🔥 필수 구현 사항 입니다.
  • App router 기반, typescript 사용, tailwindcss 사용을 베이스로 한 Nextjs 14 버전으로 프로젝트가 구성되어야 합니다.
  • Layout 에서 Title, description 에 대한 Metadata 를 설정하고, 어플리케이션 전체에 적용될 UI 를 구현합니다.
  • 151번까지의 포켓몬 리스트를 보여주는 페이지를 구현합니다.
    • root 페이지에서 보여줘도 무방합니다.
    • 반드시 클라이언트 컴포넌트로 작성해주세요. (use client 사용)
    • 포켓몬 리스트 페이지에서 직접적으로 관련 api 를 호출하는 것이 아닌, nextjs api 폴더 내에서 해당 로직에 대한 api 를 구현해야 합니다. (포켓몬 리스트 페이지 → Nextjs api 호출 → Nextjs 서버가 포켓몬 API 호출)
    • 151개의 포켓몬을 불러오는 API 는 로직 제공해드립니다. (직접 구현하셔도 무방합니다.)
  • 특정 포켓몬의 디테일을 보여주는 페이지를 구현합니다.
    • 다이나믹 페이지로 구성해주셔야 합니다. (힌트 : /[id])
    • 특정 포켓몬 디테일에 대한 정보를 가져오는 로직을 nextjs api handler 를 통해서 구현하도록 합니다. (리스트와 동일)
    • 단, 반드시 서버 컴포넌트로 작성해주세요. (use client 사용 금지)
    • 포켓몬 리스트에서 특정 포켓몬을 클릭했을 때 디테일페이지를 보여주고, 디테일 페이지에서 리스트로 돌아 갈 수 있는 로직을 만들어 주시면 더 좋을 것 같습니다.
  • 포켓몬 리스트와 상세페이지에서 항상 포켓몬들의 이미지를 보여주도록 합시다. Nextjs 가 제공하는 를 이용합니다.
  • 포켓몬 데이터에 대한 타입, 컴포넌트들의 props 에 대한 타입 등 어플리케이션 전체에 적절한 타입이 명시되어야 합니다.

1. 프로젝트 셋업

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


2. Layout 구성

  • 상세가이드

    1. 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 렌더링. 레이아웃 컴포넌트가 모든 페이지 컴포넌트를 감싸도록 함.

2. 어플리케이션 전체에 적용 될 UI 를 작성합니다.

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>&copy; 2024 Pokemon. All rights reserved.</p>
      </footer>
    </body>
  </html>
  );
}
 // children 렌더링. 레이아웃 컴포넌트가 모든 페이지 컴포넌트를 감싸도록 함.

이렇게 작성 후, 출력 화면 확인을 위해 page.tsx를 대충 밀고

import Image from "next/image";

export default function Home() {
  return <h1>가보자고</h1>;
}

위와 같이 작성 후 출력해보면

굉장히 못생겼지만 일단 출력되는 모습을 확인 가능하다.


3. /pokemons api 구성

  • 상세가이드

1. app 폴더 아래에 api 폴더를 생성하고 그 안에 pokemons 폴더를 생성합니다.

2. pokemons 폴더 안에 route.ts 파일을 생성합니다. (최종경로 src/app/api/pokemons/route.ts)

3. 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" });
  }
};

4. PokemonList 컴포넌트 작성

  • 상세가이드

1. 포켓몬들의 리스트를 보여줄 컴포넌트를 구성합니다. 단! ‘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;

  1. 포켓몬 리스트를 불러오는 외부 API 를 직접 호출하는 것이 아니라, 우리가 ‘Step 3’ 에서 만들어준 nextjs api 를 이용해서 데이터를 가지고 와야 합니다. src/app/pokemons/route.ts
    데이터를 불러오고 관리해주는 방식에 대해서는 자유롭게 해주세요

PokemonList 컴포넌트는 useEffect 훅을 사용하여 /api/pokemons 엔드포인트에서 데이터를 가져오도록 설정.

이 엔드포인트는 3에서 작성한 Next.js API 라우트를 호출.

=> 외부 API를 직접 호출하는 대신, 내부 API를 통해 데이터를 가져오게 됨.


  1. 포켓몬 데이터는 아래와 같이 구성하도록 합시다. response 값을.. 있는 그대로 타입을 정해주기에는 과제가 끝나지 않을 것 같아요.
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 } }[];
}

위에서 적용 완료.


  1. PokemonList 컴포넌트가 완성이 되면, 적절한 페이지에 해당 컴포넌트를 렌더링 시켜주세요. root(’/’) 페이지에 하셔도 무방하시고, ‘/pokemonList’ 같은 라우터를 설정해주시고 해당 페이지 안에서 렌더링 해주셔도 무방합니다.
import PokemonList from './components/PokemonList';

export default function HomePage() {
  return (
    <div className="container mx-auto p-4">
      <PokemonList />
    </div>
  );
}

5. /pokemons/[id] api 구성

  • 상세가이드

1. api/pokemons 폴더 안에 route.ts 파일을 생성합니다. (최종경로 src/app/api/pokemons/[id]/route.ts)

2. 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" });
  }
};

6. PokemonDetail 컴포넌트 작성

  • 상세가이드

1. PokemonList 에서 포켓몬을 선택했을 때, 포켓몬에 대한 상세페이지를 보여주는 페이지를 만들어 줍시다. 단! 서버 컴포넌트로 작성하도록 합니다. - ‘use client’ 를 사용하지 않습니다! (필수 요구사항!)

2. 포켓몬 리스트를 불러오는 외부 API 를 직접 호출하는 것이 아니라, 우리가 ‘Step 5’ 에서 만들어준 nextjs api 를 이용해서 데이터를 가지고 와야 합니다. src/app/pokemons/[id]/route.ts

데이터를 불러오고 관리해주는 방식에 대해서는 자유롭게 해주세요

3. 포켓몬 리스트로 다시 돌아갈 수 있는 UI 도 구성해줍시다.


6번은 ,, 내일 잡다한 에러 해결과 함께 작성하도록 할 예정.

포켓몬 정보를 불러오는데에 404 에러 이슈가 있기 때문. 그 외에는 정상 작동.

그 외 잡다한 css도 내일 수정 예정.

profile
프론트엔드 개발자를 향해서

0개의 댓글