- 포켓몬 도감 만들기 -(1)2025년 02월 05일
- redpome
- 작성자
- 2025.02.05.:02
이번 과제는 포켓몬 도감 만들기다. Props drilling 방식부터 시작하여 Context를 통한 상태관리부터 Redux ToolKit 까지의 리팩토링을 목표로하고있다. CSS 다루기도 그렇고 리팩토링에 부족한 부분까지 채울려면 할 게 너무 많다.
오늘 개인 면담에서도 코딩 경험이 많이 부족하다는 말을 듣다보니 심각한것 같다. 실시간으로 수업을 듣다보면 말로 설명하는 것이 머릿 속에 들어오지 않는걸 보면 태생적인것도 있지만 아직 많이 부족한 학습시간도 한 몫 한 것 같다. 2월달에도 왜 이렇게 밖에 나갈 일이 많은건지...
원래대로라면 Context를 통한 상태관리까지 하는 것이 맞지만 일단 여기까지 한 것을 기록으로 남겨야겠다.
일단 포켓몬 도감을 위해 만든 프로젝트의 구조는 다음과 같다.
src
├─assets
├─components
├─pages
├─GlobalStyle.jsx => 전역 스타일을 관리하기 위해 만든 파일
└─shared주로 쓰는 구조를 위주로 보이도록하겠다. assets에는 MOCK_DATA라고 해서 포켓몬의 정보를 담고 있는 파일이 있다.
│ ├── components/
│ │ ├── Dashboard.jsx => 포켓몬 선택 시 추가할 컴포넌트
│ │ ├── PokemonCard.jsx => 포켓몬 정보가 담긴 개별 카드의 구조를 다루는 컴포넌트
│ │ ├── PokemonDetail.jsx => 쿼리스트링을 이용하여 카드 클릭 시 세부 정보를 보여주는 컴포넌트
│ │ ├── PokemonList.jsx =>포켓몬 카드를 렌더링할 카드리스트 컴포넌트
│ ├── pages/
│ │ ├── Home.jsx => 최초 페이지 접속 시의 화면
│ │ ├── Detail.jsx => 세부사항을 보여주는 컴포넌트가 렌더링되는 페이지
│ │ ├── Dex.jsx => 대시보드, 카드 컴포넌트들을 보여주는 페이지
│ ├── App.js
│ ├── shared/
│ │ ├── Router.jsx => 라우터 설정을 위한 파일
대략적으로 중요한 파일들의 구조는 위와 같다.
컴포넌트간 동작의 흐름
일단 프로젝트는 3개의 branch로 나뉘는데 prop-drilling->context->redux 이렇게 상태관리에 따른 차이를 구별하기 위해 브랜치를 생성하고 프로젝트를 만들었다.
처음은 상위 컴포넌트에서 하위 컴포넌트로 Props를 전달하는 방식의 Prop-drilling 방식을 통해 구현하였다.지난 리액트 과제에서는 리스트 컴포넌트와 리스트 컴포넌트에서 렌더링 할 컴포넌트을 명확히 분리시키는데에 실패했었다.
const PokemonCard = ({ pokemonData, addPokemonCard }) => { const navigate = useNavigate(); const showPokemonDetail = () => { navigate(`/detail?id=${pokemonData.id}`); }; return ( <div> <PokemonCardFrame onClick={() => showPokemonDetail(pokemonData.id)}> <div> <img src={pokemonData.img_url}></img> </div> <div>{pokemonData.korean_name}</div> <div>{pokemonData.types}</div> <div>No. {pokemonData.id}</div> <button onClick={() => addPokemonCard(pokemonData.id)}>추가</button> </PokemonCardFrame> </div> ); }; export default PokemonCard;
포켓몬 카드를 만드는 컴포넌트에서는 상위 컴포넌트로부터 데이터를 내려받고 개별적인 카드의 구조를 만들기위한 컴포넌트로서 사용한다.
const PokemonList = ({ addPokemonCard, pokemonData }) => { return ( <PokemonListContainer> {" "} {pokemonData.map((pokemon) => ( <PokemonCard key={pokemon.id} pokemonData={pokemon} addPokemonCard={addPokemonCard} /> ))} </PokemonListContainer> ); }; export default PokemonList;
리스트 컴포넌트는 하위 컴포넌트인 카드 컴포넌트에 props를 넘겨주고 하위 컴포넌트를 감싸 렌더링할 수 있도록한다.
const Dex = () => { const [pokemonInBall, setPokemonInBall] = useState([]); console.log(pokemonInBall); const addPokemonCard = (id) => { const selectedCard = POKEMON_DATA.find((card) => card.id === id); // console.log(selectedCard); if (selectedCard) { if (pokemonInBall.length >= 6) { alert("최대 6마리까지 데리고 다닐 수 있습니다"); return; } setPokemonInBall((selectedPokemon) => [...selectedPokemon, selectedCard]); } }; const deletePokemonCard = (id) => { setPokemonInBall((prevPokemon) => prevPokemon.filter((card) => card.id !== id) ); }; return ( <Container> <DashBoard pokemonInBall={pokemonInBall} deletePokemonCard={deletePokemonCard} /> <PokemonList pokemonData={POKEMON_DATA} addPokemonCard={addPokemonCard} deletePokemonCard={deletePokemonCard} ></PokemonList> </Container> ); };
prop-drilling 방식을 위해 일단 dex 페이지에서의 부모 컴포넌트인 Dex 컴포넌트에서 함수나 데이터를 넘겨주도록하였다.
Dex->PokemonList->PokemonCard 컴포넌트로 props가 전달이 될 것이다.
대시보드에 카드를 추가하기 위해 버튼 이벤트를 위한 이벤트 흐름을 정리하면 다음과 같다.- PokemonCard 컴포넌트에서 생성한 버튼을 클릭, 이벤트 함수는 Dex에서 선언하고 props를 통해 전달하였다.
- PokemonCard의 상위 컴포넌트인 PokemonList 컴포넌트는 Dex로부터 전달받은 추가 함수를 PokemonCard에게 전달
- Dex 컴포넌트에서는 대시보드에 추가할 포켓몬들의 상태를 위해 useState를 선언하고 추가 함수는 해당 상태를 통해 관리한다.
- Dex 컴포넌트는 Dashboard 컴포넌트에 PokemonInBall이라는 상태를 넘겨준다.
- Dashboard 컴포넌트는 Dex 컴포넌트로부터 받은 상태를 렌더링한다.
return ( <Container> <DashBoard pokemonInBall={pokemonInBall} deletePokemonCard={deletePokemonCard} /> <PokemonList pokemonData={POKEMON_DATA} addPokemonCard={addPokemonCard} deletePokemonCard={deletePokemonCard} ></PokemonList> </Container>
Dex 페이지에서는 위와 같은 컴포넌트들의 배치와 props를 넘겨주는 것을 확인할 수 있다.
문제가 됐던 점
예시로 들었던 프로젝트를 보면 카드를 선택하지 않은 상태에서는 빈 포켓몬 볼이 대시보드에 존재한다. 그리고 추가하면 동적으로 빈 포켓몬 볼 대신 포켓몬 카드가 입력이 되었다.
이런 식으로 말이다. 여기서 난 아예 이런 기능을 구현할 수가 없었다. 처음에는 PokeBall 이라는 컴포넌트를 생성하고 삼항 연산자를 통해 Dashboard로 넘겨준 PokemonInBall의 상태에 따라 컴포넌트를 렌더링 할려고했으나 동적인 렌더링을 주는 것에 실패했다.
결국 이 부분은 튜터님과 AI를 통해 해결하고 내가 쓴 코드는 없었다.
<SelectedPokemonBoard> <h1>좋아하는 포켓몬은?</h1> <PokeBallContainer> {Array.from({ length: maxPokemon }, (_, index) => { if (index < pokemonInBall.length) { const bag = pokemonInBall[index]; return ( <PokemonCard key={bag.id}> <img src={bag.img_url} alt={bag.korean_name} style={{ width: "100%" }} /> <div>{bag.korean_name}</div> <div>{bag.types.join(", ")}</div> <div>No. {bag.id}</div> <button onClick={() => deletePokemonCard(bag.id)}> 삭제 </button> </PokemonCard> ); } else { return ( <PokeBall key={`empty-${index}`}>포켓몬을 고르세요</PokeBall> ); } })} </PokeBallContainer> </SelectedPokemonBoard>
Array객체 생성을하고 from 메서드를 사용했다. Array.from은 유사 배열 객체를 만들때 주로 사용한다고한다.
그러니까 현재 내 코드에서는 PokemonInBall에 있는 데이터를 가지고 대시보드에 카드를 렌더링해야하니 maxPokemon=6만큼의 가상의 배열을 만든 것이다.(_,index)는 mapFn에 해당하는 함수이며 배열의 모든 요소에 대해 호출한다. _은 값을 사용하지 않는다는 의미이고 원래 해당 자리는 배열에서 현재 처리 중인 요소이다, 나는 현재 요소의 인덱스가 필요로한데 PokemonInBall에는 카드가 배열에 담기기 때문이다.
만약 카드를 1개 추가했다고하자 PokemonInBall.length=1이고 index=0이다. 그렇다면 if문에 선언된 코드가 실행된다. index=1에서는 다시 else문이 실행된다. 이를 통해 동적으로 카드를 렌더링 할 수 있는 것이다.
'내일배움캠프' 카테고리의 다른 글
포켓몬 도감 트러블 슈팅 - 카드 중복 에러 (0) 2025.02.11 TIL - UseContext 사용기, 트러블 슈팅 (1) 2025.02.06 TIL - 트러블 슈팅 (0) 2025.01.23 실행 컨텍스트 (3) - 콜 스택, 마이크로 태스크 (0) 2025.01.17 TIL - Scope, this 바인딩, 클래스 (2) 2025.01.15 다음글이전글이전 글이 없습니다.댓글