inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

[인프런 워밍업 스터디 클럽 2기 FE] 3주차 과제 - 포켓몬 도감 앱 만들기

김예지
0

image

포켓몬 도감 앱

github : 10-pokeapi

 

기능목록

 

구현하기

띠부띠부씰 느낌으로 css를 넣어봤다.

폴더구조

📁src
├── App.js
├── App.css
├── 📁components
│   ├── Modal.js
│   ├── Nav.js
│   ├── PokemonList.js
│   └── SearchInput.js
├── 📁hooks
│   ├── useFetchPokemonData.js
│   └── useFetchPokemonDetail.js
├── 📁pages
│   ├── Detail.js
│   └── Main.js

메인에서 추가 데이터 요청하기

image

useFetchPokemonData.js : 페이지 번호에 따라 Pokémon 데이터를 가져오는 역할

export const useFetchPokemonData = (pageNumber) => {
  const [pokemonData, setPokemonData] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchPokemonData = async () => {
      setIsLoading(true);
      try {
        const promises = [];
        // 한 페이지당 20개의 포켓몬 데이터 가져오기
        const start = (pageNumber - 1) * 20 + 1; // 예: 1 페이지면 1~20, 2페이지면 21~40
        const end = pageNumber * 20;

        for (let i = start; i <= end; i++) {
          promises.push(axios.get(requests.fetchPokemonById(i)));
        }
        const responses = await Promise.all(promises);
        const newData = responses.map((response) => response.data);
        setPokemonData((prevData) => [...prevData, ...newData]); // 기존 데이터에 추가
      } catch (error) {
        console.error('포켓몬 데이터를 가져오는 중 오류 발생:', error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchPokemonData();
  }, [pageNumber]);

  return { pokemonData, isLoading };
};

상태관리 :

pokemonData: 가져온 Pokémon 데이터 배열을 저장

isLoading: 데이터 로딩 중 여부를 보여준다. (true 또는 false)

 

데이터 가져오기 :

useEffect 훅을 사용하여 pageNumber가 변경될 때마다 데이터를 가져온다.

fetchPokemonData라는 비동기 함수를 정의하고, 여기서 Pokémon 데이터를 API를 통해 가져옴

1. const { pokemonData, isLoading } = useFetchPokemonData(pageNumber); 훅에 pageNumber = 1 전달

2. fetchPokemonData 함수 내에서

const start = (pageNumber - 1) * 20 + 1; // 예: 1 페이지면 1~20, 2페이지면 21~40
const end = pageNumber * 20;

start 계산 : pageNumber 가 1일때 :첫 번째 페이지의 첫 번째 Pokémon ID가 1임을 나타낸다.

end 계산 : 첫 번째 페이지의 마지막 Pokémon ID가 20임을 나타낸다.

3. 데이터 요청

for (let i = start; i <= end; i++) {
   promises.push(axios.get(requests.fetchPokemonById(i)));
}

baseUrl(axios) + fetchPokemonById (requests) → https://pokeapi.co/api/v2//pokemon/${id}

for 루프를 통해 Pokémon 데이터를 가져오기 위해 API에 요청하고 const promises = []; 배열에 데이터 추가함

4. 데이터 수신 및 상태 업데이트

const responses = await Promise.all(promises);
const newData = responses.map((response) => response.data);
setPokemonData((prevData) => [...prevData, ...newData]); 

responses 모든 Promise가 이행되면, 이행된 결과를 포함하는 배열을 반환, responses는 20개의 응답 객체를 포함

newData responses 배열에서 각 응답의 실제 데이터 부분을 추출

setPokemonData() 기존 데이터에 새 데이터 추가

 

PokemonList.js

더보기 데이터 요청

const handleLoadMore = () => {
    setPageNumber((prevPageNumber) => prevPageNumber + 1);
  };

사용자가 '더보기' 버튼을 클릭하면 handleLoadMore 함수가 실행 이 함수에서는setPageNumber를 호출

fetchPokemonData 함수 내에서 start와 end 값을 다시 계산

start = (2 - 1) * 20 + 1 = 21

end = 2 * 20 = 40

{!isLoading ? (
   <button
      className='my-9 text-lg bg-gray-200 text-gray-600 px-9 py-2 rounded'
      type='button'
      onClick={handleLoadMore}
   >
      더보기
   </button>
) : (
   <div className='my-9 text-lg'>Loading...</div>
)}

처음에는 const [isLoading, setIsLoading] = useState(false); 표시 → setIsLoading(true); 상태 업데이트

isLoading이 true일 경우 'Loading...' 메시지를 표시하고, false일 경우 '더보기' 버튼을 보여준다.

 

포켓몬 타입별 색상코드 적용.

.bg-normal {
  background-color: #949495;
}

api데이터로 타입을 받아서 bg-${type} 형식으로 class지정.

css에서 타입별 색상 코드를 적용해 놓는다.

 

포켓몬 넘버 3자리 수로 변경 1 → 001

{String(pokemon.id).padStart(3, '0')}

 

포켓몬 검색 기능

image검색기능

export default function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const navigate = useNavigate();

  const handleSearch = () => {
    if (searchTerm.trim()) {
      navigate(`/${searchTerm.toLowerCase()}`);
    }
  };

  return (
    <div className='max-w-3xl py-7 w-full flex justify-center items-center mx-auto'>
      <div className='flex w-full max-w-[600px] rounded-xl border-2 border-gray-200 overflow-hidden'>
        <input
          type='text'
          className='px-5 py-4 w-full focus:outline-none'
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder='포켓몬 이름을 입력하세요'
        />
        <button
          className='whitespace-nowrap px-9 bg-gray-200'
          onClick={handleSearch}
        >
          검색
        </button>
      </div>
    </div>
  );
}

포켓몬 이름을 입력하고 검색 버튼을 누르면 해당 이름의 페이지로 이동한다.

포켓몬 영문 이름은 소문자이기 때문에 toLowerCase() 를 사용해서 입력된 값이 소문자로 넘어가게 한다. trim()으로 있을지 모르는 공백 제거도 해준다.

 

디테일 페이지와 모달

imageDetail.js 컴포넌트는 특정 포켓몬의 세부 정보를 보여주는 컴포넌트.

포켓몬 이름을 URL에서 가져와 해당 포켓몬의 데이터를 요청하고, 포켓몬 설명 및 타입에 따른 데미지 관계 정보를 함께 표시한다.

export default function Detail() {
  const { id: pokemonName } = useParams();
  const { pokemon, description, damageRelations } = useFetchPokemonDetail(pokemonName); 
  const [isModalOpen, setIsModalOpen] = useState(false);
  const navigate = useNavigate();

  if (!pokemon) {
    return <div>Loading...</div>;
  }

  // 모달 열기/닫기 기능
  const handleOpenModal = () => setIsModalOpen(true);
  const handleCloseModal = () => setIsModalOpen(false);

  // 이전 포켓몬 데이터로 이동 (ID로 요청)
  const handlePrev = () => {
    const prevId = pokemon.id - 1;
    if (prevId >= 1) {
      axios.get(requests.fetchPokemonById(prevId)).then((response) => {
        setIsModalOpen(false);
        navigate(`/${response.data.name.toLowerCase()}`);
      });
    }
  };
//다음 포켓몬
const handleNext = () => {/*생략*/}

return (

  //생략

 {/* 모달이 열렸을 때만 렌더링 */}
  {isModalOpen && (
    <Modal damageRelations={damageRelations} onClose={handleCloseModal} />
  )}
 )
};

1. 포켓몬 이름 가져오기 useParams 사용

const { id: pokemonName } = useParams();

PokemonList.js 에서  onClick={() => navigate(`/${pokemon.name}`)} URL에 있는 포켓몬 이름을 가져와서 id 값으로 쓴다.

 

2. 훅 useFetchPokemonDetail.js 포켓몬 이름으로 데이터 요청

const { pokemon, description, damageRelations } = useFetchPokemonDetail(pokemonName); 

해당 포켓몬의 정보 (pokemon), 설명 (description), 데미지 관계 (damageRelations)를 받아온다.

 

3. 로딩 상태 처리

포켓몬 데이터가 아직 로드되지 않았다면 Loading... 메시지를 표시한다.

  if (!pokemon) {
    return <div>Loading...</div>;
  }

 

4. 모달 열기/닫기:

  const [isModalOpen, setIsModalOpen] = useState(false);

  // 모달 열기/닫기 기능
  const handleOpenModal = () => setIsModalOpen(true);
  const handleCloseModal = () => setIsModalOpen(false);

isModalOpen 상태를 사용하여 모달의 열림/닫힘을 관리한다.

 

5. 이전/다음 포켓몬으로 이동:

  const handlePrev = () => {
    const prevId = pokemon.id - 1;
    if (prevId >= 1) {
      axios.get(requests.fetchPokemonById(prevId)).then((response) => {
        setIsModalOpen(false);
        navigate(`/${response.data.name.toLowerCase()}`);
      });
    }
  };

API를 통해 받아온 포켓몬의 고유 ID 값 사용.

handlePrev 함수는 현재 포켓몬의 ID에서 1을 빼서 이전 포켓몬으로 이동한다.

ID가 1 이상일 경우, 이전 포켓몬의 데이터를 요청하고, 응답받은 이름으로 URL을 변경. 다음 이동 함수도 같은 내용.

 

useFetchPokemonDetail 훅 : 포켓몬의 이름을 받아 해당 포켓몬의 세부 정보, 설명, 데미지 관계 정보를 가져온다.

export const useFetchPokemonDetail = (pokemonName) => {

  //생략 

  return { pokemon, description, damageRelations };
};

1. 상태관리 

  const [pokemon, setPokemon] = useState(null);
  const [description, setDescription] = useState('');
  const [damageRelations, setDamageRelations] = useState(null);

pokemon: 포켓몬의 기본 데이터를 저장

description: 포켓몬의 설명을 저장

damageRelations: 포켓몬 타입에 따른 데미지 관계 데이터를 저장

 

2. 포켓몬 데이터 요청:

useEffect(() => {
    const fetchPokemonDetail = async () => {
      try {
        // 포켓몬 기본 정보 요청
        const response = await axios.get(
          requests.fetchPokemonByName(pokemonName)
        );
        setPokemon(response.data);

        // 포켓몬 설명
        const speciesResponse = await axios.get(
          requests.fetchPokemonSpecies(pokemonName)
        );
        const flavorTextEntry = speciesResponse.data.flavor_text_entries.find(
          (entry) => entry.language.name === 'en'
        );

        setDescription(
          flavorTextEntry
            ? flavorTextEntry.flavor_text
            : 'No description available.'
        );

        // 포켓몬 타입에 따른 데미지 관계 가져오기
        if (response.data.types.length > 0) {
          const typeName = response.data.types[0].type.name; // 첫 번째 타입 선택
          const typeResponse = await axios.get(
            requests.fetchPokemonType(typeName)
          );
          setDamageRelations(typeResponse.data.damage_relations); // 데미지 관계 설정
        }
      } catch (error) {
        console.error('포켓몬 데이터를 가져오는 중 오류 발생:', error);
      }
    };

    fetchPokemonDetail();
  }, [pokemonName]);

useEffect는 pokemonName이 변경될 때마다 실행된다.

axios.get을 사용하여 포켓몬 이름으로 기본 데이터를 요청하고, setPokemon으로 상태를 업데이트.

가져온 데이터에서 영어 설명(flavor_text_entries)을 찾아서 설명 상태에 저장 만약 설명이 없으면 기본 메시지를 설정한다.

포켓몬의 타입 정보가 있으면 첫 번째 타입의 이름을 사용하여 해당 타입에 대한 데미지 관계 정보를 요청하고 이 데이터를 damageRelations 상태로 저장.

pokemon, description, damageRelations 데이터를 반환하여 Detail.js에서 사용할 수 있게 한다.

 

 

 

워밍업클럽

답변 0