🍰 [Project] Coz shopping
🌊 Coz Shopping
코드스테이츠에서 진행한 일주일간의 솔로 프로젝트로, 메인페이지/상품리스트 페이지/북마크 페이지로 이루어져있는 SPA 웹페이지입니다.
깃허브
https://github.com/dreamogu/fe-sprint-coz-shopping
GitHub - dreamogu/fe-sprint-coz-shopping
Contribute to dreamogu/fe-sprint-coz-shopping development by creating an account on GitHub.
github.com
기술 스택
- JavaScript
- React.js
- Post.css
- Yarn
- redux, react-redux, redux/toolkit
- ESLint
- Prettier
- React-Router-Dom
- React-Icons
- React-Toastify
Tools
- Figma
- Visual Studio Code
구현 요소
공통 컴포넌트
- Header
- DropDown 구현
- Footer
- Product
- 각 타입마다 다른 UI 처리
- Filter
- 데이터를 상품, 카테고리, 기획전, 브랜드 타입별로 나누어주는 컴포넌트
- Modal
- Modal 내에서도 북마크 처리 가능하게 구현
- Toast
- 북마크(또는 해제)시 Toast 출력
- Alert
- 컨텐츠가 비어있는 부분을 사용자에게 안내하기 위한 컴포넌트
페이지
- Main
- 상품 리스트 4개씩와 북마크 리스트 4개씩 출력
- ProductsList
- 무한 스크롤
- 타입별로 필터링
- Bookmark
- 무한 스크롤
- 타입별로 필터링
- etc
- Privacy, TermOfService, NotFound
개발 과정 중 겪은 어려움
1. Git
깃에서 정말 정말 정말 많은 시간을 소비하였다.
우선 아직은 git flow에 대해 제대로 된 학습을 하지 않았기 때문에 main 브랜치를 기반으로 기능 브랜치를 만든 후 계속해서 main으로 pr 및 merge를 하면 크루님들이 코드리뷰를 해주시는 방식으로 이루어졌는데 (일반적인 방식과는 조금 다르다) 초기 셋팅 이후 새 브랜치 생성 후 작업을 진행하고 pr, merge를 진행하고 사용하던 브랜치를 삭제하지 않고 그대로 작업을 진행해버린 것이다. 첫 날 새벽에 동기분들이 크루님들께 질문하는 것을 보고 잘못을 깨닫고 새 브랜치를 만든 후 지금까지 작업한 것을 옮기려 했는데 구글링을 해보니 git stash를 사용하여 현재 작업한 부분을 임시 저장 후, 새 브랜치에 붙혀넣기 방법으로 하면 된다는 글을 보고 실행해보았다. 새 브랜치로 옮겨가며 기존 브랜치는 아예 삭제 하였는데, git stash에 저장된 코딩이 내가 생각한 부분이 저장되고 있던 것이 아니었다. 어떻게 해결해야할 지 막막해서 우선 다시 프로젝트 폴더를 생성하여 처음부터 진행하기로 하였다. 아직 작업을 오래하지 않고 header, footer부분만 진행했기에 가능한 행동이었다. 익숙해질 때 까진 로컬에도 백업을 꼼꼼히 해야겠다고 결심하였다.
2. node_module, DS_Store
1차 코드 리뷰 받기 전에 작업한 code를 push할 때 용량 문제로 push가 되지 않는다는 에러가 떳다. 살펴보니 node_module 폴더가 올라가고 있었다. 초기셋팅 때도 node_module이 올라가 있었다는 것을 알게되고 급하게 gitignore 파일에 추가하였다. 사실, 그때까진 git 작동하는 방식을 착각해서 현재 ignore에 올리면 이미 올라간 원격 저장소에서도 적용이 될 거라고 생각했는데 1차 코드리뷰를 받고 이미 올라간 파일, 폴더는 따로 수정해야한다는 것을 알게 되었다.
검색해서 알아낸 방법으로 폴더를 삭제한 후, 다시 main에 pull을 땡겨왔는데 띠로리.. yarn start를 해도 리액트 서버가 작동하지 않았다. 에러코드를 확인해보니 yarn이 설치되지 않았었기에 다시 yarn install을 하여 해결하였다.
추가로, DS_Store 파일이 나는 커밋할 때 늘.. 올라갔던 파일이라 당연히 리액트에 종속되어있는 파일이라고 생각하였는데 알고보니 애플의 맥 OS X 시스템이 finder로 폴더에 접근할 때 자동으로 생기는 파일이라고 한다. 다시 폴더 내부에 포함되어있는 DS_Store을 삭제하려고 시도하였으나 node_module 폴더를 삭제 할 때 방법으로 시도하면 또 다른 이슈가 생길까봐 다른 방법을 찾아 헤맸으나 원격 저장소에서 삭제하는 방법 밖에 찾지 못하였다. 하지만 이렇게 되면 파일을 삭제할 때 마다 각각의 브랜치가 생성되어 크루분께 조언을 구하였고, 내가 생각한 방법으로 접근해보라고 하셔서 원격에서 삭제를 진행하였다.
그래서 PR내역이.. 보기가 싫어져서 너무 속상하다. 이 점은 나중에 다른 프로젝트를 하면서도 주의를 해야할 것 같다. 추가로 이런 문제가 또 발생하여도 깔끔하게 해결할 수 있는 방법도 찾아야겠다.
3. 상품 컴포넌트
처음에 상품 컴포넌트를 만들었을 때 굉장히 길었다.. 타입마다 다른 걸 출력해야해서 switch case 문을 이용하여 전달받은 Type을 구별하여 출력을 각각 다르게 작성하였는데 이게 작성 당시에도 아 이거 아닌거 같은데.. 라는 생각을 했었다.
case 'Product':
productDisplay = (
<>
<li
key={id}
className={styles.product_item}
onClick={handleModalOpen}
>
<div className={styles.product_img}>
<img
src={image_url}
alt={title}
/>
<button>
<AiFillStar
color='rgba(223, 223, 223, 0.81)'
size='24
'
/>
</button>
</div>
<div className={styles.product_detail}>
<div className={styles.product_title}>
<h3>{title}</h3>
</div>
<div className={styles.product_info}>
<div className={styles.discount}>{discountPercentage}%</div>
<div>{formattedPrice}원</div>
</div>
</div>
</li>
{modal && (
<Modal
title={title}
imgUrl={image_url}
setModal={setModal}
/>
)}
</>
);
break;
// 이렇게 4번 반복..후 productDisplay을 리턴하였던 것을,
// 중복 부분만 ProductInfo 변수에 할당한 후 switch case 하였다.
case 'Product':
productInfo = (
<div className={styles.product_detail}>
<div className={styles.product_title}>
<h3>{title}</h3>
</div>
<div className={styles.product_info}>
<div className={styles.discount}>{discountPercentage}%</div>
<div>{formattedPrice}원</div>
</div>
</div>
);
break;
//이렇게 변경한 후 반환하는 부분을 이렇게 변경하였다.
return (
<>
<li
key={id}
className={styles.product_item}
onClick={handleModalOpen}
>
<div className={styles.product_img}>
<img
src={image_url || brand_image_url}
alt={title || brand_name}
/>
<button>
<AiFillStar
color='rgba(223, 223, 223, 0.81)'
size='24
'
/>
</button>
</div>
{productInfo}
</li>
{modal && (
<Modal
title={title || brand_name}
imgUrl={image_url || brand_image_url}
setModal={setModal}
/>
)}
</>
);
결국은 내용 부분만 switch case 적용하고 중복 부분을 대거 수정하였다.
4. Redux-toolkit, Slice 만들기
나는 처음부터 이 프로젝트를 할 때 Redux-toolkit으로 해야겠다! 결심하였다. 사실 더 좋은 방법들이 있을 수도 있고, 이렇게 작은 웹 페이지에선 오히려 비효율적이었을 수도 있지만, Redux-toolkit을 공부한 후 적용해보고 싶었던 욕심이 있었다. 내가 생각한 방식은 내가 구현해야하는 기능 중 북마크의 상태와 상품들을 불러오는 것만을 Redux-toolkit으로 관리하는 것이었다. 북마크와 상품은 한 페이지가 아닌 여러 페이지에서 출력되고 있으니 props drilling 최소화할 수 있을 것이라 생각하였다.
근데 상품관련 slice를 작업할 때 비동기로 데이터를 불러오게 하였는데 처음에 메인 페이지만을 생각해서 api uri의 쿼리 부분을 변수처리 하지 않고 작성하였다가 페어분께서 지적해주셔서 그 부분을 수정하게 되었다. 사실 어려울 것은 없었지만 넓게 보고 작업을 했어야 했는데 한 부분만을 생각하고 작업했는데 약간의 깨달음?을 얻었다..
그리고 북마크 Slice를 작업할 때, 정말정말정말 많이 고뇌 하였는데, 처음에 생각한 작동 방법은 addBookmark reducer와 removeBookmark reducer을 만들어 각각 전달하는 방식이었다.
//bookmarkSlice.js reducers 부분
reducers: {
addBookmark: (state, action) => {
const product = action.payload;
state.push(product);
// localStorage에 북마크를 저장
localStorage.setItem('bookmarks', JSON.stringify(state));
},
removeBookmark: (state, action) => {
const productId = action.payload;
// 북마크를 제거
state = state.filter((item) => item.id !== productId);
// localStorage에서 북마크를 업데이트합니다.
localStorage.setItem('bookmarks', JSON.stringify(state));
}
하지만 이 방식은, 중복 되는 코드도 있으며 효율적이지 못하다는 생각이 들었다. 고민 끝에 toggle 방식으로 변경 후, 이름도 toggleBookmark로 변경하였다.
reducers: {
toggleBookmark: (state, action) => {
const { payload } = action;
state.isBookmarked = state.isBookmarked.includes(payload)
? state.isBookmarked.filter((id) => id !== payload)
: [...state.isBookmarked, payload];
localStorage.setItem('bookmark', JSON.stringify(state.isBookmarked));
},
사실 이 코드들은 제대로 작동하지 않는 코드들인데, 나는 처음에 localStorage에 상품 데이타 중 id만 저장하여 기존 데이터에서 id가 맞는 것을 맵핑하여 출력해주려고 하였는데, 생각만큼 잘 작동이 되지 않았다. 우선은 컴포넌트의 완성이 우선이라 생각하여 작성했던 코드를 Modal에 적용하려고 하다 문제를 발견하였다. (이 부분은 아래에 서술)
그리고 이 방식으로 하려면 productSlice가 필요 없는 북마크 페이지마저도 데이터를 불러와야 한다는 것을 깨달았다. (사실 더 좋은 방법이 있을 지 모르겠지만) 그래서 결국은 localStorage에 상품 정보를 전체 다 전달하여 저장하기로 결심하였다.
그렇게해서 수정한 코드가 아이디만을 추가하던 기존 코드와는 다른, 해당 객체의 아이디를 포함하는 객체의 배열인지 확인하는 코드를 작성하였다. 만약 이미 객체가 배열에 존재하면 삭제하고, 아니라면 객체를 배열에 추가하는 방법을 사용하였다.
// 완성된 코드
reducers: {
toggleBookmark: (state, action) => {
const { payload } = action;
if (isBookmarked) {
// 이미 북마크된 상품인 경우 제거
state.isBookmarked = state.isBookmarked.filter(
(item) => item.id !== payload.id
);
} else {
// 새로운 상품을 북마크하는 경우 추가
state.isBookmarked.push(payload);
}
localStorage.setItem('bookmark', JSON.stringify(state.isBookmarked));
},
그렇게 완성된 코드로 북마크 페이지 구현 성공 b
5. Modal에 Bookmark 기능 넣기
// 기존 Modal 컴포넌트에 props 전달하기
<Modal
id={id}
title={title || brand_name}
imgUrl={image_url || brand_image_url}
setModal={setModal}
/>
기존 Modal에는 모양만 잡아두고 북마크 기능이 없었다. 그래서 id, title, imgUrl, modal 상태만을 전달하였었는데, 이렇게 하니 modal에서 북마크 버튼을 누를 때 저장할 수 있는 데이터가 id, title, imgUrl, 밖에 없었다. 처음에는 문제가 될 거라고 생각하진 않았다. 내가 처음 작성해둔 bookmarkSlice 코드에선 localStorage에 id만 저장하려고 했으니까.. 그런데 이걸 북마크에서 사용하려고 하니까 이렇게 하면 안 되겠다 느껴서 bookmarkSlice를 수정하게 되었고, bookmark에 어떻게 전체 데이터를 깔끔히 전달할 수 있을까, 고민하게 되었다.
// 만약 각각 전달 했을 때
// Component
<Modal id={id}, ... , follwer={follower}, setModal={setModal}/>
// Modal
const {id, ... , follower, setModal } = props
const handleBookmarkClick = (event) => {
event.stopPropagation();
dispatch(toggleBookmark(id, ... , follower));
};
사실 엄청 부끄러운 고민인데.. 이걸 전체 풀어서 전달하면 Modal에서 구조분해할당을 하면 이렇게 작성이 되니까 코드가 엄청 지저분하다고 느껴졌다. 나는 toggleBookmark에 props 깔끔하게 넣어주고 싶은데 저렇게 풀어서 넣으면 무려 10개나 넣어야 하는 것이다.. Modal 컴포넌트에 데이터를 넣는 것도 setModal 함수를 포함하여 11개를 전달해야 했다. 나중에 아 Modal 컴포넌트에 변수를 따로 할당해서 분리할걸.. 하면서 깨달았지만 당시에는 setModal을 저장하면 안되는데 어떡하지?! 하며 엄청 고민을 했다. (앞서 말한 부끄러운 이유..)
여튼간 문제를 해결을 위해 분노의 구글링을 시전하였다. 그러다 방법을 발견하였는데, Product 컴포넌트에서 변수에 할당하는 것이었다.
그렇게 나는 Product 컴포넌트에서 modalData라는 변수를 만들어 할당하고, 이것을 Modal의 prop으로 전달하여 해결하였다.
// 수정한 코드
// Product 컴포넌트
// ...
const {
id,
type,
title,
sub_title,
brand_name,
price,
discountPercentage,
image_url,
brand_image_url,
follower,
} = props;
// ...
<Modal
setModal={setModal}
modalData={modalData}
/>
// Modal 컴포넌트
// ...
const { modalData, setModal } = props;
const { id, title, brand_name, image_url, brand_image_url } = modalData;
// modalData를 다시 한 번 구조분해할당 하였다.
// Modal 컴포넌트에서 사용하지 않는 데이터는 따로 부르지 않았으나 modalData에 포함되어 있다.
const handleBookmarkClick = (event) => {
event.stopPropagation();
dispatch(toggleBookmark(modalData));
};
아쉬운 점
- 토스트 구현과 무한스크롤을 라이브러리로 구현하였다..
- pr을 기능단위로 하지 않아 엉망진창..
- commit 메세지를 깔끔히 하지 않은 것 같다.
- git flow를 따르지 않았다.
느낀점
정말 일주일 간 천국은 간 적 없고 지옥만 왔다 갔다한 것 같다. 여러 가지 오류와 마주했는데 구글링과 GPT의 도움을 여러 차례 받아서 완성은 하였지만 현재 자신감이 떨어져 있는 상태다. 그래도 이런 과정을 겪으면서 계속해서 깨달아가는 점이 있는 것 같다. 추욱 쳐지는건 이제 그만하고 준비해둔 유튜브 클론 코딩을 할 예정이다. (근데 난 이 회고를 지금 하루 종일 쓰고 있다.)