1차 프로젝트 회고: WESH (LUSH 클론)

삼안기·2021년 7월 25일
6

회고록

목록 보기
2/3
post-thumbnail

첫 프로젝트다. 설레는 마음으로 시작했지만 쉽지만은 않은 여정. 팀원들과 함께한 첫 협업을 기억하기 위해 기록해둔다.

프로젝트 소개

LUSH clone coding
🔗  LUSH 공식 홈페이지

두근두근 첫 프로젝트!

우리팀은 LUSH 사이트를 배정받았다. 오호 생각보다 사이트가 단순한데? 라고 코린이는 외쳤다. 역시나 코린이의 생각이었고 조져지는 것은 코린이의 몫. 1차 프로젝트는 프론트엔드와 무시무시한 백엔드를 둘 다 다뤄보아야 했다. 코딩을 시작할 때 부터 프론트엔드 위주로 구현해보고 싶었던 나이기에 팀원들에게 양해를 구하고 프론트엔드에 조금 더 비중을 싣고 가기로 했다. 첫 주는 프론트엔드 구현, 두째주는 백엔드 구현으로 일정을 잡고 시작했다.

2주 동안 함께 고생했던 팀원들과 사용했던 기술스택, 협업 도구를 소개한다.


RUSH and LUSH 팀을 소개합니다!

👨‍👩‍👦‍👦 팀 멤버

최민기 ( Frontend, Backend )
김지현 ( Frontend, Backend )
신수호 ( Frontend )
안진근 ( Frontend )

🗓 작업 기간

2021.7.5 - 2021.7.16 ( 총 12일 간 진행 )

🛠 기술 스택

Frontend

⛏    HTML  |  CSS  |  JavaScript(ES6+)  |  React  |  Sass  |  Prettier  |  Eslint

Backend

🔧    Nodejs  |  Express  |  Prisma  |  MySql  |  Bcrypt  |  Jwt

협업툴

⚙️    Slack  |  Trello  |  Github  |  Google meet  |  Zoom



1주차 (Frontend)

1주차 SPRINT

우선 각자 맡을 파트를 나눴다. 나는 구현해보고 싶었던 LUSH의 메인(Main Page, Nav / Footer 제외)을 맡았고 시간이 남으면 제품 상세페이지(Detail Page)를 구현하기로 했다. 우리는 React 초기세팅을 마치고 Trello에 구현할 사항들을 정리했다. 이 때만 해도 완벽한 클론을 할 수 있을 줄만 알았지...... 현실을 마주하고 Trello 카드를 눈물을 머금고 지웠다.

🖥 프론트엔드 주요 구현 사항은

Main Page. Carousel (2 type : Fade-in-out, Slide)
Nav. Category Modal과 Member Tooltip, Search Modal
Footer. Subscribe 창
Login Page. Member / NonMember 분할과 Validation
Signup Page. Validation
List Page. Dynamic Routing
Detail Page. Conut에 따른 Price 변화 및 Review 생성 / 삭제
Cart Page. Cart 기능구현

추가로 Order 관련 페이지와 Shipping 상태를 나타내기로도 했는데 후반으로 갈수록 기존에 구현한 코드를 다듬는데 시간을 할애해 Cart 마저도 구현하지 못한 아쉬움이 있었다. 하지만 첫 주차에는 구현할 수 있는 프론트의 역량을 최대한 발휘해 좀 더 완성도 있는 방향으로 진행했다.

우리팀은 매일 아침 Daily Standup Meeting 뿐만 아니라 하루 일정 사이사이에 짧은 Standing Meeting도 자주 가지면서 서로 상황공유도 하며 기쁨고 고통을 나눴는데, 이 부분이 팀원들에게 가장 힘을 받아 코드를 짤 수 있던 원동력이 되어주었지 않나 싶다. 고통은 나누면 반이 된대요 ^3^......(4명분을 나눠서 결국 두 배가 되었다고 한다.)

Main Page

메인 페이지를 처음 시작할 때 레이아웃부터 빠르게 짜고 이미지만 후에 샥 갈아주고 싶었다. 쓸데없이 디자인 욕심이 앞서서 이상한 데에 시간을 쏟은 듯 했지만(이미지 제작 등... 그래도 생각보다 시간이 많이 안걸려서 다행인) 팀원들이 러시 공홈 이미지보다 좋다고 다들 좋아해주셔서 다행이었다.

아래는 초기 레이아웃 상태.

메인에서는 주요 구현 기능인 Carousel이 있었는데 슬라이드 종류가 두 가지였다.

Fade-in-out
Slide

처음에는 오야리 Slick 라이브러리 갸꿀 하고 쓰려는 찰나! 1차 프로젝트에서는 라이브러리 없이 구현해보자는 청천벽력같은 이야기. 그래! 할 수 있어! 하고 폭풍 구글링에 돌입했다.

▪️ Fade-in-out

우선 이미지가 돌아가면서 나타나는 동작에 대해 이해해야 했다. Slide 형식의 경우에는 여러 이미지가 가로축으로 이어져있기 때문에 Position 조정만 해주면 되었는데 Fade-in-out 형식은 여러 이미지가 보였다 : 안보였다 해야했기 때문에 이미지 여러장이 한 Position에 겹쳐있어야 했다. 그리고 한 이미지 당 3초의 interval로 나타나게 한다.

Fetch 받아온 이미지들을 map 돌려 한 자리에 위치 시킨 후 한 이미지 씩 setInterval 시킨다. 마지막 이미지가 나타나면 state는 처음 이미지로 돌아가고 다시 시작된다. 막상 구현하고 나니 생각했던 것 보다 코드가 간단했다.

▪️ Slide

Slide는 앞서 이야기한 것 처럼 Position을 이용해 브라우저에 보여지는 width 만큼 움직여줬다. 로직은 크게 Fade-in-out과 다를게 없다. 내가 구현한 Slide는 두 가지 이미지로만 했기 때문에 infinite Slide가 아니라 CSS에서 Position만 만져주었다.

▪️ ScrollTop에 따라 보여지는 기능

이 기능은 프로젝트 발표 전날 부랴부랴 넣은 기능 중 하나인데, 스크롤 위치를 받아와서 특정 위치가 되면 그 자리에 있는 컴포넌트가 보여지게 되는 것이다. 스르륵- 하는 게 별 기능 아닌데 시각적으로 괜찮아서 끼워넣었다.

▪️ Search Modal

이 기능은 Nav에 있는 기능이지만 Modal의 로직이 궁금해서 직접 짜보았다.

Detail Page

디테일 페이지에서부터 똥줄이 타기 시작했다. 앞서 구현한 Carousel이 시간을 너무 많이 잡아먹어서 빠르게 레이아웃만 잡고 살을 붙여 나갔다. 리스트 페이지와 함께 동적 라우팅도 할 계획이었지만 2주차부터 백엔드도 해야했기 때문에 우선 mockdata로 밀어붙이고 리뷰 쪽도 시간이 남으면 진행하기로 했다. ( Build 지옥의 원인이 여기 있을 줄은 꿈에도 몰랐지... )



2주차 (Backend)

2주차 SPRINT

2주차부터 백엔드에 돌입해야 했다. 손발이 나고 눈물이 벌벌 떨렸다. 게다가 2주차 화요일까지 프론트를 잡고 있었기 때문에 구현할 시간이 많지 않았다. 나는 리스트 페이지에서 Product Data를 받아오는 API를 짯는데, 과제로 했던 스타벅스 리스트 페이지 구현처럼 전체 데이터를 받아오다가 해당 카테고리에 맞는 데이터만 받아오게끔 구현해야 한다는 말을 듣고 눈물을 찔끔 흘렸다.

어떻게 하는거죠...?

그렇다면 해당 카테고리의 Query String을 받아오거라!
넵! 그런데 어떻게 하는거죠?

📲 백엔드 주요 구현 사항은

Main Page. Carousel (2 type : Fade-in-out, Slide)
Nav. Category Modal과 Member Tooltip, Search Modal
Footer. Subscribe 창
Login Page. Member / NonMember 분할과 Validation
Signup Page. Validation
List Page. Dynamic Routing
Detail Page. Conut에 따른 Price 변화 및 Review 생성 / 삭제
Cart Page. Cart 기능구현

▪️ Product API

this.props.location.search가 받는 값이 Query String인 게 흥미로웠다. this.prop 가 반환하는 배열을 여러 곳에서 다르게 사용할 수 있을 것 같다.

아직 로직을 완벽하게 이해하진 못했지만 차근차근 곱씹어봐야겠다.



Refactoring

프로젝트가 끝나고 리팩토링 시간을 가졌다. 프로젝트 기간동안은 기능구현에 급급해서 지저분한 코드도 눈가리고 아웅했지만 리팩토링을 통해서 좀 더 깔끔하게 작성할 수 있었다. 그 중 몇 가지만 소개한다!

너무 당연하지만 그 땐 당연하지 않았던 코-드!

Carousel을 사용하다보면 여러 이미지가 필요해지고 그에 따라 이미지 태그의 코드도 늘어나게 된다.

▪️ 해결방법

img 태그에 해당하는 부분을 map을 돌려 코드를 간결하게 만들어줍니다. 우선 map을 사용하기 위해 이미지들은 자식 컴포넌트로 빼주고 mockdata를 props로 넘겨줍니다.

▪️ BLOCKER

  • 기존 코드에서는 className에서 active 줄 css 속성을 state 값과 이미지와 직접적으로 일치하게 만들었지만, refactoring 과정에서 하나로 줄어든 img 태그로 인해 변경되는 state 값을 변경되는 이미지와 일치시켜주어야 했습니다. 이 과정에서 map이 돌아가는 mockdata의 id 값을 이용하였습니다.
  • 부모 컴포넌트에서 사용한 state 값을 자식에게 넘겨줄 때 자식 컴포넌트에 props를 넘겨줍니다.

▪️ 이전 코드

class MainSlide extends Component {
  constructor() {
    super();
    this.state = {
      img: 1,
    };
  } 
  // 중략
render() {
    return (
      <div className="MainSlide">
        <div className="mainSlideWrap">
          <div className="mainSlideImageWrap">
	// 이미지 태그 시작 (map 돌릴 구간).
            <a className="mainSlideClick" href="/">
              <img
                className={
                  this.state.img === 1 ? "mainSlideImg active" : "mainSlideImg"
                }
                src={`./images/main_slide_4.jpg`}
                alt="비누"
              />
            </a>
            <a className="mainSlideClick" href="/">
              <img
                className={
                  this.state.img === 2 ? "mainSlideImg active" : "mainSlideImg"
                }
                src={`./images/main_slide_5.jpg`}
                alt="비누"
              />
            </a>
            <a className="mainSlideClick" href="/">
              <img
                className={
                  this.state.img === 3 ? "mainSlideImg active" : "mainSlideImg"
                }
                src={`./images/main_slide_6.jpg`}
                alt="비누"
              />
            </a>
	// 이미지 태그 끝.
          </div>

리팩토링한 코드

//parent component
class MainSlide extends Component {
  constructor() {
    super();
    this.state = {
      imgIndex: 1,
      mainSlides: [],
    };
  }
// 중략
render() {
    return (
      <div className="MainSlide">
        <div className="mainSlideWrap">
          <div className="mainSlideImageWrap">
            {this.state.mainSlides.map(slide => {
              return (
                <MainSlideImg
                  id={slide.id}
                  img={slide.img}
                  imgIndex={this.state.imgIndex}
                />
              );
            })}
          </div>  
// child component
class MainSlideImg extends Component {
  render() {
    return (
      <div className="MainSlideImg">
        <Link className="mainSlideClick" to="/">
          <img
            className={
		// 부모 컴포넌트에서 받은 imgIndex와 id를 비교.
		// 각각 1, 2, 3 의 값을 가지고 있어서 비교해 줄 수 있다.
              this.props.imgIndex === this.props.id
                ? "mainSlideImgs active"
                : "mainSlideImgs"
            }
            src={this.props.img}
            alt="mainSlideImg"
          />
        </Link>
      </div>
    );
  }
}

2. Fetch 받아온 데이터의 배열 length 사용하기

Carousel 관련 코드를 짜면서 기존에는 내가 넣어준 이미지 개수만 고려해서 특정 number를 넣어주었다. 재사용성을 고려해 fetch 받아온 데이터의 이미지 개수, 즉 배열의 개수를 뽑아 사용했다.

▪️ 해결방법

this.state.mainSlides.length

▪️ BLOCKER

데이터 중 이미지를 포함하지 않을 경우도 고려해봐야겠다.

▪️ 이전코드

imgSlideLeft = () => {
    if (this.state.img > 2) {
      this.setState({
        img: 1,
      });
    } else {
      this.setState({
        img: this.state.img + 1,
      });
    }
  };
  imgSlideRight = () => {
    this.setState({
      img: 3,
    });
    if (this.state.img <= 1) {
      this.setState({
        img: 3,
      });
    } else {
      this.setState({
        img: this.state.img - 1,
      });
    }
  };

▪️ 리팩토링한 코드

imgSlideNext = () => {
    const { imgId, mainSlides } = this.state;
    this.setState(
      imgId > mainSlides.length - 1 ? { imgId: 1 } : { imgId: imgId + 1 }
    );
  };
  imgSlidePrev = () => {
    const { imgId, mainSlides } = this.state;
    this.setState({
      imgId: mainSlides.length,
    });
    this.setState(
      this.state.imgId <= 1
        ? { imgId: mainSlides.length }
        : { imgId: imgId - 1 }
    );
  };


Build

빌드 과정에서 자꾸 에러가 발생했다. 어디선가 메모리가 누수되고 있다는 소린데... 누수 수리를 다녀왔다.

컴포넌트를 하나하나 지우며 add / commit / push 를 반복하여 detail 페이지에서 누수를 찾았다. 그 페이지에서도 다시 하위 컴포넌트를 지우며 찾아보니 원인은 사용하던 react-icon 중 하나였다! ( 이 친구를 지우니 build )성공.

왜 그런지는 아직 찾아보는 중이다.



프로젝트를 마치며

그동안 프로젝트에 기대가 많았다. 드디어 배운 것들을 써먹을 수 있겠구나! 했지만 쉽지 않은 여정이었다. 짜보니 쉬웠던 코드들은 짜기 전까지 나에겐 결코 쉽지 않았고 프로젝트가 끝난 지금까지도 잘 이해가 안되는 코드들도 더러 있다. 특히나 백엔드에 취약한 나로서는 간단한 API인줄 알고 덤볐다가 Query string을 만나고 대멘붕이 찾아오기도 했다.

하지만 쉽지 않은 만큼 얻어가는 것도 많았다. 모호하게 알고 있던 개념들도 다시금 정리해서 이해할 수 있던 시간이었다. 이해못한 코드들은 2차 프로젝트를 하며 다시 정리해 볼 예정이다. 다음 프로젝트에서는 프론트엔드를 전담할 건데, 새로 사용해 볼 함수형 컴포넌트, Hook, Styled-componet 등 부담 가득하지만 또 설레기도 한다.

1차 프로젝트 목표가 팀원에게 폐끼치지 않고 제역할을 다해내자 였는데 잘 되었는지 반성한다. 이번 프로젝트의 과정에서나 발표에서나 감사하게도 디자인에 대해 긍정적인 피드백이 있었는데 다음엔 코드로 칭찬받고 싶은 마음이 들었다.

그리고 프로젝트 이후 Refactoring! 정말 중요하다. 새로운 기능들도 물론 해내면 좋지만 내가 짠 코드를 정리하고 발전시키면 앞으로 코드를 짤 때 훨씬 능률적으로 짤 수 있다는 것을 알게 되었다.

모쪼록 나의 한계와 수준을 알 수 있었던 1차 프로젝트. 2차에서는 그걸 넘어보자.


우리팀, 고생했습니다!

profile
문제를 해결해야지

4개의 댓글

comment-user-thumbnail
2021년 7월 25일

민기님 회고록 잘 읽었습니다. 2주간 함께 프로젝트를 진행하며 우리 팀 참 우여곡절이 많았지만 그럼에도 불구하고 으쌰으쌰하면서 다 같이 이겨냈네요 ㅎㅎ 결론적으론 멋진 우리의 페이지가 탄생하게 되어 기쁩니다! 구현하지 못해 아쉬운 부분도, 좀 더 보완해야할 부분도 있지만 그런 부분들은 틈틈이 열심히 공부도 하고, 2차 프로젝트를 통해 더 발전된 모습을 보여주고 싶네요 ㅎㅎ 정말 정말 수고 많으셨습니다 ! 화이팅 ! 주말동안 푹 쉬시고, 재정비 하셔서 곧 다가올 2차도 화이팅해봅시다 ! 감사했습니다.

1개의 답글
comment-user-thumbnail
2021년 7월 27일

사만키님 역시 잘쓰셨군요! 만키님의 디자인감각은 정말 무시무시합니다. 마지막 프로젝트때 그래도 같은팀이되서 한번이라도 같이 협업해볼기회가 있어 좋습니다. 2차 화이팅 하자구요 오키야~~~

답글 달기
comment-user-thumbnail
2021년 7월 28일

👏👏👏

답글 달기