Shopping App 회고 구현하기
개요
- 장바구니 기능이 있는 상품페이지를 구현합니다. 기본적인 디자인은 해당 링크에서 확인할 수 있습니다. 해당 프로젝트의 목적은 주어진 디자인에 맞추어 기능을 구현하는 방법론을 학습합니다.
- 에자일 방법론 중 스크럼 방법을 활용해서 진행합니다. github의 프로젝트를 활용했습니다. 링크
- 디자인 링크
진행 과정 정리
1. Router 설정
- 기존 CRA로 형성한 react 템플릿에서는 BrowserRouter로 감싸는 형태로 작업하였을 때 정상적으로 출력이 되었지만, vite 템플릿에서는 작동이 되지 않아서 공식 홈페이지 듀토리얼을 참고하면서 다시 진행하였습니다.
- 공식 블로그
- 4개의 페이지를 만들어서 기본 router 구성 Home, ProductList, Bookmark, NotFound
- BrowserRouter와 createBrowserRouter의 차이에 대해서 이해할 수 있었다. createBrowserRouter에서는 DataRoiter를 제공하기 때문에 더 많은 기능을 사용할 수 있었다. (ex. data APIs) 따라서 NotFound 페이지에서 사용하고 있는 useRouteError 훅은 createBrowserRouter에서만 사용가능하였다. 링크
- App을 절대경로로 설정하여 Router 작업을 진행하였습니다. 초기에 createBrowserRouter에서 제공하는 DataAPIs가 매우 매력적으로 다가왔습니다. 특히 DataAPIs가 제공하는 여러 훅중에서 useRouteError가 매력적이었습니다. 설정에 따라서 router 애러 뿐만 아니라, 다른 애러도 캡쳐해서 페이지를 자동으로 전환 해주는 기능이 인상깊었습니다. 이러한 APU를 사용하기 위해서 createBrowserRouter를 적용하였습니다.
- 여기서 문제는 createBrowserRouter를 사용한 경우 Outlet과 함께 사용되는데, 저는 각각의 페이지에 prop전달을 하고 싶어서 Routes와 Route를 활용했습니다.
// ./routers/Routers.jsx
import { createBrowserRouter } from 'react-router-dom';
import NotFound from '../pages/NotFound';
import ProductLists from '../pages/ProductLists';
import Bookmark from '../pages/Bookmark';
import App from '../App';
import Home from '../pages/Home';
const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{
path: '/',
element: <Home />,
},
{
path: '/products/list',
element: <ProductLists />,
},
{
path: '/bookmark',
element: <Bookmark />,
},
],
errorElement: <NotFound />,
},
]);
export default router;// ./App.jsx
import { Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import Footer from './components/Footer';
import Home from './pages/Home';
import ProductLists from './pages/ProductLists';
import Bookmark from './pages/Bookmark';
import useHeaderClick from './hook/useHeaderClick';
import NotFound from './pages/NotFound';
import useApi from './hook/useApi';
import { useDispatch } from 'react-redux';
import { dispatchData } from './redux/action/actions';
import { useEffect, useState } from 'react';
import Toast from './components/Toast';
function App() {
// header 클릭 적용하기
const { click, setClick, handleClick } = useHeaderClick();
//api 불러오기
const { data, isLoading } = useApi();
//Toast 조정
const [toast, setToast] = useState(false);
const [toastBookmar, setToastBookmark] = useState(false);
useEffect(() => {
const id = setTimeout(() => {
setToast(false);
}, 1000);
return () => {
clearTimeout(id);
};
}, [toast, toastBookmar]);
//query 데이터 redux에 저장하기
const dispatch = useDispatch();
useEffect(() => {
dispatch(dispatchData(data));
}, []);
return (
<>
<Header click={click} setClick={setClick} handleClick={handleClick} />
<Routes>
<Route
path="*"
element={
<Home
handleClick={handleClick}
isLoading={isLoading}
setToast={setToast}
setToastBookmark={setToastBookmark}
/>
}
/>
<Route
path="/products/list"
element={
<ProductLists
handleClick={handleClick}
setToast={setToast}
setToastBookmark={setToastBookmark}
/>
}
/>
<Route
path="/bookmark"
element={
<Bookmark
handleClick={handleClick}
setToast={setToast}
setToastBookmark={setToastBookmark}
/>
}
/>
<Route path="/*" element={<NotFound />} />
</Routes>
{toast && <Toast toastBookmar={toastBookmar} />}
<Footer handleClick={handleClick} />
</>
);
}
export default App;💣 다음과 같이 설정 하였을 때, 콘솔에서 다음과 같은 경고창을 보여지게 됩니다.
history.ts:487 You rendered descendant <Routes> (or called `useRoutes()`) at "/" (under <Route path="/">) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render.
Please change the parent <Route path="/"> to <Route path="*">.- 제시한 해결방법 처럼 path를 수정한 상태에서도 이와 같은 문제가 발생하고 있습니다. createBrowserRouter와 Routes를 혼합해서 사용한 문제 인듯 하여, 하나의 방식만 사용하도록 수정이 필요할 것 같습니다.
2. 기본 CSS 설정
- styledComponent의 전역 스타일 설정하기,
createGlobalStyle를 활용했습니다. 그리고 reset css를 적용했습니다. reset css - styledComponent Theme을 활용해서 기본적으로 사용하는 폰트의 크기, 각 margin 값, color를 설정했습니다.
- reset css로 인해 변화된 NotFound Page의 css를 수정했습니다.
3. 헤더 컴포넌트 만들기
- 헤더 컴포넌트를 작성했습니다. 최대한 피그마에서 작성한 디자인과 유사하게 만들도록 노력했습니다.
- icon 같은 경우 svg 와 fontawsome 아이콘을 함께 활용해서 icon을 활용했습니다.
- 말풍선 같은 경우는 다음 블로그를 참고했습니다. 링크
- 헤더 컴포넌트의 햄버거를 클릭하면 Dropdown 형태로 메뉴가 등장해야 합니다. 이때 햄버거 버튼이 클릭 되어야 햄버거가 사라지게 되는 형태가 불편하다고 느꼈습니다. 그래서 전역에서 state를 감지해서 사용할 수 있도록 수정했습니다. component hook useHeaderClick을 만들어서 App 상에서 state를 관리하도록 하였습니다. 햄버거 버튼이 클릭 되어 DropDown 컴포넌트가 보이는 상태라면, 어떤 화면을 클릭해도 다시 닫히도록 구성했습니다.
4-1. API 가지고 오기
- react-query를 활용해서 API 데이터를 불러왔습니다.
- 초기에는 api 함수만을 따로 분리하였는데, 데이터를 불러와서 결과를 받는 hook 또한 분리하는 것이 좋을 듯 하여 useApi component hook을 만들었습니다.
4-2. Card 컴포넌트 만들기
- Card Component를 만들었습니다. API의 타입 속성에 따라 설명 모양에 차이가 있어서 Description.jsx 컴포넌트에서 분기 처리를 하여 렌더링하도록 작성했습니다.
- 가격과 follower 같은 경우는 쉼표 처리를 해주어야 하기 때문에 정규표현식을 활용했습니다.
toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
4-3. 모달 만들기
- 모달 만들기는 예전에 만든 Modal component를 참고했습니다. Modal Link
5.1 redux를 통한 상태관리 ,청사진 구상하기
- 우선 rtk query를 사용하지 않고, react query를 활용하고자 한다. 따라서 react query로 받아온 데이터를 redux의 데이터로 구성하는 action을 만들어야 한다. 더불어 data를 받아 올 때, 데이터 내에서 bookmark에 관한 정보가 없기 때문에, bookmark 프로퍼티를 추가해주어야 한다.
- 먼저 redux-toolkit을 활용해서 코드를 작성하였다. 총 3개의 action을 수행하도록 작성하였다. bookmark 추가 기능과 bookmark 해제 기능, react-query에서 받아온 데이터를 초기 데이터로 설정하는 것이다.
- 이때 어디서 react query로 받아온 데이터를 처리해서 redux로 보내주는가가 문제 였다. 이를 해결하기 위해 가장 최상위인 App에서 데이터를 받아오고 이를 dispatch하도록 구상하였다.
5.2 작성 중 마주친 문제점
- bookmark 컴포넌트를 만들고, useEffect를 활용해서 App.jsx에서 최초로 API 데이터를 redux로 보내도록 설정하였다. 이때 발생하는 문제는, 페이지를 새로고침 하였을때, react query에서 cashe된 데이터를 활용하기에 새롭게 보내는 데이터가 없다는 것을 확인할 수 있었다. 또한 매번 새로 고침 할때마다 데이터가 초기화 되기 때문에 북마크 기능이 소용이 없었다. 이를 해결하기 위해 localstorage를 활용했다.
5.3 redux-persist 적용하기 참고
- redux 과련 라이브러리 중에서 local storage를 적용해주는 라이브러리를 사용해서 코드를 작성했다. 이때 redux-toolkit으로 작성한 코드를 적용하는 과정에서 애러가 지속적으로 발생해, toolkit을 활용하지 않고 일반 redux와 react-redux를 활용하는 코드로 수정하였다.(아직 toolkit에 익숙하지 않아 어디서 발생하는 오류인지 찾을 수 없어 코드를 전체적으로 수정했다.!) 추가적으로 localStorage에 저장된 데이터가 있다면, react-query 데이터를 활용하지 않고, localStorage 데이터를 활용하도록 initialvalue를 수정하였다.
- 정상 작동을 테스트 해보니 작동은 정상적으로 하였다. 다만 localStorage를 적용하니 메인 페이지의 상품페이지가 고정된 상품만 보여주게 되어 이를 수정하기 위해 random으로 숫자를 뽑는 함수를 작성했다.
- redux에서 actions을 받고 reducer을 하는 과정에서 store에 저장된 데이터가 변경되어 특정 과정에서 애러가 발생하는 것을 확인했습니다. 이를 수정하기 위해서 redux-persist가 저장하는 데이터 형태로 state의 데이터 형태를 수정하였으며, 각각의 action이 반환하는 데이터의 형태도 일치시켰습니다.
6. 북마크 페이지 & 상품페이지
- 두 페이지의 형태는 데이터를 보여주는 방식만 다를 뿐 모두 똑같이 작성했습니다. 다만 같은 코드가 너무 똑같이 반복되어 모든 기능을 구현후에 코드 리펙토링을 해야 할 것 같습니다.
- 또한 numState의 상태에 따라 화면에 뿌려줄 데이터를 추렸습니다. 이 과정에서도 같은 코드를 5번이나 반복해서 역시 이후에 리펙토링이 필요할 것 같습니다.
7. Toast UI 페이지 구성
- 처음 생각한 방식은 redux를 통한 toastUI 구성이었습니다. 마침 CH3 redux를 학습할 당시에 예시 코드에 redux middleware 중 하나인 thunk를 활용하여 toast ui작업을 한 코드가 있어서 이를 리펙토링 하여 작업을 하면 될 것 같았습니다.
- 하지만 redux를 활용하기 위해서 크게 두 가지 문제가 있었습니다. 첫 번째로 하나의 기능 구현을 위해서 새로운 reducer를 생성하고 전역 변수로 상태를 관리하는게 과연 맞을까? 라는 의문을 가지게 되었습니다. 하나의 기능을 위해 구현할 부가적인 코드가 너무나 많아진다고 생각하였습니다. 또한 현재 구현하는 APP의 규모가 그리 크지 않기 때문에 굳이 toast를 비동기로 처리해야 할 필요를 느끼지 못했습니다.
- 그래서 useState와 useEffect를 활용하여 Toast UI를 작업하도록 하였습니다.
- 처음에는 Bookmark 컴포넌트에 Toast 컴포넌트를 작성하여 작업을 하려 했습니다. 하지만 이때 각 Card 컴포넌트 별로 Toast 컴포넌트의 위치가 잡혀지고 최대 100개의 Toast 컴포넌트가 생성된다는 것을 확인할 수 있었습니다. 이는 고정된 하나의 position을 잡는 것에 문제가 있었습니다. 그래서 Toast 컴포넌트의 위치를 APP에 위치하여 하나의 컴포넌트 만을 state 처리를 통해 불러오도록 하였습니다.
- 총 두개의 state를 활용하였습니다. toastBookmark와 toast state를 활용하였습니다. toastBookmar는 보여주어야 할 Toast의 형태를 결정하는 state이고 toast state는 toast를 화면에 렌더링을 트리거하는 state로 작동하도록 구성하였습니다.
- 이후 useEffect를 통해 setTimeout을 설정해서 작업을 하였습니다. 의존성 배열에 toastBookmark와 toast를 추가하여 해당 값이 변경될 때 마다 useState가 실행되도록 작성하였습니다.
8. 무한 스크롤 구현
- "react-intersection-observer"를 통해 구현하였습니다.
- 화면 간의 이동중, 불러온 무한 스크롤이 유지 되지 않는 문제를 해결해야 할 것 같습니다.
Content Table
- 개요
- 진행 과정 정리
- - 1. Router 설정
- - 2. 기본 CSS 설정
- - 3. 헤더 컴포넌트 만들기
- - 4-1. API 가지고 오기
- - 4-2. Card 컴포넌트 만들기
- - 4-3. 모달 만들기
- - 5.1 redux를 통한 상태관리 ,청사진 구상하기
- - 5.2 작성 중 마주친 문제점
- - 5.3 redux-persist 적용하기 [참고]
- - 6. 북마크 페이지 & 상품페이지
- - 7. Toast UI 페이지 구성
- - 8. 무한 스크롤 구현
Solo-project
2023.05.18
2 / 3