코드스테이츠 부트캠프에서 솔로프로젝트로 쇼핑몰 북마크 웹 페이지 구현하였다.
전체적인 디자인은 Figma 를 통해 시안을 보여주었고 그에 맞춰 단계마다 진행하였다.
데이터는 Swagger API 문서를 이용하였다.
첫 렌더링시,
메인 페이지
에는 헤더와 상품 리스트와 북마크 리스트가 나타난다.
헤더의 좌측 로고와 타이틀 클릭시 첫 렌더링된 메인페이지로 이동된다. 각각의 상품의 사진을 클릭시 모달창이 열고 닫을 수 있다.상품의 북마크 버튼을 클릭시 북마크 리스트에 상품이 담긴다. 담긴 상품을 다시 한번 북마크 버튼을 클릭시 북마크 리스트에서 삭제된다. 헤더의 우측 햄버거 버튼을 클릭시 상품 페이지와 북마크 페이지로 이동할 수 있는 드롭메뉴가 나타난다.
상품 리스트 페이지
에는 상품들의 타입에 맞게 필터 기능을 통해 렌더링되는 화면을 볼 수 있고, 북마크 리스트 페이지
에는 북마크된 상품들이 보여지고 마찬가지로 타입별 필터 기능을 가진다.
1. 라우팅
2. 모달창
event.stopPropagation()
이용해 버블링을 방지3. 타입별 상품 데이터 렌더링
4. 북마크 기능
5. 토스트 기능
App.js
에서 ToastContainer
컴포넌트를 위치시키고 북마크 상태에 따라 토스트 컴포넌트의 autoClose
속성을 이용해 설정1.styled 파일 분리
styled-components
라이브러리를 사용하면서 컴포넌트 파일 내에 함께 구현을 했다. style 파일을 따로 만들지 않은 이유는 왔다갔다 하지않고 한 파일 내에서 쓰기 쉽게 하기 위함이었는데, 점점 구현한 컴포넌트가 증가하면서 styled 파일을 따로 분리하는 것이 가독성에서도 개발자의 환경에서도 좋다고 하셔서 따로 styled 폴더를 만들어 관리하였다.
2.inline style 보다는 styled 로 작성
return(
<BgContainer style={{ backgroundImage: `url(${card.image_url})`}} />
)
처음 코드를 구현할 때 props 로 받아온 cards.image_url
을 사용하기 위해 inline style 을 사용하여 배경화면 이미지를 구현하였다. 하지만 inline style 을 사용할 경우 가독성이 떨어질 뿐만 아니라 유지보수가 용이하지 않다는 단점이 있다고 하셨다.
inline style 로 쓴 이유가 있다면, props의 속성값을 사용하기 위해서 따로 styled-components 내에 쓰지 않았던 것인데 어떻게 하면 저 정보를 스타일로 전달할 수 있을까를 생각을 해보았다. 바보같은 생각이었다. styled-components 를 사용하는데 왜 저 정보를 이용할 수 없다고 생각을 한 것일까?!
다음과 같이 inline style 대신 url
속성에 props의 image.url 정보를 넘겨주었고, styled.js 파일에서 background-image: url(${(props) => props.url});
속성값을 이렇게 설정해주었다.
//Item.js
<ItemImage url={`${data.image_url || data.brand_image_url}`}/>
//Item.styled.js
export const ItemImage = styled.div`
...
background-image: url(${(props) => props.url});
...
`;
3.상품 렌더링
메인 페이지에서는 전체 상품 중에서 상품리스트와 북마크 리스트엔 각각 4개의 상품만 렌더링하게 되어있다.
상품 리스트에서 북마크를 했을 때 1개와 4개는 완전한 상태로 보이지만 2개, 3개인 경우에는 배치가 위와 같이 엉망으로 되었었다.
그 때의 나의 코드는 display:flex; justifyc-content: space-between;
으로 각각의 아이템이 균등하게 배치되면 된다고 생각했다. 사실상 코드는 내가 생각한 그대로 균등하게 배치된 채로 렌더링이 된다. 그치만 나는 3개여도 4개처럼 마지막 1개가 없이 보이고 싶어한 것이다.
그때 코드 리뷰를 받고 강사님께서 gap
이라는 속성을 이용해보라고 하셨다. 부모요소에서 gap
속성을 주어 각각의 아이템의 간격만 조정해보라는 것이었다.
너무 justify-content
속성을 자주 사용하다보니 다른 것들은 미처 보지도 생각하지도 않았던 것 같다.
4.전역 변수로 관리
4-1. 3번에서와 같이 메인페이지에서는 4개의 상품만 렌더링하기 위해 전체 데이터를 API로 불러와 상태 배열로 저장해놓고 slice(0,4)
메서드를 이용하였다.
items
.filter((item) => {
.filter((item) => {
return bookmark && bookmark.includes(item.id);
})
.slice(0, 4)
코드 리뷰에서 강사님이 4와 같은 수는 코드에 그대로 심게 되면 여러명의 개발자들은 무엇을 의미하는지 단번에 알 수 없을 뿐더러, 추후 저 숫자가 변경되었을 경우 일일이 수정해줘야 하는 유지보수에 있어서 문제가 된다고 말씀해주셨다.
const favoriteCount = 4;
따라서 엄청나게 나아지진 않았지만, 바깥으로 빼서 변수화를 시키고 참조하는 형태로, 그리고 네이밍도 한번에 파악할 수 있게 변경하는 것이 낫겠다고 생각했다.
4-2. 상품을 렌더링할 때, 타입이 네가지로 분류가 된다고 앞서 언급했고 타입이 중요한 이유는 이에 따라 다음과 같이 다른 UI로 렌더링 해야하는 이슈가 있다.
그래서 꼭 타입별로 나누어서 코드를 작성해야 했고, 내가 선택한 방법은 switch를 이용해 string 으로 관리했다.
const showCard = (item) => {
switch (item.type) {
case "Product": ...;
case "Exhibition": ...;
case "Brand": ...;
case "Category": ...;
default: ...;
}
}
그치만 string 으로 관리해야할 경우, 위와같이 일일이 변경해야하는 경우가 생긴다.
//type.js
export const types = {
ALL: 'ALL',
PRODUCT: 'Product',
CATEGORY: 'Category',
EXHIBITION: 'Exhibition',
BRAND: 'Brand'
};
각각의 타입을 상수화 시켜 하나의 파일을 만들어주어 각각의 컴포넌트에서 사용할 때마다 import 하는 형식을 이용했다.
5.Redux toolkit
Redux 를 배웠지만 로컬스토리지와 전역 저장소를 어떻게 연결하여 관리를 해야하는지 머릿속이 복잡했다. 일단 Redux 가 어려웠던 이유는 Actions, Dispatch, Reducer, Store 를 각각 만들고자 하니 시작이 어렵다고 생각하니 시도하기가 무서워졌다. 일단 다시한번 복기를 해보자 생각하고, redux 에 대한 구글링과 유튜브를 찾아보았다. 다행히 나와 같이 생각하는 부분이 있어서 redux 를 더 쉽게 사용할 수 있는 라이브러리인 redux toolkit 이 있었다. 낯설긴 했지만 확실히 덜 복잡해보이긴 했다. 예제를 이해하고 활용하기 까지는 꽤 오래걸려 블로깅도 따로 해보았다.
https://velog.io/@jeongjwon/Redux-Toolkit
간단히 말하면, createSlice
로 전역으로 쓸 state 를 만들어주는데, 상태를 만들어줄 때 name
, initialState
, reducers
를 지정해주고 reducers
에 상태를 변경해줄 함수를 만들어주는 것이다.
사용할 컴포넌트에서 useSelector
를 통해 state 를 가져오고 useDispatch
로 state 를 변경해줄 수 있는 함수에 접근해준다.
⭐️ 헷갈렸던 것은 state를 만들 때 reducers
과 저장소에서 지정해준 reducer
속성값 차이이다. ⭐️
전역변수로 사용해야하는 것은 모달과 북마크 기능이다.
모달 구현은 다음과 같다.
모달은 열고 닫기의 기능만 있으면 되기 때문에, 초기 상태값을 boolean
타입의 isModalOpen
과 모달을 열 때 상품의 이미지 주소나 제목 등 전달해줘야 하는 정보값 action.payload
함수를 포함하는 showModal
을 설정해준다.
//store/modal.js
import { createSlice } from '@reduxjs/toolkit';
const initialModalState = {
isModalOpen:false,showModal:{},
}
const modalSlice = createSlice({
name :'openingModal',
initialState:initialModalState,
reducers:{
open(state){
state.isModalOpen = true;
},
close(state){
state.isModalOpen = false;
},
showModal(state,action){
state.showModal = action.payload;
}
}
})
export const modalActions = modalSlice.actions;
export default modalSlice.reducer;
//App.js
import { useSelector, useDispatch } from "react-redux";
import { modalActions } from "../store/modal";
export const App = () => {
const isModal = useSelector((state) => state.modal.isModalOpen);
//모달을 열고 닫기의 상태값 지정
return(
<>
{isModal && <Modal />}
</>
)
//Item.js
import { useSelector, useDispatch } from "react-redux";
import {modalActions} from "../store/modal";
export const Item = ({ data }) => {
const dispatch = useDispatch();
const handleOpenModal =() => {
//사진을 클릭했을 떄의 이벤트
//dispatch 로 모달 액션의 reducer에 접근
//모달을 열고 open, 정보를 전달 showModal(data)
dispatch(modalActions.open());
dispatch(modalActions.showModal(data));
}
return(
...
<ItemWrapper onClick={handleOpenModal}>... </ItemWrapper>
...
)
}
⭐️ 아무래도 북마크 구현이 상당히 애를 먹었다. 모달은 일시적인 상태이지만 북마크는 새로고침이 되고 페이지가 바뀌어도 북마크된 상품들은 그대로 보존되어 있어야 한다. 이는 전역으로 관리를 하면서도 로컬 스토리지에 저장하고 있어야한다. ⭐️
초기 설정값은 로컬 스토리지에서 bookmark
라는 key 에 해당하는 value 들(id값)을 가져오는데, 만약 없다면 빈배열로 가져온다. reducer 에는 toggleBookmark함수에 state 와 action 을 파라미터로 받아온다. state 는 앞서 말한 로컬스토리지에서 받다온 id value 값들이 전달받은 action.payload (= id) 이 존재한다면 이미 북마크 되었다는 것이므로 삭제한다는 것이고, 없다면 추가한다는 것이다. 따라서 그에 맞는 toast 알림을 전해준다. 그리고 둘다 마무리는 다시 로컬스토리지에 설정을 해준다.
북마크 기능을 사용할 Item 컴포넌트에서 상태값을 가져오는데, 배열로 가져오는 것이 아니라 포함되냐 안되냐의 boolean 값으로 받아온 isMarked 로 북마크 상태를 표시한다.
//store/bookmark.js
import { createSlice } from "@reduxjs/toolkit";
import { toast } from "react-toastify";
import { styled } from "styled-components";
// import Toast from "../components/Toast";
import bookmarkIconOff from "../assets/bookmark_off.png";
import bookmarkIconOn from "../assets/bookmark_on.png";
// Toast 구현*****************************
export const ToastBox = styled.div`
display: flex;
align-items: center;
> img {
margin-right: 0.5rem;
}
> span{
color: black;
}
`;
const Toast = ({ text, image}) => {
return (
<ToastBox>
<img src={image} alt='toast' />
<span>{text}</span>
</ToastBox>
);
};
//**************************************
//초기 설정값
const initialbookmarkState =
localStorage.getItem("bookmark")
? JSON.parse(localStorage.getItem("bookmark"))
: [];
const bookmarkSlice = createSlice({
name:"bookmark",
initialState: initialbookmarkState,
reducers: {
toggleBookmark(state, action) {
if(state.includes(action.payload)){
state.splice(state.indexOf(action.payload),1); //있으면 삭제
toast(<Toast
text='상품이 북마크에서 제거되었습니다.'
image={`${bookmarkIconOff}`}/>);
}else{
state.push(action.payload); //없으면 추가
toast(<Toast
text='상품이 북마크에서 추가되었습니다.'
image={`${bookmarkIconOn}`}/>);
}
//로컬스토리에 다시 세팅
localStorage.setItem("bookmark", JSON.stringify(state));
}
}
});
export const bookmarkActions = bookmarkSlice.actions;
export default bookmarkSlice.reducer;
//Item.js
import { useSelector, useDispatch } from "react-redux";
import { bookmarkActions } from "../store/bookmark";
export const Item = ({data}) => {
const dispatch = useDispatch();
const isMarked = useSelector((state) => state.bookmark.includes(data.id));
const handleBookmark = (e) =>{
e.stopPropagation();
dispatch(bookmarkActions.toggleBookmark(data.id));
}
return(...)
}
//store.js
import { configureStore } from "@reduxjs/toolkit";
import modalReducer from "./modal";
import bookmarkReducer from "./bookmark"
const store = configureStore({
reducer: {modal: modalReducer, bookmark:bookmarkReducer}
});
export default store;
두 모달과 북마크를 관리하는 저장소는 다음과 같다. 만들어진 각각의 state 를 configureStore
로 store 에 등록하여 사용해주도록한다.
깃허브 관리
깃허브는 사용하지 않으면 항상 다 까먹는다. 마치 알고리즘과 같은 문제이긴하다. 항상 로컬에서만 작업을 해서 브랜치 분기로 작업하는 것은 참 익숙하지 않았다. 더구나 솔로 프로젝트 첫 수업부터 휴가를 쓰는 바람에 시작을 어떻게 해야하는지, 깃은 어떻게 사용해야하는지 난감했다.
main 기반에서 별도의 브랜치를 각 기능에 맞게 추가로 생성해서 구현한 후에 원결 브랜치에 push 한 후에 PR 을 하번 코드리뷰나 병합을 할 수 있다. 사실 이 부분이 어려웠던 이유는 항상 혼자만 작업을 해서 익숙치 않아 다시 저장소를 지워야하는 것이 아닌지 도전하기가 겁났다. 그래서 이번에는 다행히 솔로 프로젝트라 dev 라는 브랜치 하나만 생성해서 작업을 했지만, 다음 프로젝트를 위해서는 브랜치 작업 연습과 깃 커밋 메시지에서도 익숙해져야할 필요가 있을 것 같다.
혼자 해보다가 도움이 많이 된 레퍼런스
2. 라이브러리 활용
무작정 토스트 컴포넌트를 만들었고, 토스트라는 것은 북마크 버튼을 클릭하고 저절로 화면에 렌더링되고 사라져야하는 것이다. 한번의 클릭으로 나타나고 사라지는 것을 어떻게 구현할 수 있나 고민을 많이 했다. 그때 비동기에서 배웠던 setTimeout()
과 clearTimeout()
를 사용하면 되겠다 싶었다.
const [showToast, setShowToast] = useState(false);
const activeToast = () => {
setShowToast(true);
let timer = setTimeout(() => {
setShowToast(false);
}, 3000);
return () => {
clearTimeout(timer);
}
}
showToast 라는 상태 함수로 초기값은 false 로 지정해두다 setTimeout()
으로 3000ms 만 showToast 상태값을 바꿔주고, 그 후 다시 clearTimeout()
으로 타이머를 삭제하는 activeToast 함수를 만들었다. 그리고 사진을 클릭했을 때 activeToast 이벤트를 지정해주었다.
하지만, redux toolkit 을 공부하고 react-toastify 라는 간단한 옵션들을 지정해주고 토스트 알람을 구현하는 라이브러리를 알게되었다. 나름 복잡하다면 복잡했던 토스트 구현을 존재하는 라이브러리를 통해 조금 더 쉽고 빠르게 구현할 수 있다는 점에서 소중함을 알게 되었다.
⭐️ 부트캠프에서 첫 솔로 프로젝트를 진행하였다. 엄청 큰 규모의 작업들은 아니었지만, 그래도 이것저것 로컬스토리지와 리덕스를 사용하는데 있어서 많이 허덕이고 오랜만에 코딩 꿈도 꾸며 스트레스도 많으면서 작업을 진행했던 것 같다. 많이 힘들고 배우는데 있어서 재미도 있었지만 아무래도 크게 얻은 것은 조금이나마의 재미와 열정 덕분에 끝낼 수 있었지 않나 싶다. 아직 기능구현을 하지못한 '무한 스크롤'도 조만간 작업을 진행해봐야겠다.