💥 포켓몬 도감 만들기!
☘️ 구현해야 할 것
- 홈페이지, 도감페이지, 디테일 페이지 구성
- 페이지 라우팅 구현
- 컴포넌트를 구분하여 도감페이지를 만들 것
- 대시보드에 포켓몬 카드를 추가하고 삭제할 수 있음
- 디테일 페이지에 queryString으로 포켓몬 ID를 받아 상세 정보 표시
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;
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;
- styled-component를 사용하기 위해
yarn add styled components 해준 후 만들어 준 컴포넌트를 아래 return 에 적용- 포켓몬 도감 시작하기 버튼을 누르면 페이지가 넘어갈 수 있도록 useNavigate()를 사용 위에서
const navigate = useNavigate()로 import
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;
ㅁ 조건 1 : 선택된 포켓몬카드의 정보를 받아 대쉬보드에서는 6개까지의 카드만 보여줄 수 있음으로 카드의 배열 개수를 6개 이상이면 alert창을 띄우도록 만듬
ㅁ 조건 2: 저장되어 있는 배열들을 확인하여 선택한 카드의 id가 이미 저장되어 있는 id 인지 확인 후 존재하면 alert창을 띄움
ㅁ 조건 3 : 조건1,2에 해당하지 않으면 원래 저장되어있던 배열에 새로운 카드배열을 추가
ㅁ 조건 1 : 이미 저장되어있는 id와 클릭된 카드 id를 비교해 선택되지 않은 카드만 반환
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>
- props로 받은 selectedPokemon 을 이용해
저장된 카드들을 보여줌. 저장된 배열이 없으면 기본이미지를 보여줌
기본이미지를 6개만 보여줄 것이기 때문에 Array(6).fill()로 세팅해주고 map함수를 돌려 selectedPokemon에 index가 있으면 저장받은 카드의 정보들로 카드정보를 보여주고 없으면 기본이미지로 세팅
PokemonCard 컴포넌트에 정보를 props로 내려줌
props drilling이기 때문에 이후 정리할 예정
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;
props로 받아온 포켓몬 정보로 map함수를 이용해 카드로 하나씩 만들어줌 어떠한 함수 발생 후 페이지 이동이 아닌 단순 페이지 이동이라 Link를 이용해 페이지가 이동될 수 있도록 연결
각 버튼을 클릭시 state에 카드가 저장될 수 있도록 props로 받아온 onAddPokemon함수 연결
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;
- 카드가 클릭하면 Link를 통해 PokemonDetail/:id로 이동할 수 있도록 PokemonCard.jsx에서 만들어주었다.
id (queryString)을 가져오기 위해 useParmas사용- useParams로 받아온 id가 앞에 :가붙어있어서 slice로 제거
- import 한 MOCK_DATA의 id와 useParams로 받아온 id가 일치하는지 확인
- undefined 일 때는 포켓몬 정보를 찾을 수 없습니다.
- 정보가 있다면 이미지를 보여줌.
- button onClick = (data) =>{} 를 사용하는데 props로 받은 data와 click이벤트로 받은 data 변수명이 같아 내가 의도한 것은 props로 받은 data를 사용하는 것이 였는데 click 이벤트의 data가 들어가 카드를 만드는 함수가 구현되지 않았다.
=> 무엇이 잘못된 지 몰라 한참 찾았는데 변수명 & 함수명을 신경써야한다는 것을 다시한번 느꼈고 중요하다는 것을 알게되었다. 주의하자 꼭.- useParams로 queryString을 가져와 데이터를 표시하려고 하는데 데이터가 표시되지 않았었다.
내가 라우터에서 정의한대로 id이름을 쓰지 않고 컴포넌트에서 useParams를 만들 때 다른 이름으로 만들어 이런 오류가 생긴 것
2-2 : 기본적으로 useParams는 문자열로 들어온다.
=> 데이터에 있는 id가 숫자라면 parseInt()로 바꿔주어야 한다.
2-3 : useParams로 가져온 id가 계속 :id로 들어와 데이터의 정보와 일치하지 않았다.
=> 추가처리로 slice(1)을 해주어 id만 비교할 수 있도록 하였다.- parameter를 사용하기 위해서는 useParams를 사용해 가져와야하고
path name을 사용하기 위해서는 useLocation을 사용해야 한다.!!
위의 어려웠던 점과 헷갈렸던 점에서 새롭게 알게된 것도 많았지만 검색하면서 알게된 또 다른 것들도 있었다.
- public 폴더 사용 시:
public 폴더에 있는 이미지는 import할 필요가 없다. 이 폴더에 이미지를 저장하고, 이미지 URL을 직접 사용할 수 있다. public 폴더에 있는 이미지를 사용하는 방법은 다음과 같다:- 이미지 파일을 public 폴더에 저장 (예: public/assets/imgs/pokeball-13iwdk7Y.png).
- 경로를 사용하여 이미지 불러오기
- 웹팩이나 CRA(create-react-app)에서는 이미지 파일 경로가 public 폴더를 기준으로 설정됩니다. 이미지가 public 폴더 하위에 있다면, 경로를 "../assets/imgs/pokeball-13iwdk7Y.png"에서 "/assets/imgs/pokeball-13iwdk7Y.png"으로 변경해야 한다.
- parseInt 함수의 두 번째 인수
parseInt 함수에서 두 번째 인수로 전달되는 10은 진법을 의미한다.=. 이 인수를 사용하여 숫자를 10진수로 해석하겠다고 명시적으로 지정한다.
parseInt 함수는 문자열을 정수로 변환하는 데 사용된다. 이 함수는 두 개의 인수를 받을 수 있습니다:- 문자열: 변환할 문자열.
- 진법(radix): 문자열이 어떤 진법(수의 진법)으로 해석될지를 나타내는 정수.
진법의 예:
- 10은 10진수를 의미
- 2는 2진수를 의미
- 16은 16진수를 의미
styled-components를 이용하여 작업하는 것도 react-route-dom을 이용하여 페이지를 이동시키는 것도 img를 import해보는 것도 이번 프로젝트를 하면서 처음 해봤다. 이외에도 처음 보고 해본 것들이 많았는데 내 것으로 만드려면 몇번 더 시행착오를 겪어야할 것 같다....!
하지만 그 과정이 머릿속에서 그려지고 실제로 구현됐을 때 뿌듯함과 성취감은 힘듦을 잊게 해주는 것 ㄱ...ㅏㅌ다....! 👀
포기하지 말고 하루하루 이것을 반복하며 기반을 단단하게 쌓아보자!! ❤️