🔗 깃허브
🔗 배포링크
세 가지 브랜치로 나누어 작업
1. Props-drilling: 컴포넌트 계층 구조를 최대한 깊게 만들어 props drilling의 불편함 체감
2. Context: Context API를 활용한 리팩토링
3. Main: 최종 리팩토링 결과물
react-router-dom을 이용하여 홈페이지에서 도감 페이지로 이동
[기본화면]
[카드 추가했을 때 화면]
위의 형태의 간단한 페이지이다.
구성은 랜딩페이지, 도감페이지, 디테일 페이지이며
각 페이지별 구현사항을 정리해보겠다.
참고로 Props drilling을 Context Api로 리팩토링한 결과물만 정리해보겠다.
전체적으로 구조 부분에서 리팩토링 해야할 부분이 많아보였다.
const Router = () => {
const [pokeData, setPokeData] = useState([]);
const [selectedPoke, setSelectedPoke] = useState([]);
const mockData = MOCK_DATA;
// DashBoard.jsx 부분의 포켓볼 이미지 상태
const [ball, setBall] = useState([
{ img: "https://react-6-pokemon.vercel.app/assets/pokeball-13iwdk7Y.png" },
...... 생략
]);
// 삭제 기능
const delCard = (id) => {
confirm("삭제하시겠습니까?");
const deletedItem = selectedPoke.filter((item) => id !== item.id);
setSelectedPoke(deletedItem);
};
// 추가 기능
const addCard = (i) => {
const currentCard = pokeData[i].id;
if (selectedPoke.some((item) => item.id == currentCard)) {
alert("이미 등록된 카드입니다.");
} else {
setSelectedPoke((prev) => [...prev, pokeData[i]]);
alert("추가 하시겠습니까?");
}
};
return (
<Context.Provider
value={{
pokeData,
setPokeData,
...생략
}}
>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dex" element={<Dex />} />
<Route path="/pokemon-detail" element={<Detail />} />
</Routes>
</BrowserRouter>
</Context.Provider>
);
};
export default Router;
// Router.jsx파일
const Router = () => {
return (
<PokeDataProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dex" element={<Dex />} />
<Route path="/pokemon-detail" element={<Detail />} />
</Routes>
</BrowserRouter>
</PokeDataProvider>
);
};
// Context.jsx파일
export const PokeDataContext = createContext(null);
const initialSelectedPokes = Array(6)
.fill()
.map((item) => ({ id: uuidv4(), img_url: POKEBALL_IMG_URL }));
export const PokeDataProvider = ({ children }) => {
const [pokeData, setPokeData] = useState(MOCK_DATA);
const [selectedPoke, setSelectedPoke] = useState(initialSelectedPokes);
// 추가
const onAdd = (pokemon) => {
const emptyIndex = selectedPoke.findIndex(
(item) => item.img_url === POKEBALL_IMG_URL
);
const isDuplicate = selectedPoke.some((item) => item.id === pokemon.id);
if (isDuplicate) {
alert("이미 선택된 포켓몬입니다.");
return;
}
if (emptyIndex == -1) {
alert("6개 이상 추가할 수 없습니다.");
return;
} else {
const addConfirm = confirm("추가하시겠습니까?");
if (addConfirm) {
const newSelectedPoke = [...selectedPoke];
newSelectedPoke[emptyIndex] = pokemon;
setSelectedPoke(newSelectedPoke);
}
}
};
const onDelete = (id) => {
const confrimDelete = confirm("정말 삭제하시겠습니까?");
if (confrimDelete) {
const deletedPoke = selectedPoke.map((item) => {
if (item.id === id) {
return { img_url: POKEBALL_IMG_URL };
}
return item;
});
setSelectedPoke(deletedPoke);
}
};
const value = {
pokeData,
selectedPoke,
onAdd,
onDelete,
};
return (
<PokeDataContext.Provider value={value}>
{children}
</PokeDataContext.Provider>
);
};
수정 전에는 대시보드 카드 컴포넌트 따로, 리스트 카드 컴포넌트 카드 따로 였다.
// 대시보드의 카드 부분
<div className="dashboard">
<h2>나만의 포켓몬</h2>
<ul>
{selectedPoke.slice(0, 6).map((item, index) => {
return (
<li key={index} className="selected">
<img src={item.img_url} alt="" />
<p>{item.korean_name}</p>
<p>NO.0{item.id}</p>
<button
onClick={() => {
delCard(item.id);
}}
>
삭제
</button>
</li>
);
})}
{ball.map((item, index) => {
return (
<li key={index} className="pokeball_img">
<img src={item.img} alt="" />
</li>
);
})}
</ul>
</div>
// 포켓몬 리스트 카드 부분
<ul>
{mockData.map((item, i) => {
return (
<li key={i} onClick={() => nav(`/pokemon-detail?id=${item.id}`)}>
<p>{item.korean_name}</p>
<img src={item.img_url} alt="" />
<p>NO.0{item.id}</p>
<button
onClick={(event) => {
// 이벤트 전파 방지
event.stopPropagation();
addCard(i);
}}
>
추가
</button>
</li>
);
})}
</ul>
);
};
조건에 따라 아이디를 노출하거나, 조건에 따라 버튼 텍스트를 추가 또는 삭제로 보일 수 있도록했다.
const PoketmonCard = ({ id, korean_name, img_url, onClick, buttonText }) => {
const nav = useNavigate();
return (
<li
className="pokemonCard_wrapper"
onClick={() => nav(`/pokemon-detail?id=${id}`)}
>
<p>{korean_name}</p>
<img src={img_url} alt={`${korean_name} 이미지`} />
{img_url === POKEBALL_IMG_URL ? null : <p>NO.0{id}</p>}
{buttonText && img_url !== POKEBALL_IMG_URL && (
<button onClick={onClick}>{buttonText}</button>
)}
</li>
);
};
Detail.jsx에서 해당하는 아이디 하나만 필요한데 filter메서드를 사용했다.이 방법은 모든 요소를 탐색하게 되어 성능이 떨어질 수 있다.
const location = useLocation();
const pokeId = parseInt(location.search.slice(4));
const mockData = MOCK_DATA;
const filterData = mockData.filter((item) => item.id === pokeId);
const Detail = () => {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const paramsId = queryParams.get("id");
const { pokeData } = useContext(PokeDataContext);
// find 메서드로 변경
const currentPokemon = pokeData.find(
(item) => String(item.id) === String(paramsId)
);
// 포켓몬을 찾을 수 없을때 추가
if (!currentPokemon) {
return <p>포켓몬을 찾을 수 없습니다.</p>;
}
const { korean_name, img_url, types, description } = currentPokemon;
return (
// 생략
);
};
1. 쿼리 파라미터 처리:
URLSearchParams를 사용하여 쿼리 파라미터에서 id 값 가져오는 형태로 변경
2. 데이터 검색 방식 변경
filter메서드 대신find메서드를 사용하여 조건에 맞는 첫 번째 요소만 검색하도록 변경
3. 포캣몬 존재 여부 확인:
currentPokemon이 존재하지 않을 경우 사용자에게 포켓몬을 찾을 수 없다는 메시지를 표시
등등 수정한 부분들은 main브랜치에 올려두었다.
detail , Detail 파일 경로 설정 잘못됨
error during build:
Could not resolve "../pages/detail" from "src/Shared/Router.jsx"
file: /vercel/path0/src/Shared/Router.jsx
at getRollupError (file:///vercel/path0/node_modules/rollup/dist/es/shared/parseAst.js:392:41)
at error (file:///vercel/path0/node_modules/rollup/dist/es/shared/parseAst.js:388:42)
at ModuleLoader.handleInvalidResolvedId (file:///vercel/path0/node_modules/rollup/dist/es/shared/node-entry.js:19234:24)
at file:///vercel/path0/node_modules/rollup/dist/es/shared/node-entry.js:19194:26
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Error: Command "yarn run build" exited with 1
detail로 경로 잘못 설정되어있는 부분 Detail으로 변경
uuid 의존성 설치되지 않음
[vite]: Rollup could not resolve "uuid" from "/vercel/path0/src/Shared/Context.jsx".
This is unexpected and could lead to your application being broken at runtime.
To explicitly externalize this module, add the following:
npm i 하여 의존성 설치
수정해주었더니 정상적으로 배포가 되었다.
불과 5개월 전 작업했던 코드들이였는데 리팩토링할게 너무 많은데,
다음 5개월 후 다시 봤을때는 또 얼마나 부족해보일지 무섭다
최대한 강의를 듣고 기본을 잡으며 끊임없이 매일 노력해서 부족한것을 채워나가야겠다.