개인 프로젝트 : Pokemon Dex - (1) prop-drilling으로 구현하기

verdantgreeny·2025년 2월 4일

본캠프

목록 보기
34/56

1. 프로젝트 셋업 및 기본 컴포넌트 구성

1-1. 프로젝트 구조

1-2. 페이지 구성 및 컴포넌트 구성

Home.jsxDex.jsxDetail.jsx
컴포넌트 구성

2. 포켓몬 리스트와 선택, 삭제, 알림 기능 구현

2-1. Dex.jsx(후에 Router.jsx로 옮김)

  // 처음에는 Dex.jsx 파일이었다가 상세페이지 구현을 위해서 Router.jsx로 옮겨줬다.

  // ✅ 포켓몬 추가 기능 
  const onAddHandler = (pokemon) => {
    const addedPokemon = selectedPokemon.find((p) => {
      return p.id === pokemon.id;
    });

    if (addedPokemon) {
      alert("이미 추가된 포켓몬 입니다.");
    } else if (selectedPokemon.length >= 6) {
      alert("6개 이상의 포켓몬을 담을 수 없습니다.");
    } else {
      setSeletedPokemon([...selectedPokemon, pokemon]);
    }
  };

   // ✅ 포켓몬 삭제 기능 
  const onDeleteHandler = (id) => {
    const newPokemonList = selectedPokemon.filter((p) => p.id !== id);
    return setSeletedPokemon([...newPokemonList]);
  };

   // ✅ 포켓몬 리스트에서도 추가된 포켓몬일 경우 버튼을 바꾸기 위해 새로만든 mock-data 배열 
  const newMockList = MOCK_DATA.map((pokemon) => {
    const selectedId = selectedPokemon.map((p) => p.id);
    if (selectedId.includes(pokemon.id)) {
      return {
        ...pokemon,
        isSelected: true,
      };
    } else {
      return pokemon;
    }
  });

2-2. PokemonList.jsx


 //PokemonList.jsx

    <StPokemonList>
      {newMockList.map((pokemon) => (
        <li key={pokemon.id}>
          <PokemonCard
            pokemon={pokemon}
            onAddHandler={onAddHandler}
            isSelected={false}
          />
        </li>
      ))}
    </StPokemonList>

2-3. Dashborad.jsx

트러블 슈팅

문제 발생

나만의 포켓몬은 6개까지 저장이 가능하고 만약 포켓몬이 6개 모두 채워지지 않았을 경우에 나머지는 포켓볼 이미지로 대체하여야 했다. 그러나 나는 map 메서드를 이용해 선택된 포켓몬을 단순하게 추가하는 방식으로 구현을 하고 있었다.

원인 추론

6개의 빈 배열을 만들어야 겠다는 생각에 처음에는 단순하게 const arr = [0,0,0,0,0,0]을 만들어서 map 메서드로 돌릴 생각을 했으나 만약 포켓몬을 7개 혹은 8개 등 보유 갯수가 계속 바뀔 경우 좋지 않은 방법이라 다른 방법을 찾는 것이 좋을 것이라는 생각이 들었다. 쉽게 방식이 떠오르지 않았고 Array().fill().map() 체이닝을 이용해야 한다는 힌트를 얻었다.

해결 방안 : Array().fill().map() 체이닝

  1. Array(6)는 6개 짜리 빈 배열을 만들어 준다. 그리고 여기에 체이닝으로 Array(6).fill()을 하면 6개 짜리 배열안에 undefined가 채워진다.
Array(6).fill()
  1. Array(6).fill()에 map 메서드 체이닝
Array().fill().map() 체이닝을 이용한 예시

결과

//Dashboard.jsx

    <StDashboard>
      <h2> 나만의 포켓몬 </h2>
      <StDashbordUl>
        {Array(6)
          .fill()
          .map((_, i) => (
            <li key={i}>
              {selectedPokemon[i] ? (
                <PokemonCard
                  pokemon={selectedPokemon[i]}
                  onDeleteHandler={onDeleteHandler}
                  isSelected={true}
                />
              ) : (
                <StImg src={defaultImg}></StImg>
              )}
            </li>
          ))}
      </StDashbordUl>
    </StDashboard>

2-4. PokemonCard.jsx

    <StPokemonCard>
      <StPokemonName>
        {" "}
        <span> {String(pokemon.id).padStart(3, "0")} </span>
        {pokemon.korean_name}
      </StPokemonName>
      <Link to={`/detail?id=${pokemon.id}`}>
        <CardImg src={pokemon.img_url} alt={pokemon.korean_name} />
      </Link>
      {!isSelected ? (
        <Button
          color={pokemon.isSelected && "red"}
          type="button"
          onClick={() => onAddHandler(pokemon)}
        >
          {!pokemon.isSelected ? "추가" : "추가됨"}
        </Button>
      ) : (
        <Button
          color="red"
          type="button"
          onClick={() => onDeleteHandler(pokemon.id)}
        >
          {" "}
          삭제{" "}
        </Button>
      )}
    </StPokemonCard>

3. 디테일 페이지 구현

No.001일 경우 이전 페이지로 이동을 마지막 포켓몬인 뮤가 뜨게 구현No.151일 경우 다음 페이지로 이동을 첫 번째 포켓몬인 이상해씨가 뜨게 구현
  const Detail = ({ onAddHandler, newMockList }) => {
  const navigate = useNavigate();
  const [query] = useSearchParams();
  const detailPokemonId = +query.get("id");
  const pokemon = newMockList.find((p) => p.id === detailPokemonId);

  // ✅ 이전번호 및 다음번호 포켓몬 디테일페이지 이동
  const prePokemon = newMockList.find((p) =>
    pokemon.id === 1
      ? p.id === newMockList.length
      : p.id === detailPokemonId - 1
  );
  const nextPokemon = newMockList.find((p) =>
    pokemon.id === newMockList.length
      ? p.id === 1
      : p.id === detailPokemonId + 1
  );

  const prevDetail =
    detailPokemonId === 1
      ? `/detail?id=${newMockList.length}`
      : `/detail?id=${detailPokemonId - 1}`;
  const nextDetail =
    detailPokemonId === newMockList.length
      ? `/detail?id= 1`
      : `/detail?id=${detailPokemonId + 1}`;

  return (
    <>
      <DetailLinkSection>
        <Link to={prevDetail}>
          {" "}
          ◀︎ No.{String(prePokemon.id).padStart(3, "0")}{" "}
          {prePokemon.korean_name}{" "}
        </Link>
        <Link to={nextDetail}>
          {" "}
          No.{String(nextPokemon.id).padStart(3, "0")} {nextPokemon.korean_name}
          ▶︎{" "}
        </Link>
      </DetailLinkSection>

      <DetailSection>
        <img src={pokemon.img_url} alt={pokemon.korean_name} />
        <div>
          <div className="pokemon-number">
            No.{String(pokemon.id).padStart(3, "0")}
          </div>
          <div className="pokemon-name">{pokemon.korean_name}</div>
          <div className="pokemon-description"> {pokemon.description}</div>
          <div className="pokemon-type"> 타입 : {String(pokemon.types)} </div>
          <div className="pokemon-btn-div">
            <Button type="button" onClick={() => navigate("/dex")}>
              돌아가기
            </Button>
            <Button
              color={pokemon.isSelected && "yellow"}
              type="button"
              onClick={() => onAddHandler(pokemon)}
            >
              {!pokemon.isSelected ? "추가" : "보유"}
            </Button>
          </div>
        </div>
      </DetailSection>

      {/* 선택된 포켓몬 미리보기 */}
      <SelectedPokemonSection>
        {newMockList.map((pokemon) => {
          if (pokemon.isSelected) {
            return (
              <div key={pokemon.id} className="selected-pokemon">
                <img src={pokemon.img_url} />
                <div className="pokemon-name"> {pokemon.korean_name} </div>
              </div>
            );
          }
        })}
      </SelectedPokemonSection>
    </>
  );
};
(+나름 반응형으로 되게끔 구현했다.)

0개의 댓글