노마드코더 AI 기초 탄탄 클럽 #19 Pokemon Gallery
이 글은 노마드코더 - AI 기초 탄탄 클럽 시리즈의 12번째 기록입니다. (총 13개)
노마드코더에서 수강 가능한 풀스택 웹 기초를 6주만에 훑는 챌린지.
해당 강의: ReactJS로 영화 웹 서비스 만들기
#19는 React(JS)로 포켓몬 갤러리를 만드는 것.
챌린지 목표
- 여러 마리의 포켓몬을 카드 형태로 보여주는 갤러리를 React로 만드세요.
- 블루프린트에 제공된 포켓몬 배열을 활용하세요. (! 이걸 지금 봤는데 API에서 불러온거라 실패처리되려나…)
챌린지 조건
- 지금 블루프린트에는 Bulbasaur 한 마리만 화면에 보입니다.
pokemons배열을.map()으로 순회하여, 8마리 모두가 카드로 표시되도록 만드세요. - 각 카드에 고유한
key값을 지정하세요. - 블루프린트의 “잡기” 버튼이 동작하도록 만드세요. 버튼을 처음 누르면 잡힌 상태(🔴)가 되고, 다시 누르면 놓아준 상태(⚪)로 돌아가야 합니다.
- 각 카드의 “잡기” 상태는 서로 독립적으로 관리되어야 합니다.
리스트 렌더링 — .map() + key
pokemons.map((pokemon) => (
<PokemonCard key={pokemon.id} ... />
))
- React는 리스트를 렌더링할 때 각 요소를 추적할 고유 식별자가 필요함
key는 props가 아니라 React 내부용 힌트 —PokemonCard안에서props.key로 접근 불가- 배열 index를 key로 쓰면 순서가 바뀔 때 버그 생길 수 있어서, 고유 ID 사용이 정석
상태 끌어올리기 (Lifting State Up)
- 잡기 상태를
PokemonCard안에서 각자 관리하지 않고 굳이App으로 끌어올림(사실 챌린지 달성만 목표로 했다면 끌어올리지 않아도 됨)
// App.jsx
const [caughtIds, setCaughtIds] = React.useState([]);
// PokemonCard에 내려줌
isCaught={caughtIds.includes(pokemon.id)}
onToggle={() => toggleCatch(pokemon.id)}
- 전체 포획 수 집계 (
currentCaught, 달성률 계산)가 필요했기 때문. 미션은 아니었지만 왠지 당연히 있어야 할 것 같았다. - 상태를 공유하거나 집계해야 할 때 → 공통 부모로 올리는 게 React 패턴
useEffect + 비동기 fetch
React.useEffect(() => {
const fetchPokemons = async () => { ... };
fetchPokemons();
}, []);
useEffect콜백 자체는 async 불가 → 내부에 async 함수 선언 후 즉시 호출하는 패턴[]빈 의존성 배열 → 마운트 시 딱 한 번만 실행
Promise.all로 병렬 fetch
const detailedPokemons = await Promise.all(
data.results.map(async (pokemon, index) => {
const res = await fetch(`.../${index + 1}`);
return res.json();
})
);
- 151번까지 순차 요청하면 엄청 느림(미션은 8개였으나 151번까지가 1세대 포켓몬이라 maxIndex를 151로 잡음)
Promise.all은 모든 요청을 동시에 쏘고, 전부 완료되면 결과 배열 반환- 단, 하나라도 실패하면 전체 실패 → 실무에선
Promise.allSettled고려. 단 이번에는 하나의 API에서 데이터를 받아오는 방식이라Promise.all사용
토글 상태 관리 — 배열 기반
const toggleCatch = (id) => {
if (caughtIds.includes(id)) {
setCaughtIds(caughtIds.filter((caughtId) => caughtId !== id));
} else {
setCaughtIds([...caughtIds, id]);
}
};
- boolean 하나짜리 상태가 아니라, 어떤 ID들이 잡혔는지 배열로 관리
Set으로 관리하는 방식도 있음 — 포켓몬이 많아질수록includesO(n)보다Set.hasO(1)이 유리
다국어 처리 — 객체 룩업
const notices = {
ko: '현재 잡은 포켓몬',
en: 'Current Pokémon',
'ja-hrkt': '現在所持しているポケモン',
};
// 사용
notices[lang]
if/else나switch대신 객체를 딕셔너리처럼 쓰는 패턴- PokeAPI의
pokemon.names.find(item => item.language.name === lang)도 같은 맥락 — 언어 코드로 필터링
CSS Grid auto-fill + minmax
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
- 컬럼 수를 직접 지정하지 않아도, 컨테이너 너비에 따라 자동으로 줄 수 조정
- 반응형 갤러리 레이아웃의 정석 패턴
toFixed(n)
- 소수점 n자리까지만 보여줌. n+1자리에서 반올림
const percentageOfGoal = (currentCaught / maxIndex) * 100;
...
<span>
{notices[lang]}: {currentCaught} / {maxIndex} ({percentageOfGoal.toFixed(2)}%)
</span>
textTransform: ‘capitalize’
<p style={{ textTransform: 'capitalize' }}>
{pokemon.egg_groups.map((group) => group.name).join(', ')}
</p>
- 소문자로 데이터를 넘겨도 브라우저가 알아서 첫 알파벳을 대문자로 렌더링해줌.
replace, Set
const cleanNames = pokemon.egg_groups.map((group) =>
group.name.replace(/[0-9]/g, '')
); // 숫자 없애기
const uniqueNames = [...new Set(cleanNames)]; // 중복 제거
return (
<PokemonCard
key={pokemon.id}
name={pokemonName.name}
id={pokemon.id}
type={uniqueNames.join(', ')} // 여러개면 연달아 표시
isCaught={caughtIds.includes(pokemon.id)}
onToggle={() => toggleCatch(pokemon.id)}
/>
);

Series: 노마드코더 - AI 기초 탄탄 클럽
1 | 노마드코더 AI 기초 탄탄 클럽 #3 Position & Pseudo Selector 2 | 노마드코더 AI 기초 탄탄 클럽 #4 Playlist Layout 3 | 노마드코더 AI 기초 탄탄 클럽 #5 Books Layout 4 | 노마드코더 AI 기초 탄탄 클럽 #6 Music Player 5 | 노마드코더 AI 기초 탄탄 클럽 #8 Events&Objects 6 | 노마드코더 AI 기초 탄탄 클럽 #10 JS Casino 7 | 노마드코더 AI 기초 탄탄 클럽 #11 Christmas Clock 8 | 노마드코더 AI 기초 탄탄 클럽 #12 Random Gradient 9 | 노마드코더 AI 기초 탄탄 클럽 #15 계산기 C++ vs Python 10 | 노마드코더 AI 기초 탄탄 클럽 #16 For Loop with Python 11 | 노마드코더 AI 기초 탄탄 클럽 #18 React Calculator 12 | 노마드코더 AI 기초 탄탄 클럽 #19 Pokemon Gallery 읽는 중 13 | 노마드코더 AI 기초 탄탄 클럽 #22 Marvel Character Gallery