개발기간 : 2022/8/29 ~ 2022/9/8
개발 인원 : 프론트엔드 4명, 백엔드 2명
먼저 첫 회의때 진행한 추천 사이트에서 React학습시 만들어 보았던 인스타그램이나 소셜SNS처럼 피드 & 댓글 , 로그인 기능으로 구현할 사이트를 정할지 Basic이였던 E-commerce를 할지 고민을 했다.
지난 학습을 복습하는 것도 좋지만 해보지 않았지만 우리가 학습한 커리큘럼을 베이스로 조금만 욕심을 더 부리자는 생각에 E-commerce를 선정하였고 여러 후보 중 ALESSI로 하게 되었다.
언어 : React js, javascript
Style : sass, styled-component
Community Tools : Notion, Zep, Zoom, Slack
Version Control Tool : Git
메인페이지
제품 페이지 , 상세페이지
로그인, 회원가입, 장바구니, 검색 페이지
[Main] : 메인 페이지 내에 섹션별 컴포넌트화 작업
[상세 페이지] : 상세 페이지 내에 구매부분과 상품문의/후기 컴포넌트화 작업
[Nav bar] : 레이아웃, 스타일링, mock-up데이터 카테고리 연결후 페이지네이션
[Main] 이미지 슬라이드 : next & prev버튼, Dot, 무한 슬라이드
[Nav bar] : mock-up데이터를 이용하여 hover시 서브메뉴 UI구현
[Main] Section : 신제품 및 추천 제품 섹션 별 제품 card 구현,
[상세페이지] 상세페이지 내 상품 후기 : token을 이용하여 제품 구매 user일 경우 리뷰작성 또는 해당 user의 글을 삭제 (fetch함수와 POST & DELETE & GET메서드 이용),
[상세페이지] 상세페이지 내 상품 문의 : fetch함수를 이용하여 POST메서드로 문의글 등록 및 DELETE메서드로 user의 token을 이용한 게시글 삭제, GET메서드를 이용하여 리스트구현
: 이미지를 imgSlider로 객체형 데이터로 만들고, 이미지의 index를 state로 담아 슬라이드를 구현한다. setInterval을 이용하여 무한 슬라이드구현.
이미지 슬라이드구조
function Slider() {
const [activeIndex, setActiveIndex] = useState(0);
...
return (
<>
<SliderContent activeIndex={activeIndex} imgSlider={imgSlider} />
<Buttons/>
<Dots/>
</>
);
}
export default Slider;
const imgSlider = [
{
title: "ALE6IX",
url: "http://alessi.co.kr/_dj/img/mainbanner/2103-1.gif",
},
...
{
url: "http://alessi.co.kr/_dj/img/mainbanner/2101-5.jpg",
},
];
function SliderContent({activeIndex,imgSlider}){
return(
<>
{imgSlider.map((slide,index)=>(
<div key={index}>
<img src={slide.url} alt="img"/>
</div>
))}
</>
)
}
export default SliderContent;
삼항연산자를 이용한 Slide Button과 Dots구현
function Slider() {
...
const prevSlide = () => {
setActiveIndex(activeIndex < 1 ? imgSlider.length - 1 : activeIndex - 1);
};
const nextSlide = () => {
setActiveIndex(activeIndex === imgSlider.length - 1 ? 0 : activeIndex + 1);
};
return (
<>
<SliderContent activeIndex={activeIndex} imgSlider={imgSlider} />
<Buttons prevSlide={prevSlide} nextSlide={nextSlide} />
<Dots/>
</div>
);
}
export default Slider;
- Dots컴포넌트에 이미지state를 담고 onClick이벤트에 들어가는 함수를 담는다.
activeIndex={activeIndex}
imgSlider={imgSlider}
onclick={(activeIndex) => setActiveIndex(activeIndex)}
function Dots({activeIndex, onclick, imgSlider}){
return (
<div className="all-dots">
{imgSlider.map((slide, index) => (
<span key={index} className={`${activeIndex === index
? "dot active-dot"
: "dot" }`}
onClick={()=>onclick(index)}
></span>
))}
</div>
)
}
export default Dots;
- setInterval 무한슬라이드
useEffect(() => {
const interval = setInterval(() => {
setActiveIndex(
activeIndex === imgSlider.length - 1 ? 0 : activeIndex + 1
);
}, 3000);
return () => clearInterval(interval);
}, [activeIndex]);
시현 영상
: 데이터를 호출받고 state로 담아준후 map()메서드를 이용하여 필요한 데이터를 반환한다.
function Section(){
const [productData,setProductData] = useState([]);
useEffect(()=>{
fetch('/data/recommandList.json')
.then((res)=>res.json())
.then((data)=>{
setProductData(data.recommendProductList)
})
})
return(
<>
<h3>신상품</h3>
<span>알레시에서 꾸준한 사랑을 받는 베스트셀러</span>
<ul>
{productData.map((itemlist,index)=>{
return <li className="prd-box"
key={index}
>
<figure>
<img src={itemlist.main_image_url} alt="item" />\
<figcaption>
<dl>
<dt>{itemlist.product_name}</dt>
<dd>{Number(itemlist.price).toLocaleString()}<span>won</span></dd>
</dl>
</figcaption>
</figure>
</li>
})}
</ul>
</>
)
}
export default Section;
구현 이미지
같은 컴포넌트 내에 섹션별 데이터를 담고 싶었는데 생각대로 로직이 짜여지지 않아 컴포넌트를 분리하여 각각 데이터를 담아서 작업하였다. 추후 리팩토링 할때 한 컴포넌트를 재사용하는 방법은 고민해봐야겠다.
: 마우스 hover시 서브메뉴가 보이고 카테고리에 모든 메뉴들은 fetch함수로 받은 mock-up데이터로 구현하였다.
function Header() {
const [nav, setNav] = useState([]);
useEffect(() => {
fetch("/data/navCategories.json")
.then((res) => res.json())
.then((data) => {
setNav(data.categoryData);
});
}, []);
return (
<>
<ul>
{nav.map((navlist) => {
return (
<li key={navlist.LargeCategoryId}>
{navlist.LargeCategoryName}
<div>
<SubNav
largeCateId={navlist.LargeCategoryId}
sublist={navlist.smallCategories}
/></div>
</li>
);
})}
</ul>
</>
);
}
export default Header;
function SubNav({ largeCateId, sublist }) {
return (
<>
<ul>
{sublist.map((subNav) => {
return (
<li key={subNav.smallCategoryId}>
<NavLink
to={"/product/" + largeCateId + "/" + subNav.smallCategoryId}
>
{subNav.smallCategoryName}
</NavLink>
</li>
);
})}
</ul>
</>
);
}
export default SubNav;
- Router에 지정된 NavLink경로
<Route
path="/product/:bigId/:smallId"
element={}
/>
상세페이지 내에 상품문의, 후기를 컴포넌트화하여 작업하였다.
- 리뷰 리스트 호출
useEffect(()=>{
fetch(http://localhost:8000/products/detail/${productId}/review
,{
method:"GET",
headers:{
"Content-Type" : "application/json"
}
})
.then(res => res.json())
.then((data)=>{
setReview(data.reviewData)
})
},[newReview])
POST메서드로 리뷰 보내기
const addReview = ()=>{
const data={
title : titleValue.current.value,
content : textValue.current.value
}
fetch(`http://localhost:8000/products/detail/${productId}/review`,{
method:"POST",
headers:{
"Content-Type" : "application/json",
"Authorization" : localStorage.getItem("token")
},
body : JSON.stringify(data)
})
//보내기
.then(res => res.json())
.then((data)=>{
titleValue.current.value = "";
textValue.current.value = "";
setNewReview(data.reviewData)
})
}
상품후기
상품문의
첫번째 찾아온 시련은 깃헙이였다. 초기 설정에서 에러가 발생하면서 에러를 잡고 시작한 팀원과
해당 라이브러리를 제거후 새로 설정을 하고 시작한 팀원이 나뉘면서 우리의 경고는 시작되었고
첫 merge를 했을 때 일이 터졌다. 다행히 초반에 알게 되어 새로 초기설정을 하고 클론받아서
현재 진행중인 작업들을 다시 옮기고 merge > pull을 하였다. 이날은 정말 팀원 모두가 고생하였다.
두번째 시련은 위의 언급했던 바와 같이 진행 계획을 확실히 나누지 않은채 시작하여 레이아웃과 스타일 후 기능 구현 부분에서 다시 나눠야 했던 점이다. 팀원 모두 할수 있는 한 최대한으로 구현하려 노력하였고 기능이 들어가는 부분은 어느정도 완성 했던 것 같다.
🚨 처음 직면한 에러 : 이미지 슬라이드에 prev버튼이 안 먹히네???
한참 코드를 다시 살펴보고 다시 코딩을 해도 이상하게 한 버튼이 먹히지 않는다...
몇일을 팀원과 함께 봐도 답이 나오지 않아서 멘토에게 도움을 요청하였다.
원인은 버튼 컴포넌트!! prev와 next 양쪽이라는 생각에 컴포넌트를 각각 Button컴포넌트로 나눠서 작업을 하고는 prev와 next버튼의 함수를 한 컴포넌트씩 넘겨줘야하는데.. 각 컴포넌트당 함수를 전부 넘겨줘서 문제 였다. 나의 코드를 한번 더 의심하자!
지난 세션중 인스타그램을 만들어보면서 피드에 올리는 댓글기능을 할때였다. mock-up데이터가 아닌 DB에 연결해서 인증&인가를 통해 댓글을 추가하고 삭제하고 리스트를 불러오는 것이 궁금했다. 그때 멘토님은 DB로 연결하면 이렇게 하지 않는다고 하시고 세션을 마쳤는데 드디어 내가 구현할 때가 되었다!!
이 얼마나 설레는 일인가!!
이 기능을 하기 앞서 인증 & 인가 세션을 다시 보고 정보를 찾아보았다.
📌 refer : 인증 & 인가 이론
먼저 카테고리 탭을 이용하여 상세페이지 내에 컴포넌트가 바뀌게 작업하였다.
function ProductBottom(){
const [review,setReview]=useState(false);
const reviewBtn = ()=>{
setReview(true)
}
const qnaBtn = ()=>{
setReview(false)
}
return (
<ul>
<li className='border border-active' onClick={reviewBtn}
>상품리뷰</li>
<li className='border border-active' onClick={qnaBtn}
>상품문의</li>
</ul>
// 리뷰 및 문의
<div className='modal-container'
>{review === true ? <Review /> : <Qna /> }</div>
)
}
export default ProductBottom;
하나의 모달창을 만들때는 버튼함수에 (!state)를 사용하였는데 두개의 탭으로 하려니 복잡한 느낌이다. 이것저것 만져보다 결국 이렇게 마무리... 카테고리 기능에 대해 더 공부해 봐야겠다.
Review.js
//add list => get
const [newReview,setNewReview]=useState([]);
useEffect(()=>{
fetch(`http://localhost:8000/products/detail/${productId}/review`,{
method:"GET",
headers:{
"Content-Type" : "application/json"
}
})
.then(res => res.json())
.then((data)=>{
setReview(data.reviewData)
})
},[newReview])
✏️ 체크
// 리뷰등록
const addReview = ()=>{
const data={
title : titleValue.current.value,
content : textValue.current.value
}
fetch(`http://localhost:8000/products/detail/${productId}/review`,{
method:"POST",
headers:{
"Content-Type" : "application/json",
"Authorization" : localStorage.getItem("token")
},
body : JSON.stringify(data)
})
//보내기
.then(res => res.json())
.then((data)=>{
titleValue.current.value = "";
textValue.current.value = "";
setNewReview(data.reviewData)
})
}
일단 fetch함수를 이용해서 서버에 연결하고 GET메서드로 리뷰 리스트를 불러온다.
리뷰 추가시 재렌더링되게 하고 POST메서드로 리뷰를 추가한다. 이걸 리뷰 등록에 onClick이벤트를 사용해서 넣어준다.
🚨 두번째 에러 : 상품후기에서 api호출하여 POST테스트 중 400번 에러가 발생!!
😱 검색을 해도 도통 확인이 안된다...
멘토님 도와주세요!!
네트워크에서 에러를 확인하고 서버에 에러메세지를 확인해 보니
결제하지 않은 유저가 작성하려 해서 생긴 에러..
결제 기능을 구현하지 않은 우리 팀에서는 테스트시 백엔드에서 구매한 토큰을 받아서 하다가 최종으로 구매한 제품을 따로 지정해서 작업을 하였다.
이 에러를 통해서 네트워크로 에러를 확인하는 법을 알게되었다.🧐
✏️ 체크
//deletes
const deleteBtn = (id)=>{
fetch(`http://localhost:8000/products/detail/${productId}/review?review_id=${id}`,{
method:"DELETE",
headers:{
"Content-Type" : "application/json",
"Authorization" : localStorage.getItem("token")
},
})
.then(res => {
if(res.ok){
fetch(`http://localhost:8000/products/detail/${productId}/review`,{
method:"GET",
headers:{
"Content-Type" : "application/json"
}
})
.then(res => res.json())
.then((data)=>{
setReview(data.reviewData)
})
}
🚨 마지막 에러 : 상품후기에서 POST method로 등록후 useEffect안에 GET method로 재렌더링을 한후 DELETE method를 사용해서 삭제를 하는데 재렌더링을 어떻게 해야할까 고민하다 useEffect에 같이 넣었다. 백엔드에서 무한요청이 들어온다고 경고!!경고!!! 등록까지는 잘됐으니 삭제에서 문제인가? useEffect에서 삭제를 빼니 괜찮아졌다.. 그럼.. 이걸 어디에 넣어야 하나?? 계속 고민하고 있던 차에 함께한 팀원중 장바구니 구현을 한 지원님이 옆에서 DELETE에 바로 GET을 넣는게 어떤지 의견을 내주셨다. DELETE하면서 재렌더링이 될수 있게 GET method를 바로 넣으니 무한요청도 들어가지 않고 재렌더링이 된다! 감사합니다~ 쌩유~ 지원님!!🙇
✏️ 느낀점 : 서버 연결로 게시글을 등록하고 추가하는게 mock-up데이터를 만들어서 작업하는 것보다 훨씬 재밌구나!!! GET으로 리스트가 구현되는것, POST로 글을 등록하고 DELETE로 해당글 삭제까지 실시간으로 구현되는게 신기하고 뿌듯했다!! 다음엔 더 깔끔하게 코드를 짜서 게시글 등록을 만들어봐야겠다.
매일 스탠딩 미팅으로 진행사항 체크를 하고 FE가 구현하고자 하는 디자인과 기능을 체크하여 BE에게 놓치는 부분이 없는지 체크하며 진행하였다. 팀원 모두 홈페이지를 완성하는데에 집중하여 각자 기능별 UI를 맡아서 작업하였다. 작업을 마친 팀원의 UI나 기능을 회의시간에 전체 진행된 부분을 구현해보면서 마지막 날까지 피드백을 주고 받으며 우리의 첫 프로젝트를 무사히 완성하게 되었다.
작업 완료후 합치는 과정에서 처음 Git을 팀작업으로 다루는데 내가 초기 설정을 잘못해서 충돌발생! 다행히 첫 Pull 작업때 알게 되어 팀원중 기완님이 바로 다시 초기셋팅을 해주셨다. 이로써 PR의 중요성을 알게 되었고, 개인 브랜치를 따로 생성하여 작업해야하는 필요성을 알게 되었다.
프로젝트를 진행 할수록 의견이 맞지 않거나 불만을 가지는 팀원도 있었으나 UI를 주로 다룬 팀원에게 남은 기능을 위주로 작업을 나누고 기능을 주로 다룬 팀원에게 남은 UI부분을 주면서 서로의 맡은 역활에서 부족하게 느낀 부분을 채워가며 프로젝트를 진행 하였다. 이처럼 충분한 소통을 하고 각자 불만사항을 서로 대신 채워가며 작업을 진행하니 처음에는 의견을 내세우는것이 낯선 팀원들도 점점 스스럼없이 대화를 할수 있었다.
프로젝트 완료까지 각자의 역량에 맞춰 작업을 진행하였지만 사이트 전체적인 완성도에 대해서는 많이 아쉬웠던 프로젝트였다. 내가 좀 더 이끌었다면 이라는 생각이 들지만 나는 최선을 다했다고 말할수 있다. 그래도 개개인이 담당한 역할에 최선을 다해준 팀원들에게 고마움을 표한다.