"BASENOTE"
Find Your Scent, 가장 나다운 향을 찾아드립니다.
이 프로젝트는 Wecode Fullstack 1기 BASENOTE 팀에서 진행한 향수 쇼핑몰 PAFFEM 클론 프로젝트입니다.
➡️ 총 19일 간 진행
💎은 내가 담당한 기능.
내가 담당했던 구현 사항에 대한 기록.
굉장한 장문이니 주의!
PAFFEM 사이트의 상품의 경우, 하나의 상품 당 2가지의 용량이 존재하며 용량별 가격만 다를 뿐 상세정보는 모두 같다.
path parameter를 사용하여 현재 URL의 용량 정보를 가져와서 해당 상품의 해당 용량에 맞는 페이지에 접속할 수 있게 했다.
상품 상세정보 목데이터를 기반하여 상세페이지의 상세정보 영역에 데이터를 출력해주었다.
PAFFEM 사이트의 경우, 상세정보 영역의 정보가 모두 이미지로 되어있었지만 이미지가 아닌 코드로 구현했다.
상품의 시리즈에 따라 Keyword 영역의 방울 색상을 상이하게 적용시켜주기 위해, SCSS의 mixin을 사용하여 테마를 구현해보았다.
// Keyword.jsx
<div className={`bubbles ${series || ''}`}>
{keywords.map(keyword => (
<Circle
key={keyword.id}
mood={keyword.name}
grade={keyword.grade}
/>
))}
</div>
prop으로 받은 series 정보를 동적으로 클래스명을 설정해준 후,
// Keyword.scss
.bubbles {
@include keywordTheme(default);
position: relative;
width: 885px;
height: 540px;
margin: 0 auto;
&.wind {
@include keywordTheme(wind);
}
&.melt {
@include keywordTheme(melt);
}
&.path {
@include keywordTheme(path);
}
}
keywordTheme이라는 이름으로 만들어둔 mixin에 각 series명을 담아준다.
// KeywordTheme.scss
@mixin keywordTheme($name) {
@if $name == 'default' {
.first {
background: rgba(227, 212, 203, 0.9);
}
.second {
background: rgba(252, 243, 234, 0.7);
}
.third {
background: rgba(242, 230, 216, 0.8);
}
.fourth {
background: rgba(204, 210, 207, 0.7);
}
}
@if $name == 'wind' {
.first {
background: rgba(234, 240, 247, 0.9);
}
.second {
background: rgba(243, 246, 250, 0.7);
}
.third {
background: rgba(243, 241, 248, 0.8);
}
.fourth {
background: rgba(210, 215, 225, 0.7);
}
}
@if $name == 'melt' {
.first {
background: rgba(171, 183, 192, 0.9);
}
.second {
background: rgba(192, 172, 173, 0.7);
}
.third {
background: rgba(228, 223, 205, 0.8);
}
.fourth {
background: rgba(174, 185, 184, 0.7);
}
}
@if $name == 'path' {
.first {
background: rgba(252, 231, 224, 0.9);
}
.second {
background: rgba(255, 242, 242, 0.7);
}
.third {
background: rgba(249, 197, 193, 0.8);
}
.fourth {
background: rgba(239, 243, 249, 0.7);
}
}
}
KeywordTheme.scss
에서 테마의 name별로 배경색상을 다르게 주기 위해 if를 사용하여 조건을 나누어 보았는데, 색상 외에 같은 구문이 반복되고 있다는 점이 아직도 걸린다. 이 부분은 하드 코딩을 했다는 생각이 들어 추후 리팩토링을 통해 개선하고 싶다.
로그인 후, 장바구니 Read API와 연동하여 유저의 장바구니 아이템을 조회할 수 있다.
로컬 스토리지에 저장된 토큰을 통해 유저를 판별하기 때문에, 유저별로 다른 (자신만의) 장바구니 아이템을 조회할 수 있다!
장바구니 Update API와 연동하여 장바구니 아이템의 수량을 변경할 수 있다.
해당 아이템의 변경된 수량 값이 DB에 저장되기 때문에, 수량 변경 후 다른 페이지에 접속했다가 장바구니 페이지에 돌아와도 변경했던 수량 그대로 보존되어 있는 것을 확인할 수 있다 😊
PAFFEM 사이트에서는 장바구니가 비었을 때는 장바구니 테이블이 텅-빈 상태로 출력이 되고 장바구니가 비어있다는 안내 메시지가 존재하지 않았다.
유저에게 friendly한 서비스를 제공하고자, 토큰이 존재하지 않거나 (비회원), 유저이지만 장바구니에 아이템이 존재하지 않을 때는 "장바구니에 담은 상품이 없습니다"라는 메시지를 출력시켜보았다.
access token은 다른 토큰에 비해 만료 시간이 짧다.
자연스러운 UX와 토큰이 만료되었을 때의 에러 핸들링을 겸하여, 토큰이 만료했을 때는 로그인을 요청하는 dialog를 출력시키고, 확인 버튼을 누르면 로그인 페이지로 바로 이동할 수 있도록 구현해보았다.
(이 부분 제안해주신 소헌님 감사합니다 💖)
class Cart extends Component {
constructor() {
super();
this.state = {
cartItems: [],
isLoading: true,
};
}
getCartData = async () => {
const accessToken = localStorage.getItem('token');
if (!accessToken) {
this.setState({ isLoading: false });
return;
}
fetch(CART_API, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
.then(res => res.json())
.then(result => {
if (result.message === ERROR_MESSAGES.invalidToken) {
localStorage.removeItem('token');
window.confirm(
'안전한 서비스 이용을 위해, 일정 이용 시간 초과 후 자동 로그아웃 되었습니다.\n다시 로그인 후 이용해주세요 🌸'
) && this.props.history.push('/member/login');
this.setState({ isLoading: false });
return;
}
this.setState({ cartItems: result.cartItems, isLoading: false });
})
.catch(console.error);
};
componentDidMount() {
this.getCartData();
}
...
}
alert가 아닌 confirm을 사용한 이유는, 로그인 페이지로 이동하지 않고 장바구니 페이지에 그대로 머물 수 있는 선택지를 제공하기 위해 취소 버튼이 존재하는 confirm을 선택했다.
이 부분도 PAFFEM 사이트에 존재하지 않는 기능이지만, 데이터를 fetch해오는 컴포넌트에서 fetch 중일 때는 Loading 화면을 출력시키도록 Loader 컴포넌트를 추가로 구현해보았다.
Loader 컴포넌트를 구현하게 된 계기는 장바구니 페이지의 빈 장바구니 메시지를 구현하는 과정에 있었다.
위의 「장바구니가 비었을 때 메시지 출력」에서 구현했던 빈 장바구니 메시지는, 장바구니 아이템 배열의 길이가 0일 때 출력시키도록 하고 있었는데, 장바구니 아이템이 존재함에도 불구하고 초기값이 빈 배열이기 때문에 렌더링 직후 일시적으로 빈 장바구니 메시지가 출현되는 문제점이 있었다.
이를 해결하기 위해 isLoaded
라는 플래그를 사용했고 fetch가 완료+배열의 길이가 0일 때라는 조건을 조합해서 문제를 해결했었다.
그렇다, 초반에는 Loader 컴포넌트를 만들지 않았었다🤓
하지만 관희님의 코드 리뷰에서 isLoaded
와 this.state.cartItems
의 로직을 분리시키는 것이 더 직관적일 것이라는 조언을 주셨다.
isLoaded
를 분리하는 과정에서 단순한 플래그가 아닌 Loader 컴포넌트로 로딩화면을 구현하는 것이 더 자연스럽고, 사용자에게 친숙할 것이라고 생각했다.
그렇게 구현된 Loader 컴포넌트는 fetch를 사용하는 리스트, 상세 페이지에도 적용시켰다.
짜란-
↑ 힐끗보이는 Loader를 보여주기 위한 발악ㅋㅋㅋ
(관희님, 장현님 감사합니다 💖)
page의 컨텐츠를 중앙 정렬시키는 .container
는 모든 페이지에서 범용으로 사용되기 때문에, 재사용성을 높이기 위해 Container 컴포넌트로 분리시켰다.
Container 컴포넌트는 this.props.children
가 핵심 요소이다.
<Container>
의 children으로 page의 컨텐츠를 넣어줌으로써, 컨텐츠가 중앙에 위치하게 되도록 했다.
또한 각각의 page에 따라 .container
에 개별적으로 CSS를 추가하고 싶은 경우를 대비해서,
<Container>
에 props.option
를 받게 하고, 그 props.option
을 바탕으로 class명을 동적으로 부여하고 스타일을 설정할 수 있게 했다.
자세한 코드는 ⬇️⬇️⬇️
// Container.jsx
import React, { Component } from 'react';
import './Container.scss';
class Container extends Component {
render() {
const { children, option } = this.props;
return <div className={`container ${option || ''}`}>{children}</div>;
}
}
export default Container;
/* Container.scss */
.container {
max-width: 1080px;
width: 100%;
margin: 0 auto;
&.wide {
max-width: 1336px;
}
}
// Cart.jsx
import { Component } from 'react';
import Container from '../../../components/Container/Container';
import CartTable from './CartTable';
import './Cart.scss';
class Cart extends Component {
render() {
return (
<Container option="wide cart">
<h2 className="pageTitle">Cart</h2>
<CartTable />
</Container>
);
}
}
export default Cart;
// Cart.scss
.cart {
&.container {
margin: 150px auto 95px;
}
}
Footer는 최대한 PAFFEM 사이트를 100% 재현하도록 했다!
Footer의 메뉴 링크들은 배열에 데이터를 담아 map을 돌려서 구현했다.
↓ 아래가 PAFFEM 사이트의 Footer. 어떤가요?😋
로그인 API는 WeStarbucks 백엔드 과정에서 빛소헌님의 명강의와 열심히 학습을 한 덕분에 크게 어렵지 않았던 것 같다!
하지만 모든 것에 의미가 없는 것은 없다!
로그인 API를 구현하며, 로그인 & 회원가입과 같이 보안을 특히 고려해야하는 API에 관해서는 에러 메시지와 에러 코드를 상세하게 발생시키지 않아야한다는 점을 배웠다.
(캡쳐가 너무 크네요💦)
로그인 API의 자세한 구현 내용은 다음과 같다.
- Request로 받은 항목이 null일 때 에러 핸들링
- Bcrypt를 이용하여, Request로 입력 받은 비밀번호와 DB에 저장된 해싱된 패스워드를 비교
- 비밀번호가 일치할 시 토큰을 발급하여 Response로 토큰을 전달
토큰 인가 미들웨어도 WeStarbucks 백엔드 과정에서 열심히 만든 기억이 있기 때문에 크게 헤매지 않았다.
단, 에러가 발생했을 때 에러 코드와 에러 메시지를 함께 반환시켜주는 것이 좋다는 점을 배웠다!
토큰 인가 미들웨어의 자세한 구현 내용은 다음과 같다.
- Token을 복호화하여 얻은 유저 정보로 해당 유저가 DB에 존재하는 유저인지 조회
- 존재하는 유저라면, request로 복호화한 유저 정보를 전달, 존재하지 않는 유저라면 에러 출력
- 유효하지 않거나 만료된 토큰일 경우 에러 출력
이번 프로젝트에서 꼭 도전해보고 싶었던 장바구니 API. CRUD 중 나는 Read와 Update를 맡게 되었다.
소헌님께서 장바구니 API 구현을 진행하기 전에, 감사하게도 큰 흐름을 먼저 체크해주셨다.
회사에서 백엔드 엔지니어 분들이 회의하시는 모습을 우연히 보았을 때, 항상 큰 모니터에 Flow Chart를 띄워두고 논의를 하시는 모습을 눈여겨보았었다.
그래서 소헌님께서 체크를 해주시기 전에 Flow Chart를 이용하여 내가 생각한 Read API의 흐름을 정리해보았다.
이렇게 우여곡절의 과정을 거쳤다🙈
소헌님의 조언과 함께 수정에 수정을 거듭하여 완성된 나의 최종 Flow Chart.
프론트 부분까지 섞여버린 난잡한 야매 Flow Chart이지만, 이렇게 Flow Chart라는 하나의 도구를 이용해서 생각을 시각화해서 정리해나가는 것이 큰 도움이 되었다.
장바구니 Read API의 자세한 플로우는 다음과 같다.
- READ(GET) API 실행 전, 토큰 인가 미들웨어가 사전에 토큰 유효성 체크를 행함
- 유효한 토큰의 경우 next()로 다음 미들웨어인 controller가 실행
- controller는 request로 복호화된 users.id를 전달받음
- dao는 controller → service를 거쳐 전달받은 users.id를 이용해서 DB로부터 해당 유저의 cart item를 select하여 service로 return
- service는 controller로 cart item을 return
- controller는 return받은 cart item을 response로 클라이언트에 전달함
장바구니 수량을 Update하는 API이다. 이 API도 Read API와 동일하게 Flow Chart를 먼저 그리고 진행을 해보았다.
장바구니 Update API의 자세한 플로우는 다음과 같다.
- UPDATE(PATCH) API 실행 전, 토큰 인가 미들웨어가 사전에 토큰 유효성 체크를 행함
- 유효한 토큰의 경우 next()로 다음 미들웨어인 controller가 실행
- controller는 request로 복호화된 users.id를 전달받음
- dao는 controller → service를 거쳐 전달받은 users.id, carts.id, carts.quantity를 이용
- carts.user_id와 현재 유저(
req.foundUser[0].id
)가 같고 && quantity가 1보다 클 때만 Update를 실행
이번 프로젝트를 포함해서 위코드를 진행하며 나에게 칭찬을 해주고 싶은 점이기도 하다. 내가 아는 지식을 팀원, 동기분들에게 공유를 했던 점. 팀 프로젝트를 하면서 도입하고 싶은 기술(코드)은 적극적으로 어필을 했고(그 중의 하나가 바로 Container 컴포넌트!), 팀원분들에게 그 기술을 사용하는 방법을 소개했다. 회사 생활을 했었을 때 조언으로 가장 많이 들었던 것 중의 하나는 바로, "내가 알고 있는 지식에 대해 아웃풋하자"라는 점이었다. 아웃풋은 나 뿐만 아니라 팀 전체를 성장시키는 것에 큰 힘이 되기 때문이다. 이번 팀 프로젝트를 통해 조금은 더 적극적으로 내가 알고 있는 지식을 공유했던 좋은 기회였던 것 같다. 수고했어 나 자신!
내향적인 성격이라 평소 무언가 나서거나 이끄는 것에 소극적인 편이다. 하지만 성공적인 팀 프로젝트를 위해, 내가 힘이 될 수 있는 곳에는 보탬이 되고자 노력했다. 회의에 적극적으로 참여하면서 회의 진행을 이끌기도 하며 도왔다. 프로젝트를 진행하면서 발견한 부족한 부분을 팀원들에게 공유하며 상기시켰다. 전체적인 프로젝트 진행 상황을 체크하며 티켓과 스케줄을 조정하는 것에 힘썼다. 프로젝트를 진행하며 필요한 기본적인 룰(디렉토리 구조 등)을 정하는 것에 적극적으로 참여하며 이끌었다.
이 마음을 여기서 공개적으로 언급해도 될지 솔직히 잘모르겠다. 하지만 나중을 위해, 그리고 솔직한 내 마음을 표현하기 위해 적어본다. 위코드에서 나는 동기분들로부터 질문을 많이 받고 있는 편이다. 개발자의 문화는 기본적으로 공유를 바탕으로 하고 있기 때문에 초반에는 내가 그것에 기여를 하고 있고 도움이 된다는 생각에 기뻤지만, 그리고 점점 질문을 받게 되는 상황이 잦아지면서 시간 관리에 차질을 겪게 되고, "내가 정확한 지식을 전달하고 있을까" 등의 부담감이 생기기 시작하며 스트레스를 점점 더 크게 느끼게 되었다. 동기분들께서 고민의 고민을 거듭한 끝에 질문을 주셨다는 것을 알기에, 여유가 없을 때 조차 차마 질문을 거절하지 못했던 상황이 많았던 것 같다. 그렇게 질문을 받는다는 행위에 대해 굉장히 스트레스를 크게 느꼈다. 그리고 우리 기수에서 유일하게 현업을 경험한 경력자로써 "잘하고 싶다"는 마음과 "잘해야겠다"라는 필요 이상의 책임감을 혼자 가지게 된 것 같다. 정말 솔직히 말하자면 어느 순간부터 "경력자"라는 틀에 나를 가두게 된 것 같다. 이번 팀 프로젝트에서도 어김없이 나는 잘해야겠다, 도움이 되어야겠다는 짐을 혼자 짊어지게 되었고 생각지도 못했던 곳에서 발생하는 예외 상황이나, 프로젝트 진행이 매끄럽게 잘 되지 않을 때 굉장히 스트레스를 많이 받았다. 프로젝트가 점점 진행되면서 스트레스는 극에 달했고 팀원 분들에게 예민한 모습을 많이 보여드린 것 같아 미안한 마음이 크다. 스트레스를 받게 된 원인을 해결하기 위해 내 마음을 솔직하게 표현하며 소통하는 등 노력을 하는 것도 중요하지만, 내가 통제하지 못하는 스트레스에 관해서는 유연한 마음가짐을 가지는 것이 필요하다고 생각했다.
우리 팀에서 초반에 프로젝트 티켓을 배분할 당시, 각자 희망하는 분야를 경험해보는 것을 최우선으로 고려하는 것에 중점을 두었다. 사실 프로젝트를 진행하면서 가장 중요시해야하는 것은 바로 시간 약속을 지키는 것인데, 상황을 냉정하게 바라보지 못한 채 우선 순위를 제대로 설정하지 못했다. 물론 희망하는 분야를 구현해보는 것이 기쁘지만, 모든 곳에서 새로운 배움을 경험할 수 있으니까. 의미없는 것은 없다고 생각한다. 매일매일 팀 회의를 진행했음에도 불구하고, 개개인의 프로젝트 진행 상황을 세세하게 체크하지 못했고 그 결과 스케줄이 크게 delay가 되었다. 시간 관리에 미흡했기 때문에 필수 구현 기능을 줄이게 된 상황이 발생하게 되었다는 점이 아직까지 너무 아쉽고 속상하다. 앞으로 프로젝트를 진행할 때는 시간과 개개인의 능력을 고려해서 담당 부분을 할당해야겠다는 다짐을 했다. 그리고 무엇보다 프로젝트 진행 상황을 세심하게 체크하기 위해서는 무조건 소통. 자기자신 조차 일의 진행이 어떻게 되어가고 있는지 제대로 인지하지 못할 때가 있다. 그렇기 때문에 누군가가 표현하지 않아도 잘 진행하고 있는지 항상 먼저 질문하고 확인하는 자세를 길러야겠다고 생각했다.
잘하고 싶은 마음에, 잘됐으면 하는 마음에 팀원 분들에게 쓴소리를 많이 했던 것 같아요. 쓴소리 뿐만 아니라 진솔한 소통을 위해 내 마음 또한 솔직하게 표현해야한다는 중요성을 깨달았습니다. 다만 표현을 하는 것에 있어서도 상황에 따라 적절하게 표현하는 방법을 배워야겠다고 생각했습니다. 모든 상황에 민감하게 반응하지 않고 스트레스에 어느 정도는 무뎌질 필요가 있을 것 같아요. 그렇게 나 자신을 좀 더 편안한 상태에 두도록 마인드 컨트롤을 해야겠다고 생각했습니다. 이번 팀 프로젝트를 통해 기술 외적인 부분에서 많은 깨달음을 얻게 된 것 같습니다. 단단한 어른, 성숙한 사람이 되기 위해 더 노력하겠습니다!
사다리의 운명에 따라 최종 발표하는 나...
최종 발표 후 사망한 나...
우리 BASENOTE 팀 너무너무 수고하셨어요 💖
코하루상!! 베이스노트에서 코하루의 역할을 너무 지대했습니다. 질문도 많이 받고 남들보다 트렐로카드도 많이 가져갔었고 정말정말 많이 고생했습니다!! 덕분에 우리팀이 이정도나마 구현을 할 수 있엇습니다
오히려 제가 인생첫프로젝트라 우왕자왕하느라 팀에 도움이 못된것같아 아쉬움이 크네요. 너무 좋은 팀원이었습니다 코하루!!!! 2차는 따로하게되었지만 각자의 팀에서 또 아쉬움은 뒤로하고 잘하면서 성장합시다!!
감씨 힘내십시요 !! ㅋㅋㅋㅋ 팀끼리 회식못해서 아쉽네요 ㅜ 이번 프로젝트는 건강관리도 잘하면서 하길 바랍니다. 화이팅!
이미 하영님껜 디엠으로 회고록에 대한 이야기를 전달해드린 적 있지만 다시 댓글을 답니다! 흐흐
굉장한 회고록 이군요! 글자 한 자 한 자에 하영님의 정성과 노력, 쏟아 부은 시간들이 다 느껴집니다. 특히나 flow chart로 전체적인 흐름을 정리하신 부분은 정말 깜짝 놀랬어요. 플로우 체크가 중요하구나, 하는 것과 이렇게 도식화를 해두면 생각이 정리가 안될 때마다 다시 꺼내보며 한 번씩 흐름을 다시 훑어볼 수도 있을 것 같다는 생각이 들었습니다. 하영님 회고록은 두고 두고 찾아와서 읽어보겠습니다 👍 최고
1차 프로젝트 다시 한 번 너무 수고 많으셨다는 말씀 전하고 싶습니다! 더불어, 현재 2차 프로젝트 초반인데 마지막까지, 발표날까지 우리 컨디션 조절 잘 하면서 너무 지치지 않게 (가능할 지..) 기분 좋게 마무리하길 바랍니다! 화이팅 하영님 💖😻