ROCKA 프로젝트 리뷰

HELLO WORLD🙌·2020년 8월 7일
0

Mini-Project

목록 보기
6/8

Description

  • 코딩 1개월 차 Wecode 8기 4명의 수강생들이 쇼핑몰 '라카' 의 UI와 기능을 직접 구현하는 프로젝트
  • Front-end 2명과 Back-end 2명이 팀을 이뤄 개발 했습니다.
  • 프로젝트 기간 : 2020.5.25 ~ 2020.6.05 | 2 weeks

Tech Stack

Demo

https://youtu.be/T-Cq8Tb18IM

Pages

  • 회원가입
  • 로그인
  • 메인 페이지
  • 제품 리스트 페이지
  • 제품 상세 페이지
  • 장바구니
  • 스토어

What did I do

메인페이지

  • 스크롤이벤트를 활용하여 스크롤값에 따라 변화하는 CSS 스타일링 적용
  • keyframes animation으로 이미지슬라이드쇼 구현

제품 리스트 페이지

  • 클론 프로젝트이지만 사용자 입장에서 기존 사이트의 불편함을 인식하고 더 나은 UI/UX에 대해 고민해보고 적용
  • sort 메소드를 사용하여 조건별로 상품정렬 기능 구현
  • API에서 불러온 카테고리 분류와 제품리스트 데이터를 map 메소드 이용하여 목록구현
  • 카테고리별로 이동하는 동적라우팅 구현

스토어 페이지

  • 유저가 선택한 옵션 및 입력한 input값에 따른 검색기능

제품 상세 페이지

  • 옵션 삭제 기능 추가

Code Review

스크롤에 따른 스타일링 적용

//Main.js

//스크롤 위치 구하는 함수
  onScroll = (e) => {
    const scrollTop = ("scroll", e.srcElement.scrollingElement.scrollTop);
    this.setState({ scrollTop });
  };
  
//스크롤 이벤트는 직접 window에 접근해서 선언하는 것이기 때문에 
//componentWillUnmount() 를 통해 해당 컴포넌트가 언마운트 될 때 없애주어야한다. 
//불필요한 메모리 손실을 막고 계속해서 이벤트가 서버에서 돌아가고 있는 상황을 방지할 수 있다.
  componentWillUnmount() {
    window.removeEventListener("scroll", this.onScroll);
  }

  componentDidMount() {
    window.addEventListener("scroll", this.onScroll);
    
    return(
      ...
      // 스크롤 위치에 따라 classname을 부여해주면 그에따라 css 스타일링을 적용할 수 있다.
       <ul className={scrollTop > 1300 ? "pop" : "none"}>
      ...
    //TOP버튼이나 nav바에도 같은 방식으로 적용하였다.
    )
/* Main.scss */ 

.none {
  opacity: 0;
}
.pop {
  transition: all 0.7s ease-in;
  transform: translateY(-100px);
}

신상품 tab 구현

  constructor() {
    super();
    this.state = {
      data : [],
      activeTab: "best",
      tabClass: "activebest",
    };
    
    //onClick에서 받아온 prop(best나 new를 받아옴)을 가지고 state와 className를 바꾸기 위한 함수  
     handleClicked = (prop) => {
      this.setState({ activeTab: prop, tabClass: `active${prop}` });
  };
    
    
    const newSort = [
      ...data.sort((a, b) => b.launchdate.localeCompare(a.launchdate)),
    ];
    // 신상품 정렬을 위해 문자열과 문자열을 비교하는 localeCompare메소드를 활용하였다.
    //api에서 불러온 data배열안에서 "2020-02-20"형식의 lauchdate의 값을 비교해서 큰값이 먼저 오도록해서 최신 날짜순이 되도록 했다. 
    //spread연산자를 사용해서 state를 직접 수정하지않고 새로운 배열로 만든다.
    
    
    const newproduct = [];
    newproduct.push(newSort[0], newSort[1], newSort[2]);
    //최신 순으로 나열된 제품 중 상위3개만 추출해서 넣은 새로운 배열

    const tab = {
      //ProductList는 PRODUCT페이지에서도 제품목록을 리턴하기위해 사용되는 컴포넌트이다.
      best: <ProductList products={bestproduct} />,
      new: <ProductList products={newproduct} />,
    };

return(
  ...
              <div className="tabs">
                <ul className="tabsTitle">
                  <li
                    className={tabClass}
                    onClick={() => this.handleClicked("best")}
                  >
                    <span>BEST</span>
                  </li>
                  <li
                    className={tabClass}
                    onClick={() => this.handleClicked("new")}
                  >
                    <span>NEW</span>
                  </li>
                </ul>
//active의 초기값은 best이므로 베스트제품을 보여주고, 
//위의 버튼 클릭에 따라 state가 바뀌면서 그에따라 다른 값이 전달 된ProductList 컴포넌트를 보여준다
                <ul>{tab[activeTab]}</ul>
...
)
//PRODUCT페이지에서도 sort라는 state를 만들어서 같은 방식으로 적용하였다.
    let list;
    if (this.state.sort === "new") {
      list = <ProductList products={newSorted} />;
    } else if (this.state.sort === "best") {
      list = <ProductList products={bestSorted} />;
    } else {
      list = <ProductList products={notSorted} />;
    }

PRODUCT페이지 썸네일/목록 구현

백엔드 API로 불러온 제품데이터 json형식은 다음과 같다.

{
  "data": [
    {
      "id": 17,
      "name": "워터리 쉬어 립스틱",
      "price_krw": "18000.00",
      "description": "촉촉하게 빛나는 #물빛입술",
      "category": ["LIP"],
      "color": [
        {
          "name": "이고르",
          "stock_quantity": 1000,
          "image_url": "https://laka.co.kr/web/product/option_button/20200323/d91df56843b5f052c0733a607c8538d3.jpg"
        },
        {
          "name": "카미유",
          "stock_quantity": 1000,
          "image_url": "https://laka.co.kr/web/product/option_button/20200323/396244b15425120006155033c2fdb28e.jpg"
        }, 
        
        ...
        
//ProductList.js
render() {
    const map = this.props.products.map((item) => {
      return (
        <Link to="/ProductDetail" className="link" style={{ color: "black" }}>
          <Item
            key={item.id}
            name={item.name}
            price_krw={item.price_krw}
            description={item.description}
            color={item.color}
            outer_front_image_url={item.outer_front_image_url}
            outer_back_image_url={item.outer_back_image_url}
            category={item.category}
          />
        </Link>
      );
    });

    return <ul>{map}</ul>;
  }

//item.js
   const {
      name,
      description,
      color,
      outer_front_image_url,
      outer_back_image_url,
      price_krw,
    } = this.props;

    //color배열안의 객체의 image_url값을 이미지로 전환
    const colors = [];
    for (let i in color) {
      colors.push(color[i].image_url);
    } //빈 배열을 만들고 각각의 url 주소를 넣는다
    const url = colors.map((color, idx) => <img src={`${color}`} alt="" />); 
      // url로 이루어진 배열을 map함수를 이용해 img태그로 변환시켰다

   //15000.00 형식으로오는 price_krw값을 15,000 형식으로 변환
//Number()로 문자열을 숫자로 바꾸고, 사용자의 문화권에 에 맞는 표기법으로 시간, 금액을 리턴하는 toLocaleString메서드를 이용하였다 
     const price = Number(price_krw).toLocaleString();

return (
      <li className="item">
        <div className="front">
          <img src={`${outer_front_image_url}`} alt="" />

          <div className="back"> // hover시 나오는 부분
            <img
              className="product-img"
              src={`${outer_back_image_url}`}
              alt=""
            />
            <div className="item-info">
              <div className="item-desc">
                <p className="title">{name}</p>
                <p> {description}</p>
                <p className="color">{url}</p>
              </div>
              <div className="price">
                <p>KRW {price}</p>
              </div>
            </div>
          </div>
        </div>
      </li>
    );

PRDUCT페이지의 Category 구현


//Category.js

//ALL을 호버하면 나타나는 카테고리bar의 타이틀과 상품수도 백엔드데이터에서 받아오도록 하였다. 

//각각의 타이틀을 클릭시 /product?category=타이틀 의 주소로 라우팅된다.
render() {
    const { category } = this.state;
    let address = this.props.location.search;
    let categoryTitle = address.split("=")[1];
  
	//각각의 카테고리에 대한 제품수를 받았지만 전체수량이 없었기때문에 합계를 구하는 함수를 생성
    const allCount = () => {
      let count = 0;
      for (let i in category) {
        count += category[i].count;
      }
      return count;
    };

    return (
      //카테고리수가 열가지정도 되는 데 각각 div를 만들어줬는데, 불러온 배열 이용해서 map으로 나열해도 됐을것같다.
      <div className="nav_container">
        <div className="nav">
          <div className="category">
            <div className="categoryAll">
      //처음에 ALL이었던 타이틀 누르면 이동하는 카테고리에 맞게 타이틀이 바뀌어야함. 
      //location객체에 담긴 주소값을 활용해서 타이틀을 변환시켰다.
              {categoryTitle === undefined ? (
                <span>ALL</span>
              ) : (
                <span>{categoryTitle}</span>
              )}
              <div className="categoryImg">
                <img
                  className="icon"
                  src="https://laka.co.kr/assets/ko/images/ico/ico_arr.png"
                  alt=""
                />
              </div>
            </div>
            <div className="list">
              <div className="row">
                <span onClick={() => this.props.history.push("/product")}>
                  ALL
                  <span className="count">{allCount()}</span>
                </span>

                <span
                  onClick={() =>
                    this.props.history.push("/product?category=FACE")
                  }
                >
                  FACE
                  <span className="count">
                    {category[0] && category[0].count}
                    //cdm에서 fetch된 후에야 리턴할 값이 존재하는것이다. category에 데이터가 들어와서 true일때 값을 반환가능!
                  </span>

STORE페이지 필터링 구현

class Store extends Component {
  constructor() {
    super();
    this.state = {
      stores: [],
      userInput: "",
    };
  }

  //API에서 불러온 지점목록을 stores라는 프로퍼티의 값으로 저장
  componentDidMount() {
    fetch(`${API}/store`)
      .then((res) => res.json())
      .then((res) => this.setState({ stores: res.data }));
  }
  //자식컴포넌트에서 유저가 입력한 값을 받아와서 state에 업데이트한다.
  onSearchSubmit = (value) => {
    this.setState({ userInput: value });
  };

  render() {
    //state에 담긴 stores에 filter 메소드를 사용하여 스토어목록이 유저 입력값이 주소에 포함된 것만 필터링 되도록했다.
    //빈문자열 상태인 초기에는 전부 보여지게된다. 
    const filtered = this.state.stores.filter((store) =>
      store.address.includes(this.state.userInput)
    );

    return (
      <div className="wrapper">
        <Nav />
        <div className="store-page">
          <img
            src="https://laka.co.kr/laka_skin/images/pc/store_visual.jpg"
            alt=""
          />
          <div className="storeBox">
      //
            <Search onSubmit={this.onSearchSubmit} />
//Search컴포넌트에서 받아온 input값에 따라 필터링된 배열을 StoreList의 props로 전달

            <StoreList stores={filtered} />
//StoreList컴포넌트는 자식컴포넌트인 Store컴포넌트(각각의 스토어들)를 map을 이용해 목록으로return하는 컴포넌트이다.
          </div>
        </div>
        <Footer />
      </div>
    );
  }
}
//Search.js

class Search extends Component {
  constructor(props) {
    super(props);
    this.state = {
      userInput: "",
      selected: "",
    };
  }
//저장된 state값을 부모 컴포넌트로 전달하기 위한 함수
  onFormSubmit = (e) => {
    //form태그에 걸려있는 이벤트기때문에 페이지가 리로딩되는 것을 막아주어야한다.
    e.preventDefault();
    //부모컴포넌트에서 전달받은 props인 onSubmit을 통해 자식컴포넌트의 state값을 전달한다
    this.props.onSubmit(this.state.userInput);
  };

  onFormSelect = (e) => {
    this.props.onSubmit(this.state.selected);
  };

//검색입력창에 쓴 값을 저장
  handleUserInput = (e) => {
    this.setState({ userInput: e.target.value });
  };
//드롭다운에서 선택한 값을 저장
  handleSelect = (e) => {
    this.setState({ selected: e.target.value }, () => this.onFormSelect(e));
    //드롭다운에서 선택시 따로 엔터나 검색버튼클릭을 하지 않으므로 바로 부모컴포넌트로 전달함. 
    //setState가 비동기함수이기때문에 두번째 인자로 부모컴포넌트로 입력값을 전달할 수 있는 콜백함수를 전달하였다. 
  };

  render() {
    const { userInput, selected } = this.state;
    return (
      <div className="Search">
        <form onSubmit={this.onFormSubmit}>
          <select
            className="select"
            onChange={this.handleSelect}
            value={selected}
            className="myClassName"
          >
            <option value="">전체</option>
            <option value="서울">서울</option>
            <option value="경기">경기</option>
            <option value="인천">인천</option>
            <option value="부산">부산</option>
            <option value="대구">대구</option>
            <option value="광주">광주</option>
            <option value="대전">대전</option>
            <option value="울산">울산</option>
            <option value="충청">충청</option>
            <option value="전라">전라</option>
          </select>

          <div className="textInput">
            <input
              type="text"
              value={userInput}
              onChange={this.handleUserInput}
              placeholder="매장명으로 검색하세요."
            />
            <button type="submit" className="searchButton">
              검색
            </button>
          </div>
        </form>
      </div>
    );
  }
}

기억하고 싶은 코드

썸네일 컴포넌트 (Item.js) 에서 커서를 올리면 다른이미지와 텍스트가 나오는데 처음에 onMouseover 이벤트를 이용해서 state를 변화시켜, state에 따라 다른 div가 보이도록 했다.
그런데 뭔가 부드럽게 넘어가는 것 같지않고 버벅이는 느낌이었다.

다른방식을 생각해보니 단순히 CSS hover를 이용하여 호버될때와 안될때의 div 두개를 처음부터 렌더링하고, opacity나 z-index를 조절하는 것이다.

처음 state를 활용한 방식은 가상DOM렌더부터 시작하여 레이아웃, 페인트, Composite되는 과정을 거쳐야하지만,
바꾼 방식은 브라우저종류에 따라 다르지만 크롬에서는 페인팅부터만 다시 적용되기때문에 더 부드러워지는 것 같다. https://csstriggers.com/
그리고 굳이 필요없는 state관리를 한다는 것 자체가 오버인듯함!?

0개의 댓글