props-drilling
방식 입니다.우선 패키지를 먼저 설치해준다.
npm install react-router-dom
설치가 완료되면 폴더를 하나 만들고!
Router.jsx
까지 만들어준다.//Router.jsx import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import Home from "../pages/Home"; import Dex from "../pages/Dex"; import PokemonDetail from "../pages/PokemonDetail"; function AppRouter() { return ( <Router> <Routes> <Route path="/" element={<Home />} /> <Route path="/dex" element={<Dex />} /> <Route path="/pokemon-detail" element={<PokemonDetail />} /> </Routes> </Router> ); } export default AppRouter;
App,jsx
에 임포트한다.//App.jsx import AppRouter from "./shared/Router"; function App() { return <AppRouter />; } export default App;
이건 처음해보는거라서 처음에는 생소했다.
그런데 하다보니까 너무 편안하고 좋았다.
이것 또한 마찬가지로 패키지를 먼저 설치해준다.
npm install styled-components
패지키 설치가 완료되면 폴더를 하나 만들자!
//HomeStyles.js
import styled from "styled-components";
const MainPage = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
`;
const PokemonLogo = styled.img`
width: 600px;
margin-bottom: 20px;
`;
const MainButton = styled.button`
padding: 10px 20px;
font-size: 18px;
cursor: pointer;
border-radius: 5px;
background-color: rgb(255, 0, 0);
color: white;
border: none;
transition: background-color 0.3s;
&:hover {
background-color: rgb(204, 0, 0);
}
`;
export { MainPage, PokemonLogo, MainButton };
//Home.jsx
import { useNavigate } from "react-router-dom";
import { MainButton, MainPage, PokemonLogo } from "../styles/HomeStyles";
const Home = () => {
const nav = useNavigate();
return (
<MainPage>
<PokemonLogo src="../public/pokemon-logo-RN0wntMB.png" />
<MainButton
onClick={() => {
nav("/dex");
}}
>
포켓몬 도감 시작하기
</MainButton>
</MainPage>
);
};
>
export default Home;
이런식으로 로직을 작성해주면 끝이다.
스타일 컴포넌트로 인해 상당한 시간을 벌었다.
className
을 정해주지 않아도 됐고,
매 번 해당 요소를 찾아서 스타일링하지 않아도 됐기 때문이다.
다음으로 넘어가자.
컴포넌트 구조는 다음과 같다.
이미 로드맵에서 컴포넌트 이름도 정해줬기 때문에 구조만 나누면 됐다.
이제 MOCK_DATA
를 활용하여 리스트를 출력해보자.
//PokemonList.jsx
import PokemonCard from "./PokemonCard";
import MOCK_DATA from "../data/MOCK_DATA";
import { PokemonListMain } from "../styles/PokemonListStyles";
const PokemonList = () => {
return (
<PokemonListMain>
{MOCK_DATA.map((pokemon) => (
<PokemonCard
key={pokemon.id}
id={pokemon.id}
korean_name={pokemon.korean_name}
img_url={pokemon.img_url}
/>
))}
</PokemonListMain>
);
};
export default PokemonList;
MOCK_DATA
를 map
을 활용하여PokemonCard
형태로 뿌려준다.props-drilling
형태이기 때문에Dex.jsx
에서 상태를 관리해야한다.Dashborad
에는 초기값으로 몬스터볼 6개가 출력되어야 한다.삭제
로 바뀌어야 한다.일단 대시보드 컴포넌트를 살펴보자.
//Dashboard.jsx
import {
DashboardImg,
DashboardItem,
DashboardList,
DashboardMain,
DashboardTitle,
} from "../styles/DashboardStyles";
import PokemonCard from "./PokemonCard";
const Dashboard = ({ selectedPokemon, handleRemovePokemon }) => {
return (
<DashboardMain>
<DashboardTitle>나만의 포켓몬</DashboardTitle>
<DashboardList>
{selectedPokemon.map((pokemon) => (
<PokemonCard
key={pokemon.id}
id={pokemon.id}
korean_name={pokemon.korean_name}
img_url={pokemon.img_url}
selectedPokemon={selectedPokemon}
handleRemovePokemon={handleRemovePokemon}
onDashboard={true}
/>
))}
{Array.from({ length: 6 - selectedPokemon.length }).map((_, index) => (
<DashboardItem key={`empty-${index}`}>
<DashboardImg src={"../public/pokeball-13iwdk7Y.png"} />
</DashboardItem>
))}
</DashboardList>
</DashboardMain>
);
};
export default Dashboard;
Dashboard
의 로직은 다음과 같다.{Array.from({ length: 6 - selectedPokemon.length }).map((_, index) => ( <DashboardItem key={`empty-${index}`}> <DashboardImg src={"../public/pokeball-13iwdk7Y.png"} /> </DashboardItem> ))}
위 로직은 몬스터볼 개수를 셋팅 해주는 로직이다.
코드를 하나씩 뜯어보자.
Array.from({ length: 6 - selectedPokemon.length }):
는 정해진 길이만큼의 배열을 생성하는 방법이다.
고정된 6개의 슬롯을 유지하기 위해 남은 빈 슬롯의 수만큼 반복 렌더링할 배열을 생성해야하기 때문에 사용했다.
6 - selectedPokemon.length
로 현재 포켓몬 수를 뺀 빈 슬롯의 수를 계산한다.
.map((_, index) => ( ... )):
map
함수에서 (_, index) =>
의 _
는 의미 없는 변수로, 해당 값이 필요하지 않다는 표시다.
props-drilling
형태이기 때문에 Dex.jsx
에 상태관리 로직을 구현해줬다.
//Dex.jsx
import { useState } from "react";
import Dashboard from "../components/Dashboard";
import PokemonList from "../components/PokemonList";
import { DexMain } from "../styles/DexStyles";
import { toast } from "react-toastify";
const Dex = () => {
const [selectedPokemon, setSelectedPokemon] = useState([]);
const handelAddPokemon = (pokemon) => {
if (selectedPokemon.some((p) => p.id === pokemon.id)) {
toast.info("이미 추가된 포켓몬입니다.");
return;
}
if (selectedPokemon.length > 5) {
toast.info("최대 6개까지만 추가할 수 있습니다.");
return;
}
setSelectedPokemon([...selectedPokemon, pokemon]);
};
const handleRemovePokemon = (id) => {
setSelectedPokemon(selectedPokemon.filter((p) => p.id !== id));
};
return (
<DexMain>
<Dashboard
selectedPokemon={selectedPokemon}
handleRemovePokemon={handleRemovePokemon}
/>
<PokemonList
selectedPokemon={selectedPokemon}
handelAddPokemon={handelAddPokemon}
handleRemovePokemon={handleRemovePokemon}
/>
</DexMain>
);
};
export default Dex;
props
로 전달해준다.Tip. toast
는 UI 라이브러리입니다.
npm install react-toastify
// App.js
import React from 'react';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
function App() {
return (
<div>
{/* 다른 컴포넌트들 */}
<ToastContainer />
</div>
);
}
export default App;
import { toast } from 'react-toastify';
function SomeComponent() {
const notify = () => {
toast("이것은 알림입니다!");
};
return (
<button onClick={notify}>알림 띄우기</button>
);
}
//PokemonList.jsx
import PokemonCard from "./PokemonCard";
import MOCK_DATA from "../data/MOCK_DATA";
import { PokemonListMain } from "../styles/PokemonListStyles";
const PokemonList = ({ handelAddPokemon, handleRemovePokemon }) => {
return (
<PokemonListMain>
{MOCK_DATA.map((pokemon) => (
<PokemonCard
key={pokemon.id}
id={pokemon.id}
korean_name={pokemon.korean_name}
img_url={pokemon.img_url}
handelAddPokemon={handelAddPokemon}
handleRemovePokemon={handleRemovePokemon}
onDashboard={false}
/>
))}
</PokemonListMain>
);
};
export default PokemonList;
PokemonList
에서 이벤트 작동함수는 필요가 없지만 PokemonCard
에서 해당 함수가 필요하기 때문에 다시금 props
로 두 가지 함수를 넘겨준다.//PokemonCard.jsx
import { useNavigate } from "react-router-dom";
import {
PokemonCardButton,
PokemonCardImg,
PokemonCardInfoWrap,
PokemonCardItem,
PokemonCardName,
PokemonCardNumber,
} from "../styles/PokemonCardStyles";
const PokemonCard = ({
id,
korean_name,
img_url,
handelAddPokemon,
handleRemovePokemon,
onDashboard,
}) => {
const nav = useNavigate();
const formattedId = id.toString().padStart(3, "0");
const handleButtonAdd = (e) => {
e.stopPropagation();
if (onDashboard) {
handleRemovePokemon(id);
} else {
handelAddPokemon({ id, korean_name, img_url });
}
};
return (
<PokemonCardItem
onClick={() => {
nav(`/pokemon-detail/${id}`);
}}
>
<PokemonCardImg src={img_url} />
<PokemonCardInfoWrap>
<PokemonCardName>{korean_name}</PokemonCardName>
<PokemonCardNumber>No. {formattedId}</PokemonCardNumber>
</PokemonCardInfoWrap>
<PokemonCardButton onClick={handleButtonAdd}>
{onDashboard ? "삭제" : "추가"}
</PokemonCardButton>
</PokemonCardItem>
);
};
export default PokemonCard;
props
로 넘겨받은 함수를 바탕으로 로직을 작성해준다.
코드를 살펴보자.
e.stopPropagation()
: PokemonCardItem
에 onClick
속성이 줬기 때문에 e.stopPropagation()
을 추가하여 이벤트 전파를 막아줬다.
onDashboard
: 대시보드에 올라가 있는지 여부를 판단한다.
초기값은 false
로 이 조건에 따라 버튼이 추가 또는 삭제로 나타난다.
참고 : Dashboard
에서 카드가 렌더링될때 true
(대시보드 로직참고)
const formattedId = id.toString().padStart(3, "0")
: 숫자를 001
, 091
, 151
이런식으로 표현해준다.
우선 Router.jsx
의 로직을 살펴보겠다.
//Router.jsx <Route path="/pokemon-detail/:id" element={<PokemonDetail />} />
//PokemonCard.jsx <PokemonCardItem onClick={() => { nav(`/pokemon-detail/${id}`); }} >
Router.jsx
에서 <Route path="/pokemon-detail/:id" element={<PokemonDetail />} />
로 라우터 설정을 해줬기 때문에,
PokemonCard.jsx
에서 useNavigate
를 활용한 이동이 가능한것이다.
useNavigate
는 단순한 경로 이동만을 담당한다.
라우터 설정이 없으면 해당 컴포넌트를 찾질 못한다.
꼭 기억해둘것.
//PokemonDetail.jsx
import { useParams, useNavigate } from "react-router-dom";
import MOCK_DATA from "../data/MOCK_DATA";
import {
DetailButton,
DetailImg,
DetailInfo,
DetailMain,
DetailTitle,
} from "../styles/PokemonDetailStyles";
const PokemonDetail = () => {
const { id } = useParams();
const nav = useNavigate();
const selectedPokemon = MOCK_DATA.find(
(pokemon) => pokemon.id === Number(id)
);
return (
<DetailMain>
<DetailImg src={selectedPokemon.img_url} />
<DetailTitle>{selectedPokemon.korean_name}</DetailTitle>
<DetailInfo>타입 : {selectedPokemon.types.join(", ")}</DetailInfo>
<DetailInfo>{selectedPokemon.description}</DetailInfo>
<DetailButton
onClick={() => {
nav(-1);
}}
>
뒤로 가기
</DetailButton>
</DetailMain>
);
};
export default PokemonDetail;
const { id } = useParams()
에 대해 잠시 살펴보자.useParams
는 React Router
에서 제공하는 훅으로, 현재 URL에 포함된 매개변수 값을 객체 형태로 반환한다.
useParams
는 주로 Route
컴포넌트에 동적 경로 (예: /pokemon/:id)가 포함된 경우, 그 경로에서 특정 값을 추출하는 데 사용된다.
:id
: :
를 사용하면 :
뒤는 동적으로 변하는 값이라는것을 나타낸다.
:
로 설정하지 않으면 추출불가
따라서, const { id } = useParams()
로직은 다음과 같다.
URL
주소(pokemon-detail/:id
)에 동적으로 오게되는 id
값을 추출한다.추가적인 Tip.
1. 여러 파라미터가 있을 때
만약 라우트 경로가 /pokemon-detail/:id/:name
와 같이 여러 개의 동적 파라미터가 포함된 경우라면,
const params = useParams();
// 예를 들어 URL이 http://localhost:5173/pokemon-detail/2/pikachu일 경우
params = { id: "2", name: "pikachu" };
2. 구조 분해 할당을 하지 않는경우
const params = useParams();
params
에는 useParams
가 반환하는 객체 전체가 들어가게 된다.// 구조분해할당 하지 않은경우
const params = useParams();
const id = params.id;
const id = useParams().id;
params = { id: "2" };
console.log(params.id); // "2"
console.log(params.name); // "pikachu"
// 구조분해할당
const { id, name } = useParams();
console.log(id); // "2"
console.log(name); // "pikachu"
기껏 추가했는데, 클릭을 한 번 잘못하면 대쉬보드가 초기화 된다.
로컬스토리지로 해결해볼까 했지만 관뒀다.
이것은 차후에 해결해보도록 하자.
솔직히 숙련주차 강의를 보면서 엄청 쫄아있었다.
내가 이거를 활용해서 과제를 할 수 있을까??
이런 걱정이 더 많이 들었었기 때문이다.
그런데 막상 과제에 부딪혀보니 몰랐던 개념들에 대해 더 많이 이해 할 수 있었고, 특히나 리액트 훅의 적절한 사용법을 아는데 도움이 많이 됐다.
과제 내주신 튜터님 정말 감사합니다.
이제 ContextAPI로 리팩토링하는것과 선택구현사항이 남았다.
끝까지 잘해내봅시다! 화이팅!