WECODE 1차 팀프로젝트로 팀원 6명으로 구성되어 영양제 사이트인 Pilly를 클론 코딩 하였다.
- 기간: 2021. 7. 05.~16. (2주)
- 백엔드: 김태영, 설지우, 최명준
- 프론트엔드: 이수정, 이윤경, 최창원
💡프로젝트 방식 "SCRUM"
- Sprint: 1주일
- Daily Stand Up Meeting: am 11:00
- Use Tool: Trello
회의를 통해 메인페이지를 담당하게 되었고, 두번째로 제품 상세페이지를 맡게 되었다.
멘토님과 미팅을 통해 메인페이지에선 commerce에 필요한 부분은 과감하게 버리고 기능 적으로 슬라이드만 구현해 보기로 하였다.
내가 슬라이드를 구현하고자 할 때 고려한 부분은 두가지로
슬라이드 광고판은 언제든지 내용이 바뀔 수 있기 때문에 슬라이드 카드 갯수가 고정되어 있지 않고 data에 따라 유동적이길 바랬고 첫번째 고려사항은 목표한대로 100% 달성 할 수 있었고 두번째 고려사항은 80%정도 완성 되었다고 생각할 수 있다.(20%는 Code Point 에서 후술)
.map()
매서드를 활용하여 <ul>
의 <li>
로 랜더링 하였다.//data를 받아올때 구조분해를 통해 전달 하였다.
{SLIDE_LIST.map(({ id, icon, description, user }) => (
<li className="slideList" key={id} value={id}>
<article className="slideContent">
<i className={icon}></i>
<div className="slideTextWrap">
//description의 줄바꿈을 위해 '/n'을 줄바꿈을 원하는 곳에 미리 삽입한 후 기준을 삼아 .split()매서드로 나눈 후 <p> tag로 감싸 줄을 나눠주었다.
{description.split('\n').map((line, index) => (
<p key={index} className="slideText">
{line}
....
<ul className="slideButtons">
{SLIDE_LIST.map(button => (
<li className="slideButtonList" key={button.id}>
<button
className="slideControlButton"
onClick={this.slideHandler}
value={button.id}
>
//네비게이션 버튼도 data갯수에 맞춰 생성될 수 있도록 data 함께 .map()매서드에 넣어 주었고 후술 될 slideCardNumber 변수에 따라 조건부 랜더링되어 현재 위치를 알 수 있도록 하였다.
{slideCardNumber === button.id ? (
<BiRadioCircleMarked />
) : (
<BiRadioCircle />
)}
</button>
</li>
transform: translateX
style
을 활용 슬라이드 카드 만큼 보여지는 부분을 감소 시키려 하였고 올바른 접근이었지만 감소시키는 기준이 필요하고 감소시켜주는 기능을 하는 함수가 일정시간 마다 실행되어야하는 문제가 남아 있었다.state
값을 통해 해결 하였고, 자동으로 실행되는 부분은 setInterval()
함수를 통해 해결 하였는데 이때 다시 setInterval()
함수를 없애줘야되는 상황이 생겼다.(그대로 이동할 경우 메모리 누수경고가 발생한다.) 이를 위해 ref
를 활용해 IntervalId
를 주어 componentWillUnmount()
시 clearInterval()
시켜 주었다.this.slideIntervalId = React.createRef();
this.state = {
slideCardNumber: 0,
};
}
componentDidMount() {
this.slideIntervalId.current = setInterval(this.moveNextSlide, 2500);
}
componentWillUnmount() {
clearInterval(this.slideIntervalId.current);
}
//state 값인 slideCardNumber를 기준으로 ++ 시켜주며 setState시켜 주었고 마지막에 다시 돌아가기위해 삼항연산자로 기준을 주었다.
moveNextSlide = () => {
const { slideCardNumber } = this.state;
const nextSlideCardNumber =
slideCardNumber > SLIDE_LIST.length - 2 ? 0 : slideCardNumber + 1;
this.setState({ slideCardNumber: nextSlideCardNumber });
};
...
//변화되는 slideCardNumber를 기준으로 100vw만큼(1개의 카드 폭) 감소하여 슬라이드가 넘어갈 수 있도록 inline style을 주었다.
<ul
className="slide"
style={{ transform: `translateX(${-100 * slideCardNumber}vw)` }}
>
이로서 메인페이지의 슬라이드 기능은 구현이 완료 되었지만 개인적으로 두가지 아쉬운점이 남아 있었다.
아쉽게도 두 가지는 해결하지 못했지만 과제로 남겨놓고 수정 할 수 있는 날이 오길 바래본다.
상세 페이지 구성에서 고려한 부분은 메인페이지와 마찬가지로 commerce적인 부분은 배제하였고 대신 필리 홈페이지의 개선점으로 생각한 부분을 수정해 보기로 했다.
필리 사이트에선 제품 리스트에서 장바구니에 담고 상세 페이지에 들어갈 경우 확인이 되지 않는것이 아쉬워 기획단계에서 우리 프로젝트는 상세페이지와 연동시키기로 결정되었다.
또한 제품 리스트와 상세페이지를 이동시키기 위해선 동적 라우팅 개념의 path parameter(또는 Path Variable)를 활용할 필요가 있었다.
먼저 동적 라우팅을 위해
<Route exact path="/product/:productID" component={ProductDetail} />
Routes 컴포넌트에서 productID란 변수값을 상세페이지(ProductDetail)로 넘겨 주었고 fetch(${GET_PRODUCTS_API}/${this.props.match.params.productID}
받아온 productID를 이용해 데이터를 받아올 수 있었다.
또한 장바구니 버튼 연동을 위해서 제품별로 백엔드 데이터에 cart_exist값을 부여해 어디서든 제품을 카트에 전송할 경우 cart_exist가 변경되도록 로직을 구성 하였고 이를 기준으로 장바구니에 담겨 있는 것을 표시하여 주었다.
생각한것 보다 원활하게 프로젝트가 진행되어 추가구현 사항까지 2주차에 충분히 진행해 볼 수 있을거라 판단하였고 나는 네비게이션 미구현 기능 구현, 결제 기능을 담당해 진행 하였다.
팀원인 윤경님께 많은 부분을 담당하다보니 세세한 기능구현엔 시간을 쓸 수 없는 상황이 되었고 내가 navigation에 2가지 기능을 더하기로 하였다.
1. 스크롤에 따른 배경 스타일 변경
2. 현재 위치를 나타 낼 수 있는 글씨 색상 변경
처음에 스크롤에 따른 이벤트를 주기위해 isNavtransper의 state값을 스크롤 이벤트에 따라 변경되도록 하였는데 바뀌는 것이 없어도(스크롤이 움직이는 한) 계속해서 setState가 이루어지는 문제가 발생하였다, 그러던 중 공식 문서에서 PureComponent를 사용하면 얕은 수준의 비교를 통해 state의 변경이 없으면 추가로 setState가 실행되지 않는다는 것을 알게 되었고 가볍게 문제를 해결할 수 있었다.
//언급한 PureComponent 활용
class Nav extends PureComponent {
constructor() {
super();
this.state = {
//현재위치를 알기 위해 state값을 활용하였다
navActiveNumber: 0,
isNavTransper: false,
};
}
componentDidMount = () => {
window.addEventListener('scroll', this.handleScroll);
};
//setInterval()과 마찬가지로 unMount시 스크롤 이벤트는 제거해 주어야 한다.
componentWillUnMount = () => {
window.removeEventListener('scroll', this.handleScroll);
};
//스크롤이 발생할 경우 state값이 변경된다, 이를통해 nav style을 변경 시키도록 하였다.
handleScroll = () => {
if (window.pageYOffset > 0) {
this.setState({ isNavTransper: true });
} else if (window.pageYOffset === 0) {
this.setState({ isNavTransper: false });
}
};
//현재 위치는 이벤트가 발생한 index를 현재위치를 나타내는 state값으로 활용하였다.
navActiveHandler = index => {
if (!parseInt(index)) {
this.setState({ navActiveNumber: 0 });
} else this.setState({ navActiveNumber: index + 1 });
};
render() {
const { navActiveNumber, isNavTransper } = this.state;
return (
//삼항연산자를 활용한 스타일 변경
<nav className={`navbar ${isNavTransper ? 'transper' : ''}`}>
...
<li className="navList">
{MENU_LIST.map((menu, index) => (
<Link
//state값에 따라 스타일 변경을 주었고
className={`navLink ${
navActiveNumber === index + 1 ? 'active' : 'disactive'
}`}
to={menu.link}
key={index}
//클릭이 발생한 index를 알기 위해 인자로 받아주었다.
onClick={() => this.navActiveHandler(index)}
>
...
결제 기능은 기능적으로 특별한 부분은 없었지만 데이터를 가공하는 부분에서 애를 먹었다.
우리가 갖고 있는 기존 cartList를 백엔드에서 받기 편한 orderList로 가공을 해야했는데
cartList = [
{
productID: 1,
productName: 'VitaminA',
quantity: 1,
productPrice: 19500,
thumbnail_image_url: 'http://',
},
{
productID: 2,
productName: 'VitaminB',
quantity: 3,
productPrice: 19500,
thumbnail_image_url: 'http://',
},
];
이러한 형태를
orderList = {
'0': { productID: 1, quantity: 1 },
'1': { productID: 2, quantity: 3 }
}
이런 형태로 바꿔주어야 했다.
처음엔 배열의 delete
매서드를 활용해 로직을 구성했는데 멘토님의 '저는 한번도 delete
라는 매서드를 써본적이 없어요'라는 코멘트에 다시 머리를 싸메고(결국 도움을 받아) 해결하게 되엇다.
const newCartList = cartList.map(cart => {
const {productID, quantity} = cart;
return {productID, quantity};
});
orderList = {...newCartList}
보면 아주 간단하지만 바로 생각으로 이어지지 못하는거 보면 아직까지 배열과 객체를 제대로 다루지 못하는 듯 😭.. 아쉽다..
필리사이트에서 장바구니에 담긴 걸 다시 담으려고하면 알림이 뜨는 '토스트'기능이 구현되어 있다, 사실 구현을 하긴 했는데 방법이 잘못되었을 뿐... 되긴 된다..
그리고 아마도 시간이 더 있었으면 해결 할 수 있는 문제라 좀 더 아쉽게 느껴진달까?(state값을 활용했는데 ref값으로 바꾸면 되지 싶은..)
그래도 우리 기획상 필수 구현사항은 아니었고(사이트에 꼭 필요한 기능은 아닌것으로 판단)이미 장바구니에 담긴 것을 확인 할 수 있도록 변경 하였기 때문에 포기하고 다음 스텝으로 넘어 갈 수 있었다.
//미구현 토스트 코드
addedCartAlert = () => {
const { addedCartAlertList } = this.state;
addedCartAlertList.push('');
this.setState({ copyAddedCartAlertList: addedCartAlertList });
setTimeout(() => {
addedCartAlertList.pop();
this.setState({ addedCartAlertList: addedCartAlertList });
}, 3000);
};
1차 프로젝트를 하며 좀더 많은 기능, 좀더 고급 기술에 집중하기 보단 지난 한달간 배운것을 100% 활용해서 구현해 볼 수 있었던 점,
팀원 모두가 욕심 부리기 보다 각자의 역활에 충실하고 서로 도울 수 있었고, 다른팀은 발표할 때 아쉬운점을 먼저 얘기하였지만 우리팀은 결과에 전원 만족할 수 있던 점,
또 모두가 집에간 시간까지 6명 전원 모두 회고미팅을 하며 서로 좋고 아쉬운점을 나눌 수 있었던 점,
'너무 만족스러운 프로젝트 였다.'
전체적으로 밸런스와 팀웍이 돋보이는 백엔드 팀원과 정말 열정과 노력을 둘다 겸비한 두분의 프론트 팀원들 사이에서 나는 팀의 일정 조율과 정확한 목표를 공유할 수 있는 부분에 집중 하였다. 원빈과 장동건이 있으니 감초역활만 해도 성공할 수 있단 생각이었달까? 그러기 위해서 조금 늘어지려던 회의시간에 뜬금없이 주도하려고 하고 목표를 결정하는 무례함을 참고 좋게 봐주신 팀원들 감사합니다.
가장 어려운부분을 맡아서 끝까지 해낸 수정님,
정말 굳은일 부터 정말 많은 코멘트를 받아가면서 힘든 부분을 구현해 낸 윤경님,
그리고 요구하는 대로 뚝딱뚝딱 만들어준 백엔드 지우님,
항상 끝까지 남아서 사소한거 하나부터 전체적인 데이터를 계속 맞춰준 명준님,
그리고 진짜 진짜 처음부터 끝까지 고생많았던 태영님
다들 너무 수고 많았고 덕분에 최고의 프로젝트를 경험할 수 있었습니다.
🙏다시한번 감사합니다!😍
부록: 1차 프로젝트 회고록
2주간 Villy 팀을 이끌어 주셔서 너무 감사했습니다! 창원님 💊