Vite을 이용한 React 생성
npm init vite
위 코드를 통해 vite을 이용해 react 생성합니다.
project name은 react-pockmon-app으로 지어줬고, framework는 react, variant는 JS입니다.
npm install axios react-router-dom
npm install -D autoprefixer postcss tailwindcss
필요한 패키지 설치합니다.
npx init tailwindcss
위 명령어를 통해 tailwindcss 실행합니다.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
// './src/**/*.{js, jsx, ts, tsx}',
'./src/**/*.jsx',
'./index.html',
'./src/styles/main.css'
],
theme: {
extend: {},
},
plugins: [],
}
tailwind.config.cjs 파일입니다.
content의 주석부분은 에러가 계속 떠서 처리하는 겁니다.
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
postcss.config.cjs파일입니다.
@tailwind base;
@tailwind components;
@tailwind utilities;
src폴더 안에 index.css파일입니다.
npm run dev
여기까지 세팅 완료되며 위 명령어를 통해 테스트 해봅니다.
API를 통해 포켓몬 데이터 가져오기
const [pokemons, setPokemons] = useState([])
const url = 'https://pokeapi.co/api/v2/pokemon/?limit=1008&offset=0'
useEffect(() => {
fetchPokeData()
}, [])
const fetchPokeData = async () => {
try {
const response = await axios.get(url)
console.log(response.data.results)
setPokemons(response.data.results)
} catch(error) {
console.error(error)
}
}
app.jsx에서 axio를 통해 api로 데이터 가져옵니다.
<article className='pt-6'>
<header className='flex flex-col gap-2 w-full px-4 z-50'>
Input form
</header>
<section className='pt-6 flex flex-col justify-content items-center overflow-auto z-0'>
<div className='flex flex-row flex-wrap gap-[16px] items-center justify-center px-2 max-w-4xl'>
{pokemons.length > 0 ?
(
pokemons.map(({url, name}, index) => (
<div>
{name}
</div>
))
) :
(
<h2 className='font-medium text-lg text-slate-900 mb-1'>
포켓몬이 없습니다.
</h2>
)
}
</div>
</section>
</article>
App.jsx파일에 return부분입니다. 화면에서 기본 틀을 먼저 제작한 겁니다.
포켓몬 카드 생성하기
const [pokemon, setPokemon] = useState()
useEffect(() => {
fetchPokeDetailData()
}, [])
async function fetchPokeDetailData() {
try{
const response = await axios.get(url)
console.log(response.data)
const pokemonData = formatPokemonData(response.data)
setPokemon(pokemonData)
}catch(error) {
console.error(error)
}
}
function formatPokemonData(params) {
const {id, types, name} = params
const PokeData ={
id,
name,
type: types[0].type.name
}
return PokeData
}
src폴더 안에 components폴더 생성한 후 PokeCard.jsx 파일을 생성합니다.
코드는 위와 같습니다.

App.jsx에서 PokeCard 컴포넌트를 import해서 사용합니다.
포켓몬 카드 UI 생성하기
const bg = `bg-${pokemon?.type}`
const border = `border-${pokemon?.type}`
const text = `text-${pokemon?.type}`
const img = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${pokemon?.id}.png`
PokeCard.jsx파일 변수들 추가합니다.
<>
{pokemon &&
<a
href={`/pokemon/${name}`}
className={`box-boder rounded-lg ${border} w-[8.5rem] h-[8.5rem] z-0 bg-slate-800 justify-between items-center`}
>
<div className={`${text} h-[1.5rem] text-xs w-full pt-1 px-2 text-right rounded-t-lg`}>
#{pokemon.id.toString().padStart(3, '00')}
</div>
<div className='w-full f-6 flex items-center justify-center'>
<div className='box-border relative flex w-full h-[5.5rem] basis justify-center items-center'>
<img
src={img}
alt={name}
width='100%'
className='object-contain h-full'
/>
</div>
</div>
<div className={`${bg} text-xs text-zinc-100 h-[1.5rem] rounded-b-lg uppercase font-medium pt-1`}>
{pokemon.name}
</div>
</a>
}
</>
PokeCard.jsx파일 return부분을 위와 같이 작성합니다.
export function SetColors() {
return (
<>
<div className="text-normal bg-normal "></div>
<div className="text-fire bg-fire "></div>
<div className="text-water bg-water "></div>
<div className="text-electric bg-electric "></div>
<div className="text-grass bg-grass "></div>
<div className="text-ice bg-ice "></div>
<div className="text-fighting bg-fighting "></div>
<div className="text-poison bg-poison "></div>
<div className="text-ground bg-ground "></div>
<div className="text-flying bg-flying "></div>
<div className="text-psychic bg-psychic "></div>
<div className="text-bug bg-bug "></div>
<div className="text-rock bg-rock "></div>
<div className="text-ghost bg-ghost "></div>
<div className="text-dragon bg-dragon "></div>
<div className="text-dark bg-dark "></div>
<div className="text-steel bg-steel "></div>
<div className="text-fairy bg-fairy"></div>
<div className="text-none bg-none"></div>
</>
)
}
위에 PokeCard.jsx파일에서 bg색상이 필요합니다.
components폴더 안에 SetColors 파일을 생성한 후 위와 같이 각 색상을 표시해줍니다.
colors: {
primary: '#ffcb05',
second: '#3d7dca',
third: '#003a70',
normal: '#A8A77A',
fire: '#EE8130',
water: '#6390F0',
electric: '#dfbc30',
grass: '#7AC74C',
ice: '#97d4d2',
fighting: '#b83e3a',
poison: '#A33EA1',
ground: '#E2BF65',
flying: '#A98FF3',
psychic: '#F95587',
bug: '#A6B91A',
rock: '#B6A136',
ghost: '#735797',
dragon: '#6F35FC',
dark: '#705746',
steel: '#B7B7CE',
fairy: '#D685AD',
none: '#BfBfBf',
}
tailwind.config.cjs파일 theme옵션 중 extend 안에도 각 색상을 표시해줍니다.
Image Lazy Loading
Image Lazy Loading은 페이지 안에 있는 실제로 화면에 보일 필요가 있을 때 로딩을 할 수 있도록 하는 테크닉입니다.
구현 방법:
- JS 이벤트를 이용해서 구현
- Intersection Observe API 이용해서 구현
- 브라우저 Native Lazy Loading 이용 => loading 속성 이용
import React, { useEffect, useState } from 'react'
const LazyImage = ({url, alt}) => {
const [isLoading, setIsLoading] = useState(true)
const [opacity, setOpacity] = useState('opacity-0')
useEffect(() => {
isLoading ? setOpacity('opacity-0') : setOpacity('opacity-100')
}, [isLoading])
return (
<>
{isLoading && (
<div className='absolute h-full z-10 w-full flex items-center justify-cneter'>
...loading
</div>
)}
<img
src={url}
alt={alt}
width='100%'
height='auto'
loading='lazy'
onLoad={() => setIsLoading(false)}
className={`object-contain h-full ${opacity}`}
/>
</>
)
}
export default LazyImage
src폴더안에 components폴더 안에 LazyImage.jsx파일을 만듭니다. 위 코드를 작성합니다.

PokeCard.jsx 파일에 img태그를 삭제한 후 위와 같이 수정합니다.
더보기 기능 생성하기


App.jsx파일을 위와 같이 수정하면 됩니다.
import React, { useEffect, useState } from 'react'
import axios from 'axios'
import './App.css'
import PokeCard from './components/PokeCard'
function App() {
const [pokemons, setPokemons] = useState([])
const [offset, setOffset] = useState(0)
const [limit, setLimit] = useState(20)
useEffect(() => {
fetchPokeData(true)
}, [])
const fetchPokeData = async (isFirstFetch) => {
try {
const offsetValue = isFirstFetch ? 0 : offset + limit
const url = `https://pokeapi.co/api/v2/pokemon/?limit=${limit}&offset=${offsetValue}`
const response = await axios.get(url)
setPokemons([...pokemons, ...response.data.results])
setOffset(offsetValue)
} catch(error) {
console.error(error)
}
}
return (
<article className='pt-6'>
<header className='flex flex-col gap-2 w-full px-4 z-50'>
Input form
</header>
<section className='pt-6 flex flex-col justify-content items-center overflow-auto z-0'>
<div className='flex flex-row flex-wrap gap-[16px] items-center justify-center px-2 max-w-4xl'>
{pokemons.length > 0 ?
(
pokemons.map(({url, name}, index) => (
<PokeCard key={url} url={url} name={name}/>
))
) :
(
<h2 className='font-medium text-lg text-slate-900 mb-1'>
포켓몬이 없습니다.
</h2>
)
}
</div>
</section>
<div className='text-center'>
<button
onClick={() => fetchPokeData(false)}
className='bg-slate-800 px-6 py-2 my-4 text-base rounded-lg font-bold text-white'>
더 보기
</button>
</div>
</article>
)
}
export default App
현재 App.jsx파일 전체 코드입니다.
검색 기능 생성하기

App.jsx파일에 handleSearch 함수를 만듭니다.

App.jsx파일에 검색 부분을 위와 같이 디자인합니다.
useDebounce Custom Hookds 만들기
검색입력에 입력할 때 결과가 나타날 때까지 지연이 있습니다. 이 기능은 debounce라는 function에 의해 제어됩니다.
debounce function은 사용자가 미리 결정된 시간 동안 타이핑을 멈출 때까지 keyup이벤트의 처리를 지연시킵니다.
이렇게 하면 UI코드가 모든 이벤트를 처리할 필요가 없고 서버로 전송되는 API호출 수도 크게 줄어듭니다.
입력된 모든 문자를 처리하면 성능이 저하되고 백엔드에 불필요한 로드가 추가될 수 있습니다.
import { useEffect, useState } from "react"
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
src폴더안에 hooks폴더안에 useDebounce.js파일을 만들고 위와 같이 작성합니다.



App.jsx파일을 위와 같이 추가 및 수정합니다.
AutoComplete기능 생성하기
AutoComplete기능을 구현하기 위해 관련된 이름들을 모두 가져와야 합니다. 하지만 현재 데이터를 20개씩 가져오기 때문에 소스 코드의 수정이 필요합니다.



App.jsx파일을 위와 같이 수정, 추가합니다.
import React, { useEffect, useState } from 'react'
import axios from 'axios'
import './App.css'
import PokeCard from './components/PokeCard'
import { useDebounce } from './hooks/useDebounce'
function App() {
// 모든 포켓몬 데이터를 가지고 있는 state
const [allPokemons, setAllPokemons] = useState([])
// 실제로 리스트로 보여주는 포켓몬 데이터를 가지고 있는 state
const [displayedPokemons, setdisplayedPokemons] = useState([])
// 한번에 보여주는 포켓몬 수
const limitNum = 20
const url = 'https://pokeapi.co/api/v2/pokemon/?limit=1008&offset=0'
const [searchTerm, setSearchTerm] = useState('')
const debouncedSearchTerm = useDebounce(searchTerm, 500)
useEffect(() => {
fetchPokeData()
}, [])
useEffect(() => {
handleSearchInput(debouncedSearchTerm)
}, [debouncedSearchTerm])
const filterDisplayedPokemonData = (allPokemonsData, displayedPokemons = []) => {
const limit = displayedPokemons.length + limitNum
// 모든 포멧몬 데이터에서 limitNum만큼 더 가져오기
const array = allPokemonsData.filter((pokemon, index) => index + 1 <= limit)
return array
}
const fetchPokeData = async () => {
try {
// 1008 포켓몬 데이터 받아오기
const response = await axios.get(url)
// 모든 포켓몬 데이터 기억하기
setAllPokemons(response.data.results)
// 실제로 화면에 보여줄 포켓몬 리스트 기억하기
setdisplayedPokemons(filterDisplayedPokemonData(response.data.results))
} catch(error) {
console.error(error)
}
}
const handleSearchInput = async (searchTerm) => {
if(searchTerm.length > 0) {
try {
const response = await axios.get(`https://pokeapi.co/api/v2/pokemon/${searchTerm}`)
const pokemonData = {
url: `https://pokeapi.co/api/v2/pokemon/${response.data.id}`,
name: searchTerm
}
setAllPokemons([pokemonData])
} catch (error) {
setAllPokemons([])
console.log(error)
}
} else {
fetchPokeData(true)
}
}
return (
<article className='pt-6'>
<header className='flex flex-col gap-2 w-full px-4 z-50'>
<div className='relative z-50'>
<form className='relative flex justify-center items-center 2-[20.5rem] h-6 rounded-lg m-auto'>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className='text-xs w-[20.5rem] h-6 px-2 py-1 bg-slate-500 rounded-lg text-gray-300 text-center'/>
<button type='submit' className='text-xs bg-slate-900 text-slate-300 w-[2.5rem] h-6 px-2 py-1 rounded-r-lg text-center absolute right-0 hover:bg-slate-700'>
검색
</button>
</form>
</div>
</header>
<section className='pt-6 flex flex-col justify-content items-center overflow-auto z-0'>
<div className='flex flex-row flex-wrap gap-[16px] items-center justify-center px-2 max-w-4xl'>
{displayedPokemons.length > 0 ?
(
displayedPokemons.map(({url, name}, index) => (
<PokeCard key={url} url={url} name={name}/>
))
) :
(
<h2 className='font-medium text-lg text-slate-900 mb-1'>
포켓몬이 없습니다.
</h2>
)
}
</div>
</section>
<div className='text-center'>
{(allPokemons.length > displayedPokemons.length) && (displayedPokemons.length !== 1) &&
(
<button
onClick={() => setdisplayedPokemons(filterDisplayedPokemonData(allPokemons, displayedPokemons))}
className='bg-slate-800 px-6 py-2 my-4 text-base rounded-lg font-bold text-white'>
더 보기
</button>
)
}
</div>
</article>
)
}
export default App
현재의 App.jsx파일입니다.
AutoComplete컴포넌트 생성하기
import React, { useState } from 'react'
const AutoComplete = ({allPokemons, setDisplayedPokemons}) => {
const [searchTerm, setSearchTerm] = useState('')
const filterNames = (input) => {
const value = input.toLowerCase();
return value? allPokemons.filter((e) => e.name.includes(value)) : [];
}
const handleSubmit = (e) => {
e.preventDefault()
let text= searchTerm.trim()
setDisplayedPokemons(filterNames(text))
setSearchTerm('')
}
const checkEqualName = (input) => {
const filteredArray = filterNames(input)
return filteredArray[0]?.name === input ? [] : filteredArray
}
return (
<div className='relative z-50'>
<form
onSubmit={handleSubmit}
className='relative flex justify-center items-center 2-[20.5rem] h-6 rounded-lg m-auto'
>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className='text-xs w-[20.5rem] h-6 px-2 py-1 bg-slate-500 rounded-lg text-gray-300 text-center'/>
<button type='submit' className='text-xs bg-slate-900 text-slate-300 w-[2.5rem] h-6 px-2 py-1 rounded-r-lg text-center absolute right-0 hover:bg-slate-700'>
검색
</button>
</form>
{checkEqualName(searchTerm).length > 0 && (
<div className='w-full flex bottom-0 h-0 flex-col absolute justify-center items-center translate-y-2'>
<div className='w-0 h-0 bottom-0 border-x-transparent border-x-8 border-b-[8px] border-gray-700 -translate-y-1/2'>
</div>
<ul className='w-40 max-h-[134px] py-1 bg-gray-700 rounded-lg absolute top-0 overflow-auto scrollbar-none'>
{checkEqualName(searchTerm).map((e, i) => (
<li key={`button-${i}`}>
<button
onClick={() => setSearchTerm(e.name)}
className='text-base w-full hover:bg-gray-600 p-[2px] text-gray-10'>
{e.name}
</button>
</li>
))}
</ul>
</div>
)}
</div>
)
}
export default AutoComplete
현재의 App.jsx파일입니다.
map 메소드를 이용해서 array의 내용을 모두 훑어 보고 부합한 결과를 출력하는 겁니다.

auto complete를 할 때 스크롤바를 없애고 싶습니다.
위에 javascript에서도 scrollbar-none 속성을 추가한 적이 있습니다.
npm i -D tailwind-scrollbar
위 명령어를 통해 해당 모듈을 설치합니다.

tailwind.config.cjs의 plugins에 해당 모듈을 등록합니다.

스크롤바가 없어지는 거 보입니다.
'배워서 따라하는 포플 > 포켓몬 도감 앱' 카테고리의 다른 글
| 타입스크립트로 변경하기(1) (0) | 2023.09.18 |
|---|---|
| 로그인 페이지 생성하기 (0) | 2023.09.17 |
| 상세 페이지 생성하기(3) (0) | 2023.09.17 |
| 상세 페이지 생성하기(2) - Damage Relations (0) | 2023.09.16 |
| 상세 페이지 생성하기(1) (0) | 2023.09.14 |