라인프렌즈 클론 프로젝트 회고록

devCecy·2020년 12월 27일
4

Project

목록 보기
1/2
post-thumbnail

위코드에서 4주간 진행하는 Foundation 기간을 완료한 후 시작한 1차 프로젝트. 그 마무리를 위한 회고록🌱

1. 클론 프로젝트 소개


✔️ 클론사이트 명 : 라인프렌즈 샵 (https://brand.naver.com/linefriends)
✔️ 팀명 및 구성 : 라인아미고스 / 프론트) 안미현, 이은진, 강경오 / 백엔드) 문승희, 오승현
✔️ 프로젝트 기간 : 2020.12.14 ~ 2020.12.24
✔️ 최종 결과물 영상 : https://www.youtube.com/watch?v=-LT4xFDPEIU
✔️ 담당 페이지 : 로그인, 회원가입, 상품리스트 페이지
✔️ 담당 페이지 세부 구현사항

로그인)

  • 레이아웃
  • access_token를 활용하여 서비스 제한 및 제공여부 구현

회원가입)

  • 레이아웃
  • 유효성 검사 기능 구현

상품 리스트)

  • 레이아웃
  • fetch API를 활용한 Data rendering
  • Qurey string을 활용한 filter 기능 구현
  • Query string를 사용하여, 상품리스트에서 상세 페이지로 이동
  • 상품 리스트 Pagination 기능 구현
  • Modal창을 이용한 상품 상세 미리보기 기능 구현

추가구현 하고싶은 것)

  • 로그인) 로그인 상태 유지
  • 회원가입) 핸드폰번호 인증
  • 상품리스트) 무료배송 버튼
  • 상품리스트) 상품상태 라벨 (New, Best, Sold out)

2. 구현한 기능 및 코드정리

🎈로그인 페이지

🌈 라인프렌즈 스토어는 로그인, 회원가입 페이지 모두 네이버의 레이아웃을 사용하고 있다. 네이버의 로그인/회원가입 페이지는 정말 깔끔 그 자체 인것 같다.

👩🏻‍💻 access_token를 활용하여 서비스 제한 및 제공여부 구현

  • 로그인 버튼에 onClick속성을 이용하여 handleClick이라는 함수가 실행되도록 했다. 그 안에서 fetch함수를 통해 서버에 접근하며, 사용자가 입력한 id와 pw가 서버에 있는지 확인했다.
  • id와 pw는 POST메소드를 통해 불러오며, 백앤드 서버에서 반환하는 메시지가 SUCCESS일 경우 즉, 사용자가 입력한 id,pw가 서버에 존재할 경우 메인페이지로 이동한다.
  • 또한, 서버에서 보내준 access_token이 존재할 경우 브라우져 로컬스토리지에 토큰을 저장해 준다. 토큰을 저장해 주는 이유는 추후 로그인시 토큰을 통해 사용자가 장바구니 혹은 결제페이지에 접근 할 수 있도록 하기 위해서다. 이번 프로젝트에서는 장바구니 및 결제페이지의 기능을 구현하지 않았기 때문에, 토큰 여부를 확인하여 로그인 완료시 메인페이지 상단 우측의 로그인 버튼을 마이페이지로 변경하는 기능에 사용했다.
 handleClick = () => {
    fetch('http://10.168.1.140:8000/user/signin', {
      method: 'POST',
      body: JSON.stringify({
        username: this.state.id,
        password: this.state.pw,
      }),
    })
      .then((response) => response.json())
      .then((response) => {
        if (response.message === 'SUCCESS') {
          this.props.history.push('/')
        }
        if (response.access_token) {
          localStorage.setItem('token', response.access_token)
        } else {
          alert('아이디 또는 비밀번호가 일치하지 않습니다.')
        }
    })
  }

📌 기억해두고 싶은 코드

처음에는 idChangepwChange함수를 각각 만들어 주었으나, 두 함수는 초기state를 제외한 모든 구조가 동일하기 때문에 아래와 같이 리팩토링 해줄 수 있었다.

setState안의 [id]는 비구조화 해준 event.target.id이며, this.state.idid동일하기 때문에 '이것은 비구조화해준 id다!'를 나타내기 위해 대괄호를 사용해 준다.

아주 간단한 코드지만, 꼭 기억해 두어 앞으로도 깔끔한 코드를 많이 작성하고 싶다.

handleIdPwChange = (event) => {
    const { id, value } = event.target
    this.setState({
      [id]: value,
    })
  }

🎈회원가입 페이지

👩🏻‍💻 유효성 검사 기능 구현
회원가입 페이지는 모든 input마다 유효성 검사가 실행된다. 유효성 검사 코드를 작성하다 정규표현식이라는 것을 알게 되었다.

 const emailValidation = /^[a-z0-9_-]{5,20}$/ ;
 const pwValidation = /^.*(?=^.{8,16}$)(?=.*\d)(?=.*[a-zA-Z])(?=.*[~,!,@,#,$,*,(,),=,+,_,.,|]).*$/ ;

가장 굵직한 규칙을 적어보자면,

  • ^는 정규표현식의 시작, $는 종료를 나타낸다.
  • []는 문자열의 집합 혹은 범위를 나타내며 여러 조건을 적을 경우 그 사이는 -으로 나누어준다.
  • {}는 문자열의 횟수 혹은 범위를 나타낸다.
  • emailValidation/^[a-z0-9_-]{5,20}$/ 를 해석해보면, 영문 소문자, 숫자 혹은 '_',-' ' 두개의 특수문자만을 사용할 수 있으며, 5-20자를 사용해 아이디를 만들어 주어야 함을 나타낸다.

📌 코드 50줄은 줄였던 부분
처음에는 id, pw, 생년월일 등 모든 input의 유효성 검사를 각각 하나의 함수로 만들어 실행시켜 주었다. 그러자, 코드가 어마무시하게 길어졌다. 모두 각각의 함수로 빼주기 보다, 유효성 메세지가 생성되어야하는 자리에서 바로 삼항연산자를 사용해 아래와 같이 바꿔주자 코드가 아주 깔끔해졌다.

 <div className='pwBox'>
  <span className='label'>비밀번호</span>
  <input
    className='idPwBox'
    id='pw'
    type='password'
    value={pw}
    onChange={this.handleValueChange}
    onKeyUp={this.handlePwValidation}
    />
  <span className='validationMassage'>
    {
      (isValid && !pw ? '필수 정보입니다.' : '', 
   isValid &&!pwValidation.test(pw)
   ? '8~16자 영문 대/소문자, 숫자, 특수문자를 모두 사용하세요.' : '') 	} 
  </span>
</div>

👩🏻‍💻 성별 체크는 드롭박스 형식으로 이루어져있는데, 처음에는 select와 option태그의 존재를 몰랐으므로, div로 박스를 만들고 onClick이벤트로 박스가 나타나고, 성별을 선택하는 모든 이벤트를 만들어 주었다. 그러자 이 간단한 박스를 만드는데 또 어마무시한 코드를 적어주어야 했다. 그러나 select태그를 사용하자 드롭박스가 짜잔! 나타났으며, onChange이벤트 만으로 성별체크를 간단히 할 수 있었다.

<span className='label'>성별</span>
<select
  className='genderDropdown'
  id='gender'
  value={gender}
  onChange={this.handleValueChange}
>
  <option value='none'>선택안함</option>
  <option value='female'>여성</option>
  <option value='male'>남성</option>
</select>
<span className='validationMassage'>
  {isValid && gender === 'none'
    ? '필수 정보입니다.' : ''}
</span>

👩🏻‍💻 회원가입 버튼을 클릭하면, 아래의 fetch함수가 실행되며, 로그인과 마찬가지로 POST메소드를 통해 input박스안에 적힌 value들을 서버로 전달한다. 이때가 처음으로 백엔드와 진짜 통신을 했던 경험이라 기쁘고 신기했다. 👻

 fetch('http://10.168.1.140:8000/user/signup', {
      method: 'POST',
      body: JSON.stringify({
        username: email,
        password: pw,
        name: name,
        gender: gender,
        date_of_birth: `${birthYear}-${birthMonth}-${birthDay}`,
        country_code: countryCode,
        phone_number: phoneNumber,
      }),
    })
      .then((response) => response.json())
      .then((response) => {
        if (response.message === 'SUCCESS') {
          alert('라인 아미고스샵에 오실걸 환영합니다!')
          this.props.history.push('/login')
        }
      })
  }

🎈상품리스트 페이지

🌈 상품리스트는 간단해 보이지만 그 안에 다양한 기능 구현이 필요했다. 이때, 많은 개발자들의 노고 또한 깨닳을 수 있었는데, 사이트 내에서 조금이라도 동적으로 움직이는 부분이 있다면 자연적으로 생긴 기능은 단 한개도 없으며, 모두 개발자들의 어루만짐(?)으로 태어난 것이기 때문이다. 이때부터 모든 사이트나 어플을 볼때마다 도데체 이 기능은 어떻게 구현한거지?라는 생각과 와-나도 이 기능 구현해 보고싶다, 와, 이건 진짜 섬세하게 만드셨네 등등의 아주 조금은 더 사용자가 아닌 개발자 입장으로서의 생각을 가질 수 있었다.

시작은 참 많은 import로...👀

import React, { Component } from 'react'
import Products from './component/Products'
import Filters from './component/Filters'
import SideCategory from './component/SideCategory'
import Header from '../../Components/Header/Header'
import Footer from '../../Components/Footer/Footer'
import ImgPurchInfo from '../ProductDetail/Component/ImgPurchInfo'
import './ProductList.scss'

👩🏻‍💻 fetch API를 통한 Data rendering
상품 리스트 페이지에는 백엔드로 부터 받아와야할 데이터들이 많았다. 상품 이미지나 리뷰 수, 평점 등 페이지가 처음 랜딩되는 순간 보여져야 하는 데이터들을 componentDidMount 함수안에서 fetch함수를 통해 모두 요청해 주었다. 아래 코드는 전체 상품 데이터를 불러오는 오기 위해 적은 것이며, 전역변수로 미리 LIMIT=20을 선언해주어 사용했다. 처음 상품리스트 페이지가 랜딩 될때 20개의 상품만 보여주기 위해 아래와 같이 적어 주었다.

componentDidMount = () => {
    fetch(`http://10.168.1.149:8000/product/products_info?limit=${LIMIT}`)
      .then((response) => response.json())
      .then((response) => {
        this.setState({
          productArr: response.PRODUCTS,
        })
      })
}

👩🏻‍💻 Pagination
자식컴포넌트에 페이지네이션을 위한 버튼을 만들어 준 뒤, button태그안에 data-idx를 부여해준다. 그리고 onClick이벤트를 이용하여 부모컴포넌트에서 작성한 함수를 가져와준다.

<div className={selectedArr[0] ? 'nowPageNumBox' : 'nextPageNumBox'}>
  <button
    className='nextPageNum'
    data-idx='0'
    onClick={this.props.fetchProduct}
    >
    1
  </button>
</div>
  • 부모 컴포넌트에서 만든 함수는 다음과 같다. 자식 컴포넌트에서 onClick이벤트가 발생하면, 그것을 인자로 받아준다. offset은 페이지가 넘어갈때 그 첫 시작번호를 정해주기 위해 선언해주었으며, data-idx값을 가져오게 된다.

  • 전체 상품을 API를 통해 불러오되, 20개씩, 20 x offset을 시작번호로 가져온다. offset은 idx값으로 선언해주었기 때문에 2페이지 버튼 클릭시 idx값 1과 limit 20을 곱해 2페이지는 20부터 20개의 상품을 불러오는 것이다.

  • 처음, 이 부분의 코드를 작성하고 백엔드와 통신해 보는데 잘 되지 않았다. 그럴때는 백엔드에서 limit과 offset을 어떤 조건으로 선언해 주었는지 확인해 볼 필요가 있다. limit과 offset은 내장함수가 아니라 우리가 선언해주는 대로 사용하는것이기 때문에 프론트와 백에서 서로 다른 조건으로 변수를 선언해 주었을 경우 원하는 결과가 나오지 않을 수 있다.

fetchProduct = (e) => {
 const offset = e?.target.dataset.idx

    fetch(
      `http://10.168.1.149:8000/product/products_info?limit=20&offset=${
        offset * LIMIT
      }`
    )
      .then((response) => response.json())
      .then((response) => {
        this.setState({
          productArr: response.PRODUCTS,
        })
      })

👩🏻‍💻 컴포넌트 재사용하여 모달창 띄우기

  • 상품 이미지에 마우스를 올리면 호버효과를 통해 상품을 미리보기 할수 있는 버튼이 등장한다. 상품 미리보기는 상세페이지의 가장 상단 부분만 나오며, 나는 모달창에 상세페이지 컴포넌트의 일부분인 ImgPurchInfo컴포넌트를 재사용했다.

  • 미리보기 모달창에서 어려웠던 점은 호버버튼을 클릭하면 어떤 상품을 클릭하던 1번 상품만 나오는 것이였다. 알고보니, 1~20까지의 상품이 아래로 한꺼번에 나온것이였고 그러자 레이아웃이 많이 깨졌다. 문제는 상품이미지를 클릭할 때 아이디로 1~20이 모두 선택되었기 때문이었다.
  • 그래서 모든 상품의 정보가 들어있는 productArrmap돌려 그 인자로 받은 el.product_id 와 사용자가 클릭한 아이디가 담겨있는 clickedId와 비교하여 그 값이 같다면 ImgPurchInfo컴포넌트를 반환했다. 그러자 문제가 말끔하게 해결되었다!
<div className={detailModal ? 'modal' : 'modal hidden'}>
  <div 
    className='layout' 
    onClick={this.handleDetailModal}>
  </div>
  <div className='popup'>
    <div className='modalHeader'>
      <span>간략보기</span>
      <button className='closeBtn' onClick={this.handleDetailModal}>
                X
      </button>
    </div>
      {this.state.productArr.map((el) => {
        if (el.product_id === clickedId) {
          return (
            <ImgPurchInfo
              id={el.product_id}
              productName={el.name}
              imgUrl={el.product_image}
              price={el.price}
              reviewArray={rate}
              /> 
             )
          } else {
            return null
          }
       })}
  </div>
</div>

👩🏻‍💻 동적라우팅 활용하여 상세페이지로 이동

  • 상품리스트 페이지에서 상품 하나를 클릭 했을 때 그에맞는 상세페이지로 넘어가기 위해서는 React Router라는 3rd party 라이브러리를 사용해 주어야 한다. 동적라이팅 부분도 따로 블로깅 할 예정이다.

  • 아래 코드는 상품 디테일 컴포넌트에서 적어준 것이며, 디테일 페이지가 랜딩 될때 API주소 끝부분에 this.props.match.prams.id가 생성되어 찍힐 수 있도록 해주었다. 이 부분이 상품의 고유한 번호가 되어준다.

componentDidMount() {
    fetch(`http://10.168.1.149:8000/product/${this.props.match.params.id}`)
      .then((res) => res.json())
      .then((res) => {
        this.setState({
          productData: res.product,
        })
      })
  • 상품리스트 페이지에 나타나는 하나의 상품이미지에 goToDetail이라는 onClick이벤트를 생성해 주었다. goToDetail이벤트는 url끝부분이 productdetailthis.props.id가 더해져 생성된다.
goToDetail = () => { this.props.history.push(`/productdetail/${this.props.id}`)
  }
  • 동적라이팅 기능을 이용하기 위해, withRouter를 설치한 후, 위의 goToDetail함수를 적어준 상품 컴포넌트에 import, export해주는 것도 잊지말아야 한다.
import { withRouter } from 'react-router-dom'
...
export default withRouter(Product)
  • 마지막으로 해주어야 할 작업은, Routes.js파일에 경로를 설정해 주는 일이다. 동적라우팅을 사용하지 않을 경우, 상품 상세페이지의 path/productdetail/ 였지만, 동적라우팅을 사용할 경우, 그 뒤 고유한 id를 받아준다는 의미로 :id로 변경해 주어야 한다.
 <Route exact path='/productdetail/:id' component={ProductDetail} />

1차 프로젝트를 마치며,

내가 클론하고싶어 발표한 라인프렌즈 샵이 프로젝트 사이트로 선정되었으며, PM이 되었다.

📍하고싶었던 사이트가 선정된것은 정말 감사한 일이다. 누구보다 사이트에 대한 애정과 동기부여를 가지고 시작할 수 있기 때문이다. 그리고 PM이 되었다. 처음 진행하는 프로젝트였기에 스스로에게 미숙함을 많이 느꼈다. 또한, 내가 생각하는 방향과 목표는 팀원들 개개인의 원함과 다를 수 있음을 인정하고 알아가게 되는 기간이였으며, 그 중간 지점을 맞춰가는 것 혹은 최선의 방향으로 나가는 것이 쉽지만은 않다는 것 또한 알게 되었다. 1차 프로젝트에서 느끼고 경험한 것들로 인해 2차 프로젝트는 어떻게 진행해야할지 조금은 감이 잡힌 것 같다.

팀원분들의 회고록을 읽고 조금 더 글을 추가하고 싶어졌다.

📍
프로젝트 기간동안 프론트와 백엔드로써의 소통을 자주 이뤄졌다고 생각하나, 사람과 사람으로써의 소통은 과연 충분했나 돌아보게된다. 팀원분들 모두 각자의 자리에서 묵묵히 코딩을 진행하지만, 그 안에서 힘들고 답답한 부분이 많으셨을 텐데 그런 부분을 먼저 묻고 함께 방향을 제시하지 못했음에 스스로가 PM이였음을 언급하기 민망해져버렸다. 그럼에도 끝까지 프로젝트를 완주해 주신 팀원 한분한분들께 감사한 마음 뿐이며, 그런 우리 모두가 대단하다고 생각한다. 짧다면 짧고 길다면 길었을 10일 간의 시간들이 우리가 앞으로 개발자로 살아가며 현업에서 소통하는 문제에 있어 큰 깨닳음과 영감과 더 좋은 방향으로 나아갈 수 있는 기회를 미리 선물해 준것이라고 믿는다.

적다보니 어찌저찌 만들어지는 코드가 아닌 흐름이 명확하게 읽히는 코드를 작성하고 싶다는 갈증을 느낀다.

📍 '1차 프로젝트 한번 해봤으니 2차는 더 많은 기능을 구현해야지'라는 생각보다, 기본적인 것들을 더 잘 적용하며, 코드 리팩토링을 통해 좀 더 깔끔한 코드를 작성해야 겠다는 생각이 든다. 기능구현에 마음이 앞서다 보니 코드 리팩토링의 시간에 많은 시간을 쓸수 없었기 때문이다. 컴포넌트 재사용,변수명 간결하고 명확하게, 스프레드 연산자의 사용, map 돌리는 위치 확인, scss 네스팅, 반응형 구현 등을 통해 2차에는 좀 더 깔끔한 코드를 작성하는 훈련을 해보고 싶다.

마지막으로,
위코드 15기, 라인아미고스, 그리고 나, 정말 우리 모두 수고했다!
💚

profile
🌈그림으로 기록하는 개발자🌈

2개의 댓글

comment-user-thumbnail
2020년 12월 29일

글 잘보고갑니다~ 위코드 직장인 주말 코스도 있나요? 아니면 무조건 3개월 코스만 존재하는건가요?

1개의 답글