Day27_개인프로젝트 포켓몬도감 만들기

정소현·2024년 8월 22일
0

스파르타

목록 보기
34/50

💥 포켓몬 도감 만들기!

☘️ 구현해야 할 것

  1. 홈페이지, 도감페이지, 디테일 페이지 구성
  2. 페이지 라우팅 구현
  3. 컴포넌트를 구분하여 도감페이지를 만들 것
  4. 대시보드에 포켓몬 카드를 추가하고 삭제할 수 있음
  5. 디테일 페이지에 queryString으로 포켓몬 ID를 받아 상세 정보 표시

구현페이지 이미지

home 홈페이지

dex 카드페이지

detail 카드상세페이지

진행했던 순서

  1. yarn create vite "" --template react 로 프로젝트 셋업
  2. pages폴더를 만들어 Home, Dex, PokemonDetail/.jsx 파일 생성
  3. shared폴더 생성 후 Router.jsx폴더를 만들어줌
  4. yarn add react-router-dom

✨ Router.jsx 에 Route path 와 element를 연결

import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "../pages/Home";
import Dex from "../pages/Dex";
import PokemonDetail from "../pages/PokemonDetail";

const Router = () => {
  return (
    <div>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/Dex" element={<Dex />} />
          <Route path="/PokemonDetail/:id" element={<PokemonDetail />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
};

export default Router;

☘️ Home.jsx 작성

import React from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";

const Wrap = styled.div`
  width: 90%;
  height: 100vh;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 30px;
`;

const StartButton = styled.button`
  width: 200px;
  height: 50px;
  border: none;
  border-radius: 10px;
  font-family: "content";
  font-size: 1.5rem;
  cursor: pointer;
  box-shadow: 0 10px 20px rgba(151, 151, 151, 0.23),
    0 6px 6px rgba(151, 151, 151, 0.23);

  transition: 0.2s;
  &:hover {
    box-shadow: 0 10px 20px rgba(151, 151, 151, 0.23),
      0 6px 6px rgba(151, 151, 151, 0.5);
    transform: translate3d(0, -5%, 0);
  }
`;

const HomeH1 = styled.h1`
  font-size: 9rem;
  font-family: "main";
  font-weight: 500;
`;

const Home = () => {
  const navigate = useNavigate();
  return (
    <Wrap>
      <HomeH1>POKEMON</HomeH1>
      <StartButton onClick={() => navigate("/Dex")}>
        포켓몬 도감 시작하기
      </StartButton>
    </Wrap>
  );
};

export default Home;

Home.jsx 코드 설명

  1. styled-component를 사용하기 위해
    yarn add styled components 해준 후 만들어 준 컴포넌트를 아래 return 에 적용
  2. 포켓몬 도감 시작하기 버튼을 누르면 페이지가 넘어갈 수 있도록 useNavigate()를 사용 위에서
    const navigate = useNavigate()로 import

☘️ Dex.jsx 작성

import React from "react";
import Dashboard from "../components/Dashboard";
import PokemonList from "../components/PokemonList";
import { useState } from "react";
import MOCK_DATA from "../mock";

const Dex = () => {
  const [selectedPokemon, setSelectedPokemon] = useState([]);

  //포켓몬 추가로직
  const addPokemon = (pokemon) => {
    // 7개 이상일 경우 alert창 띄우기

    if (selectedPokemon.length >= 6) {
      alert(
        "포켓몬은 6개까지만 선택가능합니다. 대쉬보드에서 포켓몬 삭제 후 추가해주세요"
      );
    } else if (
      selectedPokemon.some((prevPokemon) => prevPokemon.id === pokemon.id)
    ) {
      alert("이미 존재하는 포켓몬입니다.");
    } else {
      setSelectedPokemon((prevSelected) => [...prevSelected, pokemon]);
    }
  };

  //포켓몬 삭제로직
  const removePokemon = (pokemon) => {
    setSelectedPokemon((prevSelected) =>
      prevSelected.filter((prevPokemon) => prevPokemon.id !== pokemon.id)
    );
  };

  return (
    <div>
      <Dashboard
        selectedPokemon={selectedPokemon}
        onRemovePokemon={removePokemon}
      />
      <PokemonList pokemonData={MOCK_DATA} onAddPokemon={addPokemon} />
    </div>
  );
};

export default Dex;

✨ Dex.jsx 코드 설명

  1. Dex.jsx페이지에서 선택한 포켓몬들이 나타나게 할 Dashboard 컴포넌트와 PokemonList 컴포넌트로 나눔
  2. PokemonList에서 선택한 카드를 저장할 수 있도록 useState()를 사용해 선택한 카드를 추가, 삭제 하는 함수를 생성
  3. MOCK_DATA에 있는 정보를 띄우기 위해 import

1. addPokemon

ㅁ 조건 1 : 선택된 포켓몬카드의 정보를 받아 대쉬보드에서는 6개까지의 카드만 보여줄 수 있음으로 카드의 배열 개수를 6개 이상이면 alert창을 띄우도록 만듬
ㅁ 조건 2: 저장되어 있는 배열들을 확인하여 선택한 카드의 id가 이미 저장되어 있는 id 인지 확인 후 존재하면 alert창을 띄움
ㅁ 조건 3 : 조건1,2에 해당하지 않으면 원래 저장되어있던 배열에 새로운 카드배열을 추가

2. removePokemon

ㅁ 조건 1 : 이미 저장되어있는 id와 클릭된 카드 id를 비교해 선택되지 않은 카드만 반환


☘️ Dashboard.jsx 작성

const Dashboard = ({ selectedPokemon, onRemovePokemon }) => {
  const defaultImg = "/assets/imgs/pokeball-13iwdk7Y.png";
  return (
    <div>
      <DashboardContainer>
        <DashboardH1>나만의 포켓몬</DashboardH1>
        <DashBoardCards>
          {Array(6)
            .fill()
            .map((_, index) => (
              <DashBoardCard key={index}>
                {selectedPokemon[index] ? (
                  <>
                    <img
                      src={selectedPokemon[index].img_url}
                      alt={selectedPokemon[index].korean_name}
                    ></img>
                    <p>{selectedPokemon[index].korean_name}</p>

                    <DeleteButton
                      onClick={() => onRemovePokemon(selectedPokemon[index])}
                    >
                      삭제
                    </DeleteButton>
                  </>
                ) : (
                  <DefaultImg src={defaultImg}></DefaultImg>
                )}
              </DashBoardCard>
            ))}
        </DashBoardCards>
      </DashboardContainer>
    </div>

✨ Dashboard.jsx 설명

  1. props로 받은 selectedPokemon 을 이용해
    저장된 카드들을 보여줌. 저장된 배열이 없으면 기본이미지를 보여줌
    기본이미지를 6개만 보여줄 것이기 때문에 Array(6).fill()로 세팅해주고 map함수를 돌려 selectedPokemon에 index가 있으면 저장받은 카드의 정보들로 카드정보를 보여주고 없으면 기본이미지로 세팅

☘️ PokemonList.jsx

PokemonCard 컴포넌트에 정보를 props로 내려줌
props drilling이기 때문에 이후 정리할 예정


☘️ PokemonCard.jsx

const PokemonCard = ({ pokemonData, onAddPokemon }) => {
  return (
    <>
      <CharCards>
        {pokemonData.map((data) => (
          <CharCard key={data.id}>
            <Link
              to={`/PokemonDetail/:${data.id}`}
              style={{ textDecoration: "none" }}
            >
              <img src={data.img_url} alt={data.korean_name} />
              <CharCardH3>{data.korean_name}</CharCardH3>
              <p>NO.{data.id}</p>
            </Link>
            <CardButton type="button" onClick={() => onAddPokemon(data)}>
              추가
            </CardButton>
          </CharCard>
        ))}
      </CharCards>
    </>
  );
};

export default PokemonCard;

✨ PokemonCard.jsx 코드풀이

props로 받아온 포켓몬 정보로 map함수를 이용해 카드로 하나씩 만들어줌 어떠한 함수 발생 후 페이지 이동이 아닌 단순 페이지 이동이라 Link를 이용해 페이지가 이동될 수 있도록 연결
각 버튼을 클릭시 state에 카드가 저장될 수 있도록 props로 받아온 onAddPokemon함수 연결


☘️ PokemonDetail.jsx

const PokemonDetail = () => {
  // parameter가져오기
  const { id } = useParams();
  // MOCK_DATA의 id앞 : 제거
  const paramsId = id.slice(1);
  // id에 일치하는 MOCK_DATA 가져오기
  const detailData = MOCK_DATA.find(
    (pokemon) => pokemon.id === parseInt(paramsId)
  );
  const navigate = useNavigate();
  console.log(paramsId);
  if (!detailData) {
    return (
      <Wrap>
        <DetailCard>
          <ContentP>
            포켓몬 정보를
            <br /> 찾을 수 없습니다.
          </ContentP>
          <ContentButton onClick={() => navigate("/Dex")}>
            뒤로 가기
          </ContentButton>
        </DetailCard>
      </Wrap>
    );
  }
  return (
    <Wrap>
      <DetailCard>
        <img
          style={{
            width: "100px",
            padding: "1rem",
          }}
          src={detailData.img_url}
          alt={detailData.korean_name}
        />
        <ContentH2>{detailData.korean_name}</ContentH2>
        <ContentP>
          Type : {detailData.types} <br />
          {detailData.description}
        </ContentP>
        <Buttons>
          <ContentButton onClick={() => navigate("/Dex")}>
            뒤로 가기
          </ContentButton>
          <ContentButton>추가하기</ContentButton>
        </Buttons>
      </DetailCard>
    </Wrap>
  );
};

export default PokemonDetail;

✨ PokemonDetail 코드풀이

  1. 카드가 클릭하면 Link를 통해 PokemonDetail/:id로 이동할 수 있도록 PokemonCard.jsx에서 만들어주었다.
    id (queryString)을 가져오기 위해 useParmas사용
  2. useParams로 받아온 id가 앞에 :가붙어있어서 slice로 제거
  3. import 한 MOCK_DATA의 id와 useParams로 받아온 id가 일치하는지 확인
  4. undefined 일 때는 포켓몬 정보를 찾을 수 없습니다.
  5. 정보가 있다면 이미지를 보여줌.

🥵 어려웠던 점 & 헷갈렸던 점

  1. button onClick = (data) =>{} 를 사용하는데 props로 받은 data와 click이벤트로 받은 data 변수명이 같아 내가 의도한 것은 props로 받은 data를 사용하는 것이 였는데 click 이벤트의 data가 들어가 카드를 만드는 함수가 구현되지 않았다.
    => 무엇이 잘못된 지 몰라 한참 찾았는데 변수명 & 함수명을 신경써야한다는 것을 다시한번 느꼈고 중요하다는 것을 알게되었다. 주의하자 꼭.
  2. useParams로 queryString을 가져와 데이터를 표시하려고 하는데 데이터가 표시되지 않았었다.
    내가 라우터에서 정의한대로 id이름을 쓰지 않고 컴포넌트에서 useParams를 만들 때 다른 이름으로 만들어 이런 오류가 생긴 것
    2-2 : 기본적으로 useParams는 문자열로 들어온다.
    => 데이터에 있는 id가 숫자라면 parseInt()로 바꿔주어야 한다.
    2-3 : useParams로 가져온 id가 계속 :id로 들어와 데이터의 정보와 일치하지 않았다.
    => 추가처리로 slice(1)을 해주어 id만 비교할 수 있도록 하였다.
  3. parameter를 사용하기 위해서는 useParams를 사용해 가져와야하고
    path name을 사용하기 위해서는 useLocation을 사용해야 한다.!!

🌟 새롭게 알게된 것

위의 어려웠던 점과 헷갈렸던 점에서 새롭게 알게된 것도 많았지만 검색하면서 알게된 또 다른 것들도 있었다.

  1. public 폴더 사용 시:
    public 폴더에 있는 이미지는 import할 필요가 없다. 이 폴더에 이미지를 저장하고, 이미지 URL을 직접 사용할 수 있다. public 폴더에 있는 이미지를 사용하는 방법은 다음과 같다:
  2. 이미지 파일을 public 폴더에 저장 (예: public/assets/imgs/pokeball-13iwdk7Y.png).
  3. 경로를 사용하여 이미지 불러오기
  4. 웹팩이나 CRA(create-react-app)에서는 이미지 파일 경로가 public 폴더를 기준으로 설정됩니다. 이미지가 public 폴더 하위에 있다면, 경로를 "../assets/imgs/pokeball-13iwdk7Y.png"에서 "/assets/imgs/pokeball-13iwdk7Y.png"으로 변경해야 한다.
  5. parseInt 함수의 두 번째 인수
    parseInt 함수에서 두 번째 인수로 전달되는 10은 진법을 의미한다.=. 이 인수를 사용하여 숫자를 10진수로 해석하겠다고 명시적으로 지정한다.
    parseInt 함수는 문자열을 정수로 변환하는 데 사용된다. 이 함수는 두 개의 인수를 받을 수 있습니다:
  6. 문자열: 변환할 문자열.
  7. 진법(radix): 문자열이 어떤 진법(수의 진법)으로 해석될지를 나타내는 정수.
    진법의 예:
  • 10은 10진수를 의미
  • 2는 2진수를 의미
  • 16은 16진수를 의미

🌟 작업하며 느낀 점

styled-components를 이용하여 작업하는 것도 react-route-dom을 이용하여 페이지를 이동시키는 것도 img를 import해보는 것도 이번 프로젝트를 하면서 처음 해봤다. 이외에도 처음 보고 해본 것들이 많았는데 내 것으로 만드려면 몇번 더 시행착오를 겪어야할 것 같다....!
하지만 그 과정이 머릿속에서 그려지고 실제로 구현됐을 때 뿌듯함과 성취감은 힘듦을 잊게 해주는 것 ㄱ...ㅏㅌ다....! 👀
포기하지 말고 하루하루 이것을 반복하며 기반을 단단하게 쌓아보자!! ❤️

0개의 댓글