React를 활용해 포켓몬스터 API로 포켓몬 도감만들기 2
이전에 대충 도감 목록을 띄웟을때 포켓몬들이 세로로만 정렬되어 있엇다 보기 불편하니까 가로로 정렬해보자
이번에 필요한건 css
먼저 pokedex.css 파일을 만들어 주고
.container {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.pokemon {
display: flex;
flex-direction: column;
align-items: center;
margin: 10px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
간단한 css 파일을 추가한 후
pokedex.js 파일에 가서
const renderPokemonList = () => {
return pokemonData.map((pokemon) => (
<div key={pokemon.id} className='pokemon'>
<img src={pokemon.sprites.front_default} alt={pokemon.korean_name} />
<p>{pokemon.korean_name}</p>
<p>ID: {pokemon.id}</p>
</div>
));
};
return (
<div className='container'>
{renderPokemonList()}
</div>
);
};
이렇게 두군데에 className을 추가해 주고 나면
이제 난 포켓몬을 클릭하면 해당하는 포켓몬의 자세한 정보가 나왓으면 좋겠다.
포켓몬api에 얼마만큼의 정보가 들어 있을지 모르니 그것부터 알아보자
https://pokeapi.co/docs/v2 여기 공식 홈페이지에서 확인할 수 있는데 왠만한건 다들어있는거 같으니
도감번호, 이름, 키, 무게, 이미지, 속성, 특성, 기술, 진화방법, 설명 까지만 나오게 하자
엄청 많이 불러오는거 같지만 성별이라던지 위치 아이템 비전머신적용여부등 할 수 있는게 더많다
근데 상세정보를 출력하기 전에 첫 도감페이지 로딩이 너무 오래걸리고 있다 채감상 3초정도?
요즘엔 이정도 속도면 뭔가 문제가 있다고 판단할 정도이기에 속도를 좀더 높여보자
내가볼때 이건 map으로 151번의 api호출이 있기 때문인 걸로 보이는데
react-infinite-scroll-component를 사용해서 당장 화면에 보여지는 포켓몬만 출력 속도를 줄이고
페이지가 맛이간게 아닌 로딩중이란것을 표현 하기 위해 loding 화면 또한 넣어보자
import React, { useState, useEffect } from "react";
import axios from "axios";
import "./pokedex.css";
import { Link } from "react-router-dom";
import InfiniteScroll from 'react-infinite-scroll-component';
const Pokedex = () => {
const [pokemonData, setPokemonData] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const pokemonPerPage = 56;
useEffect(() => {
const fetchData = async () => {
const allPokemonData = [];
for (let i = 1; i <= currentPage * pokemonPerPage; i++) {
const response = await axios.get(`https://pokeapi.co/api/v2/pokemon/${i}`);
const speciesResponse = await axios.get(`https://pokeapi.co/api/v2/pokemon-species/${i}`);
const koreanName = speciesResponse.data.names.find((name) => name.language.name === "ko");
allPokemonData.push({ ...response.data, korean_name: koreanName.name });
}
setPokemonData(allPokemonData);
};
fetchData();
}, [currentPage]);
const fetchMoreData = () => {
setCurrentPage((prevPage) => prevPage + 1);
};
return (
<InfiniteScroll
dataLength={pokemonData.length}
next={fetchMoreData}
hasMore={currentPage * pokemonPerPage < 151}
loader={<h4>Loading...</h4>}
endMessage={<p>All Pokémon have been loaded</p>}
className="container"
>
{pokemonData.map((pokemon) => (
<div key={pokemon.id} className="pokemon">
<Link to={`/pokemon/${pokemon.id}`}>
<img src={pokemon.sprites.front_default} alt={pokemon.korean_name} />
<p>{pokemon.korean_name}</p>
<p>도감번호: {pokemon.id}</p>
</Link>
</div>
))}
</InfiniteScroll>
);
};
export default Pokedex;
pokdex.js를 조금 바꿔봤다
여기서 중요한 부분만 설명하자면
const pokemonPerPage = 56으로 설정되어 있는데
처음 보여주는 포켓몬이 56마리 라는것 너무 짧게해서 스크롤이 내려가지 않으면 업데이트 되지 않는다.
내 화면 해상도의 경우 52마리 까지 표시되기에 일단 56으로 설정했다
그렇게 하면
다른 문제가 발생하는데 56*3의 숫자로 168번까지 데이터를 불러와 버리는 것이다.............
난 1세대만 원하기에 불러올 포켓몬 수를 151까지로 제한해 보자
import React, { useState, useEffect } from "react";
import axios from "axios";
import "./pokedex.css";
import { Link } from "react-router-dom";
import InfiniteScroll from 'react-infinite-scroll-component';
const Pokedex = () => {
const [pokemonData, setPokemonData] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const pokemonPerPage = 76;
const totalPokemon = 151;
useEffect(() => {
const fetchData = async () => {
const allPokemonData = [];
for (let i = 1; i <= Math.min(currentPage * pokemonPerPage, totalPokemon); i++) {
const response = await axios.get(`https://pokeapi.co/api/v2/pokemon/${i}`);
const speciesResponse = await axios.get(`https://pokeapi.co/api/v2/pokemon-species/${i}`);
const koreanName = speciesResponse.data.names.find((name) => name.language.name === "ko");
allPokemonData.push({ ...response.data, korean_name: koreanName.name });
}
setPokemonData(allPokemonData);
};
fetchData();
}, [currentPage]);
const fetchMoreData = () => {
setCurrentPage((prevPage) => prevPage + 1);
};
return (
<InfiniteScroll
dataLength={pokemonData.length}
next={fetchMoreData}
hasMore={currentPage * pokemonPerPage < totalPokemon}
loader={<h4>Loading...</h4>}
endMessage={<p>All Pokémon have been loaded</p>}
className="container"
>
{pokemonData.map((pokemon) => (
<div key={pokemon.id} className="pokemon">
<Link to={`/pokemon/${pokemon.id}`}>
<img src={pokemon.sprites.front_default} alt={pokemon.korean_name} />
<p>{pokemon.korean_name}</p>
<p>도감번호: {pokemon.id}</p>
</Link>
</div>
))}
</InfiniteScroll>
);
};
export default Pokedex;
const totalPokemon = 151;const pokemonPerPage = 76;
for (let i = 1; i <= Math.min(currentPage * pokemonPerPage, totalPokemon); i++) 이부분이
for 루프로 currentPage 및 pokemonPerPage 값에 의해 결정되는 포켓몬 데이터를 가져오는 역할을 하는것인데하나하나 풀어보면 i는 포켓몬 ID를 나타나는 인덱스로 1에서 시작해서 점점 증가하게 된다currentPage의 경우 현재 페이지를 나타내는 상태변수로 역시 1에서 시작 스크롤할수록 증가하게 되며pokemonPerPage는 각 배치에서 가져오는 포켓몬 수를 나타내고 있다 현재 76으로 설정해 놓앙ㅆ다totalPokemon은 표시하려는 총 포켓몬의 수이며
Math.min(currentPage * pokemonPerPage, totalPokemon) 이부분에서 루프의 상한선을 계산하게 되는데
currntPage와 pokemonPerPage를 곱해서 현재 페이지까지 얼마나 많은 포켓몬을 가져와야 하는지 결정하고그다음 결과와 totalPokemon의 최소값을 계산한다 그러면 currntPage * pokemonPerPage가 151보다 높은 값이 되더라도 151에서 루프가 중지되게 된다
이게 하다보니까 좀 복잡하게 설명이 됬는데 최소값을 계산해서 그중 작은값을 가져오는게 결국Math.min이라는 javascript내장함수다
Math.min(currentPage * pokemonPerPage, totalPokemon) 여길 보면 쉼표로 나눠져 있는데
currentPage * pokemonPerPage 이값과 totalPokemon 이값 둘의 최소값을 구하는 것이라고 판단하면 좋다
아무튼 이렇게 하면
전보다는 조금 빠르게 그래봐야 2초정도로 느껴지지만 훨씬 나아진거 같은 느낌이 든다
다음 상세페이지로 넘어가보자
component폴더에
PokemonDetails.js파일을 만들고
import React from 'react';
const PokemonDetails = ({ pokemon }) => {
if (!pokemon) {
return <p>Loading...</p>;
}
const renderTypes = () => {
return pokemon.types.map((type) => <span key={type.type.name}>{type.type.name}</span>);
};
const renderAbilities = () => {
return pokemon.abilities.map((ability) => <span key={ability.ability.name}>{ability.ability.name}</span>);
};
const renderMoves = () => {
return pokemon.moves.map((move) => <li key={move.move.name}>{move.move.name}</li>);
};
return (
<div>
<h2>{pokemon.korean_name} (#{pokemon.id})</h2>
<img src={pokemon.sprites.front_default} alt={pokemon.korean_name} />
<p>English Name: {pokemon.name}</p>
<p>Height: {pokemon.height}</p>
<p>Weight: {pokemon.weight}</p>
<p>Type: {renderTypes()}</p>
<p>Abilities: {renderAbilities()}</p>
<p>Moves:</p>
<ul>{renderMoves()}</ul>
</div>
);
};
export default PokemonDetails;
그다음 같은 폴더에
PokemonDetailsPage.js를 만들고
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams } from 'react-router-dom';
import PokemonDetails from './PokemonDetails';
const PokemonDetailsPage = () => {
const { id } = useParams();
const [pokemonData, setPokemonData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`);
const speciesResponse = await axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`);
const koreanName = speciesResponse.data.names.find((name) => name.language.name === 'ko');
setPokemonData({ ...response.data, korean_name: koreanName.name });
};
fetchData();
}, [id]);
return <PokemonDetails pokemon={pokemonData} />;
};
export default PokemonDetailsPage;
마지막으로 App.js를 변경
import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Pokedex from "./components/Pokedex";
import PokemonDetailsPage from "./components/PokemonDetailsPage";
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Pokedex />} />
<Route path="/pokemon/:id" element={<PokemonDetailsPage />} />
</Routes>
</Router>
);
}
export default App;
이렇게 하고 이상해씨를 클릭해보면?
영어로 나온다
그럼 한국어로 바꿔보자
먼저 PkemonDetails.js
import React from 'react';
const PokemonDetails = ({ pokemon }) => {
if (!pokemon) {
return <p>Loading...</p>;
}
const renderTypes = () => {
return pokemon.types.map((type, index) => (
<span key={type.type.name}>
{type.type.korean_name}
{index < pokemon.types.length - 1 ? ', ' : ''}
</span>
));
};
const renderAbilities = () => {
return pokemon.abilities.map((ability, index) => (
<span key={ability.ability.name}>
{ability.ability.korean_name}
{index < pokemon.abilities.length - 1 ? ', ' : ''}
</span>
));
};
const renderMoves = () => {
return pokemon.moves.map((move) => <li key={move.move.name}>{move.move.korean_name}</li>);
};
return (
<div>
<h2>{pokemon.korean_name} (#{pokemon.id})</h2>
<img src={pokemon.sprites.front_default} alt={pokemon.korean_name} />
<p>이름: {pokemon.korean_name}</p>
<p>키: {pokemon.height}</p>
<p>무게: {pokemon.weight}</p>
<p>속성: {renderTypes()}</p>
<p>특성: {renderAbilities()}</p>
<p>기술:</p>
<ul>{renderMoves()}</ul>
</div>
);
};
export default PokemonDetails;
그다음 PokemonDetailsPage.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams } from 'react-router-dom';
import PokemonDetails from './PokemonDetails';
const PokemonDetailsPage = () => {
const { id } = useParams();
const [pokemonData, setPokemonData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`);
const speciesResponse = await axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`);
const koreanName = speciesResponse.data.names.find((name) => name.language.name === 'ko');
const typesWithKoreanNames = await Promise.all(
response.data.types.map(async (type) => {
const typeResponse = await axios.get(type.type.url);
const koreanTypeName = typeResponse.data.names.find(
(name) => name.language.name === 'ko'
).name;
return { ...type, type: { ...type.type, korean_name: koreanTypeName } };
})
);
const abilitiesWithKoreanNames = await Promise.all(
response.data.abilities.map(async (ability) => {
const abilityResponse = await axios.get(ability.ability.url);
const koreanAbilityName = abilityResponse.data.names.find(
(name) => name.language.name === 'ko'
).name;
return { ...ability, ability: { ...ability.ability, korean_name: koreanAbilityName } };
})
);
const movesWithKoreanNames = await Promise.all(
response.data.moves.map(async (move) => {
const moveResponse = await axios.get(move.move.url);
const koreanMoveName = moveResponse.data.names.find(
(name) => name.language.name === 'ko'
).name;
return { ...move, move: { ...move.move, korean_name: koreanMoveName } };
})
);
setPokemonData({
...response.data,
korean_name: koreanName.name,
types: typesWithKoreanNames,
abilities: abilitiesWithKoreanNames,
moves: movesWithKoreanNames,
});
};
fetchData();
}, [id]);
return <PokemonDetails pokemon={pokemonData} />;
};
export default PokemonDetailsPage;
바꾸는 김에 좀더 바꿔봤다 이상해씨의 속성을 예를 들면 풀, 독타입인데 출력되는 값은 그냥 풀독 이어서 너무 대충인 느낌이 난다 그래서 코드 중간에
{index < pokemon.types.length - 1 ? ',' : ' '}를 추가하여
타입이 1보다 많으면 쉼표와 공백을 추가해 줫다
특성도 마찬가지 그러면
훌륭하다
마지막으로 보기 어려우니 css 만 조금 건드려 보자
이런식으로 만들고 싶은데 이건 개발이 아닌 디자인의 영역이 더크기에
이정도로 일단 만족하려 한다
아 적용한 css는
body {
background-color: #f5f5f5;
}
.pokemon-details {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
background-color: #f0e9d9;
border: 2px solid #db2a27;
border-radius: 15px;
margin: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.pokemon-details img {
width: 150px;
height: 150px;
object-fit: cover;
}
.pokemon-details h2 {
font-size: 24px;
font-weight: 700;
margin-bottom: 10px;
color: #db2a27;
}
.pokemon-details p {
font-size: 18px;
font-weight: 500;
margin-bottom: 5px;
color: #404040;
}
.type, .ability {
display: inline-block;
background-color: #e8e8e8;
padding: 5px 10px;
border-radius: 20px;
margin-right: 10px;
font-weight: 600;
color: #404040;
}
다음 pokemonDetails.js에
import React from 'react';
import './PokemonDetails.css'
const PokemonDetails = ({ pokemon }) => {
if (!pokemon) {
return <p>Loading...</p>;
}
const renderTypes = () => {
return pokemon.types.map((type, index) => (
<span key={type.type.name} className="type">
{type.type.korean_name}
{index < pokemon.types.length - 1 ? ', ' : ''}
</span>
));
};
const renderAbilities = () => {
return pokemon.abilities.map((ability, index) => (
<span key={ability.ability.name} className='ability'>
{ability.ability.korean_name}
{index < pokemon.abilities.length - 1 ? ', ' : ''}
</span>
));
};
const renderMoves = () => {
return pokemon.moves.map((move) => <li key={move.move.name}>{move.move.korean_name}</li>);
};
return (
<div className='pokemon-details'>
<h2>{pokemon.korean_name} (#{pokemon.id})</h2>
<img src={pokemon.sprites.front_default} alt={pokemon.korean_name} />
<p>이름: {pokemon.korean_name}</p>
<p>키: {pokemon.height}</p>
<p>무게: {pokemon.weight}</p>
<p>속성: {renderTypes()}</p>
<p>특성: {renderAbilities()}</p>
<p>기술:</p>
<ul>{renderMoves()}</ul>
</div>
);
};
export default PokemonDetails;