[1차 프로젝트 후기] Wesop

dosilv·2021년 5월 22일
7

Project Reviews 🎉

목록 보기
1/2

🌈 프로젝트 소개

🌤 Aesop 웹 사이트 클론 코딩

스킨, 헤어 및 바디 케어 브랜드인 Aesop 웹 사이트를 참고하여 모든 기능을 직접 설계 및 구현한 클론 코딩 프로젝트! 다른 커머스 사이트에 비해 상품들이 다양하거나 방대하지는 않았지만 프론트엔드의 시각에서 전체적으로 부드러운 애니메이션 효과들이 눈에 띄었다. (개인적으로 사이트가 예뻐서 하는 내내 재미있었다~~🥰)

🌤 프로젝트 기간

2021.05.10 - 2021.05.21 (약 2주)

🌤 팀원 구성

Backend: 문성호, 양미화
Frontend: 김도은, 김휘성(PM), 최원근

🌤 기술 스택 (Frontend)

HTML/CSS, JavaScript, React, SASS

🌤 구현한 기능

⚡ 메인 페이지

  • Navigation Bar에서 상품 카테고리, 로그인, 회원가입, 카트, 주문내역 등 다양한 모달 관리
  • JWT 및 SessionStorage를 이용한 로그인/로그아웃 시 Navigation Bar 구성 변경
  • 이미지 슬라이더(Carousel)를 활용한 상품 미리보기
  • 휠 이벤트를 활용한 동적 로고 디자인 (회전 효과)
  • 라이브러리 없이 구현한 메뉴 애니메이션 (3단 슬라이드)

⚡ 로그인 및 회원가입

  • JWT를 이용한 로그인, 회원가입
  • 아이디, 비밀번호 validation

⚡ 상품 리스트

  • 컴포넌트 재사용을 통한 다양한 상품 목록 구현
  • mouseOver시 장바구니 버튼 활성화

⚡ 상세 페이지

  • react-router-dom을 이용한 동적 라우팅
  • radio button을 통한 사이즈/가격/상품 이미지 변경

⚡ 장바구니

  • fetch API를 이용한 장바구니 추가, 변경, 삭제 기능
  • reduce 함수를 활용한 합계 금액 계산

⚡ 검색창

  • query string을 이용한 검색 데이터 fetch
  • decodeURI를 통한 한글 유니코드 디코딩

🌤 내가 맡은 역할

  • 메인 페이지 및 상품 리스트 페이지 구성
  • 로그인/로그아웃 상태에 따라 바뀌는 Nav bar
  • 3단 슬라이더 메뉴
  • 카트(장바구니) 레이아웃 및 조회, 수량변경, 삭제, 주문 기능
  • 상품 이미지 Carousel
  • 카테고리 ➡ 상품리스트 ➡ 상세 페이지 동적 라우팅
  • 상품 검색 기능


🌈 기록하고 싶은 코드

사실 배운 게 너무너무 너어어어무 많아서 모든 코드를 하나하나 기록하고 싶다.... 그치만 숨 돌릴 틈도 없이 바로 2차 프로젝트에 뛰어들어야 하기 때문에...... 추려서 적어 보는 코드들 😢


🌤 Nav bar의 modal 관리

이솝 홈페이지에는 로그인과 회원가입이 별도의 페이지가 아닌 모달 형태였기 때문에, nav에 딸린 모달만 저만큼이다.

  1. 3개의 메인메뉴(제품보기, 읽기, 검색)
  2. 로그인
  3. 회원가입
  4. 카트
  5. 주문내역
  6. 비회원 안내 창 (로그인이 필요한 페이지입니다)

참고로 5번 6번은 우리가 자체적으로 추가한 것! 그래서 이 많은 모달들을 끄고 켜기 위해서 각각의 true/false state값과 이걸 바꿔주는 메서드를 별도로 만들자니... 반복되는 코드들도 많고 비효율적일 것 같았다.

좋은 방법이 없을까 고민하다가 각각의 true/false값을 한 오브젝트에 묶어서 하나의 state값으로 만들고, 오브젝트의 key로 각 요소의 이름(NAV_DATA의 요소)을 부여했다.

constructor(props) {
  super(props);
  this.state = {
    openState: {
      제품보기: false,
      읽기: false,
      검색: false,
      로그인: false,
      회원가입: false,
      카트: false,
      주문내역: false,
      비회원: false,
    },
    ...
  };
}

toggle을 위한 메서드도 navToggle로 통일한 뒤 파라미터로 key를 전달받아 그에 상응하는 openState의 요소를 변경할 수 있게 했다.

navToggle = nav => {
  const { openState } = this.state;
  this.setState({
    openState: { ...openState, [nav]: !openState[nav] },
  });
};

객체에서 값을 불러올 때 키를 변수로 두는 방법은 알았는데, (Obj.스트링 대신 Obj[변수] 이렇게) 객체 중괄호 내에서 키를 변수로 설정하는 방법은 이번에 처음 알게 되었다. 똑같이 대괄호를 쓰면 되는 거였다~~~😮😮

그리고 중간에 전개연산자+객체이름(...openState)을 써 준 이유는 나머지 객체 요소는 그대로 유지하고, 특정 객체 값만 변경하기 위해서!



🌤 로그인/로그아웃 상태에 따른 Nav bar(+modal) 변경

로그인/로그아웃 상태는 로그인 시 발행되는 JWT의 유무로 확인했다. Nav 컴포넌트에서 isLoggedIn이라는 state변수를 만들고, getItem으로 session storae에서 token을 꺼낸 값을 거기에 담았다.

token이 존재하면 해당 토큰의 값(string)이 전달될 거고(=true), 없으면 null이 나와 false가 될 것이라고 생각했음!

  constructor(props) {
    super(props);
    this.state = {
      ...,
      isLoggedIn: JSON.parse(window.sessionStorage.getItem('accessToken')),
    };
  }

그래서 먼저 삼항연산자를 이용해 조건에 따라 다른 요소로 map을 돌렸다.

<ul className="rightMenu">
{
  !isLoggedIn
    ? NAV_DATA.slice(3, 6).map((nav, index) => (
        <li
          key={index}
          onClick={() => {
            index >= 2 ? navToggle('비회원') : navToggle(nav);
          }}
        >
          {nav}
          <hr />
        </li>
      ))
    : NAV_DATA.slice(6).map((nav, index) => (
        <li
          key={index}
          onClick={() => {
            index === 0 ? logOut() : navToggle(nav);
          }}
        >
          {nav}
          <hr />
        </li>
      ))
}
</ul>

이때 NAV_DATA의 index랑 NAV_DATA.slice(..) 한 상태의 index랑 다른데 그걸 동일하게 생각해서... 작동이 제대로 안 되어서 뭐가 문제지?!?!!? 하고 애먹었다.
그러니까 NAV_DATA에서 index 6인 요소는 NAV_DATA.slice(6)에서는 index 0이라는 걸 간과함!!!!😤😤

* 참고로 NAV_DATA 는 이렇게 생겼다.

const NAV_DATA = [
  '제품보기',
  '읽기',
  '검색',
  '로그인',
  '회원가입',
  '카트',
  '로그아웃',
  '카트',
  '주문내역',
];
  • NAV_DATA.slice(3, 6)이 로그아웃 상태일 때 보여줄 nav고,
  • NAV_DATA.slice(6, 8)이 로그인 상태일 때 보여줄 메뉴들!
  • 제품보기, 읽기, 검색은 공통이고 leftMenu라는 클래스명을 가진 ul tag에서 이미 map 되어 있다.

그리고 로그인/로그아웃 시마다 nav의 상태를 바꿔주기 위해서 로그아웃 메서드 내부 & componentDidUpdate 내부에 isLoggedIn을 갱신하는 setState 메서드를 넣어줌!

componentDidUpdate() {
  if (
    this.state.isLoggedIn !==
    JSON.parse(window.sessionStorage.getItem('accessToken'))
  ) {
    this.setState({
      isLoggedIn: JSON.parse(window.sessionStorage.getItem('accessToken')),
    });
  }
}
>
logOut = () => {
  window.confirm('로그아웃 하시겠습니까?') &&
    window.sessionStorage.removeItem('accessToken');
  this.setState({
    isLoggedIn: JSON.parse(window.sessionStorage.getItem('accessToken')),
  });
};

처음에는 componentDidUpdate에만 넣어주면 로그인/로그아웃 시 둘 다 자동으로 상태반영이 될 줄 알았는데, logOut 메서드가 session storage에서 토큰을 삭제하는 역할밖에 안 해서, 컴포넌트를 업데이트 시키지는 않는 모양이다. 그래서 따로 적어주니까 작동함~~~

🌤 query string을 이용한 검색 결과 받아오기

인스타그램 클론코딩 당시에도 필터링(자동완성) 기능을 구현했었는데, 그땐 그냥 모든 계정 정보들을 대상으로 startsWith 메서드를 사용해 프론트단에서 어찌어찌 하는... 그런 방식이었다. 근데 데이터가 무한정으로 많아지면 그런 식으로 할 수는 없기 때문에! 프론트에서 클라이언트의 요청을 보내면 백엔드에서 그에 따른 데이터를 걸러서 전송해 준다. 보통 쿼리스트링을 이용한다고 하고, 우리 팀도 쿼리스트링으로 구현함!

사실 미화님은 일찌감치 완성해 주셨는데 프론트에서 방법을 몰라(ㅠㅠ..) 못하고 있다가 마감 전날 새벽에 악상이(??) 떠올라서 극적으로(!!) 성공했다.

먼저 form과 input 태그에 들어가야 할 속성들

  • action: 쿼리스트링(?[name]=[value]) 직전까지의 fetch 받을 url
  • method: 쿼리스트링이 필요하므로 get!
    *post 방식은 폼 데이터를 별도로 첨부해서 서버로 전송하는 방식이고, 쿼리스트링과는 별도..! 정확한 용도가 아직 와닿지 않아 추후에 조금 더 공부해야겠다.
  • name: ?[A]=[b] 에서 A! 백엔드와 맞춰야 하는 키값.
<form action="/products/search" method="get">
  <input name="search_name" />
</form>

저렇게만 세팅해 주면 클라이언트가 input에 뭘 입력하고 submit했을 때 localhost:3000/products/search?search_name=(클라이언트가 검색한 단어)로 이동한다.

그러면! 그 페이지에서 저 url 뒤에 붙은 쿼리스트링을 똑 떼서 fetch 주소를 동적으로 변화시키면 검색 결과를 받아올 수 있다~~~

현재 페이지의 쿼리스트링 추출하는 법
해당 컴포넌트에 react-router-dom을 임포트해 withRouter를 걸어주고 this.props.location.search!

getData = () => {
  console.log(`${PRODUCTS_BASE_URL}/products${this.props.location.search}`);
  fetch(
    `${PRODUCTS_BASE_URL}/products/search/item${this.props.location.search}`
  )
    .then(productData => productData.json())
    .then(productData => {
      this.setState({
        products: productData['result'],
      });
    });
};

문제는 검색결과 페이지에서 스킨 검색결과 이런 식으로 표시하고 싶었는데 그것도 쿼리스트링에서 가져오려니 한글의 경우 유니코드화 되어 있어서 %EC%8A%A4%ED%82%A8 검색결과 이렇게 나왔다. 😑 그래서 decodeURI라는 함수를 알아내서 쓰고, +기호로 바뀌는 띄어쓰기를 다시 원상태로 되돌리기 위해 아래처럼 코드를 짬!

<h1>
  {`${decodeURI(this.props.location.search.split('=')[1])
    .split('+')
    .join(' ')} 검색결과`}
</h1>


🌤 Element.getBoundingClientRect()



🌈 아쉬운 점

1. 구현하지 못한 Filtering 기능
백엔드 미화님께서 쿼리스트링으로 필터링을 해 주셨는데 지식 부족으로 완성하지 못했다...ㅠ.ㅠ url로 fetch 받아오는 건 검색 기능이랑 비슷할 것 같은데 checkbox onChange 시마다 submit하는 방법을 몰라서(사실 그렇게 구현하는 게 맞는 건지도 모르겠다!!!) & 시간 부족으로 일단 PASS.... 위케아팀 레포 훔쳐보고 작동 원리를 알아내야겠다 😎😎

2. IntersectionObserver API 사용 X
스크롤 이벤트에 사용할 수 있는 API인 InetersectionObserver! 예전 인스타그램 클론 때 무한스크롤을 구현하면서 어렴풋이 알게 되었는데, 언젠가 한 번 써먹어 봐야지 했지만... 아는 거 구현하느라 바빠서 시도조차 못 했던 게 아쉬움😢 하지만 또 다른 새로운 거... getBoundingClientRect를 배웠다는 데 의의를...!

3. Pagination
이솝 홈페이지엔 상품 리스트가 캐러셀 형식으로 되어 있어서 페이지네이션이 없지만! 추가해서 하려면 할 수 있었을 텐데.... 일단 있는 것부터 하다가 하지 못한 게 쪼끔 아쉽다.

4. 시간에 쫓겨서 조금은... 결과에 집중하지 않았나.....
중간에 인원 변동이 있어서 역할 분배가 고르지 못하게 됐고, 갑작스레 일이 많아지는 바람에 와다다다 하느라 같은 프론트인 휘성님&원근님이랑 충분한 코드 공유를 못한 것 같아서 죄송하고 아쉬웠다. 결과물보다 중요한 건 협업이라는 점을 명심하자!!!!!!!!!



🌈 배운 점

1. 협업은 생각보다 어렵다!
일을 쪼개서 하면 할당량이 줄어드니까 훨씬 쉽지 않을까?! 싶지만... F-F, B-B, F-B간의 끊임없는 소통이 정말정말 필요하다. 백엔드에서 하는 일을 충분히 이해해야 하고, 또 프론트끼리도 각자의 진행상황만 보고할 게 아니라 서로의 코드를 살펴보고 얘기를 많이 많이 나누는 게 중요하다고 느꼈다. 나눠서 일을 맡더라도 결국은 그걸 합쳐서 하나의 사이트를 구성해야 하고, 그러려면 전체적인 로직이 연결되어 있어야 하기 때문에..... 우리는 컴포넌트별로 롤을 나눴어서 그게 더더더더 필요했다.

2. 그럼에도 불구하고! 협업이기에 가능했던 것 (I💛Team WeSop)
처음에도 여유로운 느낌은 아니었지만, 막판엔 정말 시간이 부족해서 끼니도 걸러가면서 하루종일 키보드만 두드렸다😫😫😫 이걸 만약 혼자 했으면 정말 너무 서럽고... 배고프고.... 피폐해졌을 것 같은데...... 너무 배려 넘치고 재밌고 잘 맞는 팀원들과 함께해서 지치지 않고 해낼 수 있었다ㅠ.ㅠ 진짜로!!! 끊임없는 격려와 칭찬과 이상한 개그들 덕분에 프로젝트 기간 내내 너어어어무 즐겁고 행복했다. 너무 고맙고 소중한 팀 위솝💚🍀🍀

profile
DevelOpErUN 성장일기🌈

7개의 댓글

comment-user-thumbnail
2021년 5월 23일

와 정리까지 깔끔하고 예쁜 도은님 ❣️❣️ 도대체 못하시는게 뭔가요!!
저희 팀 다른 프론트분들도 모두 유능하셨지만 도은님이 계셔서 더 든든하게 느껴졌어요
항상 나긋하게 팀원들 도닥여주시고 잘 이끌어주셔서 감사했습니다 위솝팀 최고!! 🥰🥰

1개의 답글
comment-user-thumbnail
2021년 5월 23일

여기가 CSS 맛집이라던데.. 애니메이션 효과 너무 잘 봤어요! 고생하셨습니다!

1개의 답글
comment-user-thumbnail
2021년 5월 26일

ㅋㅋㅋㅋ 정말 css 맛집이네요 . 시연하실 때 너무 감탄했습니다. !

1개의 답글