⚬ 개인과제 - 포켓몬도감 만들기
➊ 기본 UI 구성하기
➋ 상태관리와 포켓몬 추가, 삭제기능 구현하기
➌ 조건 불일치 시 알림기능 구현하기
➍ 포켓몬 디테일 페이지 구현하기
⚬ 이벤트 버블링 문제 발생 및 해결
⚬ 엄격한 비교 "===" 사용 중 놓쳤던 부분
1. 기본 UI 구성하기
⚬ react-router-dom 설치 후, App.jsx에서 페이지 라우팅을 설정
⚬ Home.jsx에서는 "포켓몬 도감 시작하기" 버튼을 만들어, useNavigate를 사용해 버튼 클릭시 /dex로 이동하도록 작성
// App.jsx
const App = () => {
return (
<>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dex" element={<Dex />} />
</Routes>
</BrowserRouter>
</>
);
};
// Home.jsx
const Home = () => {
const navigate = useNavigate();
return (
<StyleCt>
<StyleImg src="https://react-6-pokemon.vercel.app/assets/pokemon-logo-RN0wntMB.png" />
<StyleButton onClick={() => navigate("/dex")}>
포켓몬 도감 시작하기
</StyleButton>
</StyleCt>
);
};
⚬ 포켓몬이 들어있는 mock 데이터를 Dex 페이지의 PokemonList 컴포넌트에 props로 전달
// Dex.jsx
const Dex = () => {
return (
<>
<Dashboard />
<PokemonList pokemonList={MOCK_DATA} />
</>
);
};
⚬ PokemonList 컴포넌트는 map 함수로 PokemonCard 를 생성하고 데이터들을 전달
// PokemonList.jsx
const PokemonList = ({ pokemonList }) => {
return (
<StyledListCt>
{pokemonList.map((pokemon) => {
return (
<PokemonCard
key={pokemon.id}
pokemon={pokemon}
onAdd={() => {}}
isSelected="false"
/>
);
})}
</StyledListCt>
);
};
⚬ PokemonCard 컴포넌트에서 props로 받은 데이터를 이용해 각 카드를 생성
// PokemonCard.jsx
const PokemonCard = ({ pokemon, onAdd, isSelected }) => {
const { id, img_url, korean_name } = pokemon;
return (
<StyledCard>
<img src={img_url} alt={`${korean_name} 이미지`} />
<p>{korean_name}</p>
<p>No. {id} </p>
{isSelected ? (
<StyledButton>삭제</StyledButton>
) : (
<StyledButton>추가</StyledButton>
)}
</StyledCard>
);
};
⚬ styled-components 를 이용해 각 컴포넌트 별 기본 스타일링 적용
2~3. 상태 관리와 포켓몬 추가, 삭제 기능 & 조건 불일치 시 알림기능 구현하기
const Dex = () => {
const [selectedPokemon, setSelectedPokemon] = useState([]);
// ===포켓몬 추가===
const addPokemon = (pokemon) => {
const addedPokemon = [
...selectedPokemon,
{
id: pokemon.id,
img: pokemon.img_url,
name: pokemon.korean_name,
number: `No. ${pokemon.id}`,
isSelected: true,
},
];
// 추가한 포켓몬이 선택된 포켓몬 리스트 존재하는지 찾기
const samePokemon = selectedPokemon.find(
(addedPk) => addedPk.id === pokemon.id
);
if (selectedPokemon.length >= 6) {
alert("더이상 포켓몬을 추가할 수 없습니다!");
} else if (samePokemon) {
alert("이미 선택된 포켓몬 입니다!");
} else {
setSelectedPokemon(addedPokemon);
}
};
// ===포켓몬 삭제===
const removePokemon = (pokemon) => {
const removedPokemon = selectedPokemon.filter((select) =>
select.id === pokemon.id ? false : true
);
setSelectedPokemon(removedPokemon);
};
return (
<>
<Dashboard
selectedPokemon={selectedPokemon}
onRemovePokemon={removePokemon}
/>
<PokemonList pokemonList={MOCK_DATA} onAddPokemon={addPokemon} />
</>
);
};
export default Dex;
4. 포켓몬 디테일 페이지 구현하기
⚬ react router dom의 useSearchParams 훅을 사용하여 상세페이지 주소 쿼리스트링의 특정 포켓몬 id 값을 get으로 가져왔다.
가져온 id 값을 사용해 포켓몬 데이터 객체에서 동일한 id 값의 포켓몬을 뽑아내서 해당 포켓몬의 상세정보를 출력하도록 구현했다.
// App.jsx (디테일 페이지 루트 추가)
<Route path="/pokemon-detail" element={<PokemonDetail />} />
// PokemonCard.jsx (포켓몬 카드 클릭 시 디테일 페이지로 이동)
<StCard onClick={() => navigate(`/pokemon-detail?id=${id}`)}>
// PokemonDetail.jsx
const PokemonDetail = () => {
const navigate = useNavigate();
const [params, setParams] = useSearchParams();
const pokemonId = params.get("id");
const pokemon = MOCK_DATA.find((p) => p.id === +pokemonId);
return (
<StDetailContainer>
<StImg src={pokemon.img_url} alt={pokemon.korean_name} />
<StH2>{pokemon.korean_name}</StH2>
<p>타입: {pokemon.types.join(", ")}</p>
<p>{pokemon.description}</p>
<StButton onClick={() => navigate("/dex")}>뒤로 가기</StButton>
</StDetailContainer>
);
};
■ 이벤트 버블링 문제 발생
포켓몬 카드클릭 시 각 포켓몬의 상세페이지로 이동하기위해서 카드에 onClick={() => navigate("/pokemon-detail")} 을 주었다.
카드를 눌렀을 때 detail 페이지로 이동은 하는데 추가버튼까지도 기존에 추가한 onAdd 함수가 실행되지 않고 detail 페이지로 이동하는 문제가 생겼다.
⚬ 발생한 이유
카드 내에 겹쳐진 영역(버튼)을 클릭하면 부모요소인 전체 영역의 이벤트가 발생되는 이벤트 버블링이 일어나고 있었다.
⚬ 해결한 방법
Event.stopPropagation() 메서드를 사용하여 부모요소 카드에 추가한 이벤트를 막고 버튼에 추가한 함수를 실행하도록 하였다.
* Event.stopPropagation() - 현재 이벤트가 캡처링/버블링 단계에서 더 이상 전파되지 않도록 방지한다.
<StButton onClick={(e) => {
e.stopPropagation();
onAdd();
}}>추가</StButton>
■ 엄격한 비교 "===" 사용 중 놓쳤던 부분
포켓몬 상세페이지 주소의 쿼리스트링 id 값과 동일한 포켓몬 정보를 불러오기 위해 useSearchParams, get 으로 쿼리스트링 id 값을 가져왔다. 그리고 포켓몬 데이터가 들어있는 MOCK.DATA 에서 find 를 활용해 주소의 id 값과 포켓몬 정보를 가져오려 했으나 계속 뜨지않았고 콘솔을 확인해보니 undefined 이 나왔다.
const [params, setParams] = useSearchParams();
const pokemonId = params.get("id");
const pokemon = MOCK_DATA.find((p) => p.id === pokemonId);
⚬ 발생한 이유
쿼리스트링에서 받은 1은 보기에 숫자여도 타입이 string 이고 데이터에서 뽑아낸 1 은 타입이 number 였던 것 !!
거기다가 엄격한 비교(===) 로 타입이 다른 두가지를 확인하고 있었으니 undefined 가 출력된 것 이었다.
* 엄격한 비교 : 값과 값의 종류가 모두 같은지를 비교
⚬ 해결한 방법
비교하는 대상의 타입을 동일하게 해주면 되므로 string 타입인 pokemonId 의 앞에 + 를 붙여서 숫자 타입으로 변환해주었다.
const pokemon = MOCK_DATA.find((p) => p.id === +pokemonId);
엄격한 비교(===) 를 느슨한(==) 비교로 바꾸어주어도 동작하지만, 느슨한 비교를 사용할 경우 의도치 않는 결과를 마주하게 될 가능성이 있으므로, 특별한 이유가 없다면 엄격한 동등을 사용하도록 하자!
🐣 느낀점
어제까지 숙련주차 강의 복습을 마무리 하고 오늘부터 개인과제를 시작했다. 과제 진행 순서 가이드를 따라 차근차근 만들었는데 지난 번 올림픽 메달 트래커 과제에서 했던 부분들이 정말 많이 도움이 되었다. 그때는 기능을 하나 추가하는 것도 강의를 보면서 겨우 따라하곤 했었는데 이번엔 그때보다는 착착 진행한 것 같다.
오늘 겪었던 문제 중에 이벤트 버블링에서 내가 의도한 함수가 실행이 안됐었는데 구글링으로 방법을 찾아서 이리저리 적용해보니 해결이 되었다. 이 과정을 통해서 새로운 이벤트 메서드를 알게되었다.
그리고 쿼리스트링의 값을 가져와 사용하는 상세페이지 부분에서 각각의 id 를 찍어보면 둘다 1이 나오는데, 왜 비교해서 찾은 값은 아무리 콘솔에 찍어봐도 undefined 가 나오는지 이해를 못했다. 그리고 결국 튜터님께 찾아갔고 각 id값의 타입이 다르다는 걸 그제야 알아차렸다. 이미 알고있고, 잘 살펴보면 쉽게 해결할 수 있었던 문제였어서 아차싶었는데 다음부터는 이런 타입 부분까지 놓치지 않도록 신경쓸 수 있는 계기가 된 것 같다.
오늘은 prop drilling 으로 페이지를 구현했는데, 이제부터는 Context API를 사용해서 상태 관리를 리팩터링해야한다. 지금 조금 만져봤는데 컴포넌트들이 날라가고 오류가 뜬다. 이번에도 차근차근 잘 해보자. 익숙해지도록 여러 시도도 해보고 많이 써보기.
'TIL' 카테고리의 다른 글
개인과제 - 포켓몬도감 만들기 ③ (0) | 2024.08.26 |
---|---|
개인과제 - 포켓몬도감 만들기 ② (0) | 2024.08.23 |
react-router-dom (0) | 2024.08.21 |
RTK, RRD, Supabase (0) | 2024.08.20 |
타임어택 과제 보완 (0) | 2024.08.19 |