1st Project 회고

Jivyy·2020년 6월 7일
2

ESSAY

목록 보기
1/3

Epliogue

각자 프로젝트를 진행하고 싶은 사이트를 발표했고 투표가 진행되었다.
우리 팀이 맡은 프로젝트는 글로벌 샌드위치 브랜드인 서브웨이의 한국 웹사이트.

먼저는 목표 설정이 중요하다고 생각했다. 우리는 모두 취업을 목표로 두고 이 프로젝트에 진지하게 임하는 중이며 물론 제대로 된 결과물을 내는 것이 중요하다. 하지만 개개인의 능력치에 따른 목표 설정 또한 중요하다.

팀의 목표✨

한국 웹사이트를 바탕으로 프로젝트를 진행을 하려 하니 단순히 메뉴와 매장 위치 등의 정보 전달만을 목적으로 하는 사이트여서인지 한국 사이트에는 그 흔한 회원가입, 로그인 조차 없었고 물론 주문하기 기능도 없었다.
(주문기능을 넣으려다가 만 것인지 개발자도구로 뜯어보면 주문하기 div는 있다.....🙄)

1. 추가 기능 구현

우리는 미국 사이트를 참조하여 회원가입/로그인 기능과 주문하기(커스텀 샌드위치 만들기) 기능을 추가해 보기로 했다

2. Trello & Scrum(1 week sprint) & Daily Standup Meeting

우리는 팀으로 일하는 법을 배우는 중이기도 하다.
이런 관점에서 협업을 위한 트렐로는 정말 유용할 듯 싶다.(이전에 다녔던 회사에서 트렐로를 이용했다면 많은 사고가 줄었을 텐데 라는 생각..😅)
각자 어떤 맡아서 진행할 지 세부적.구체적으로 계획할 수 있고, 진행상황을 파악하기 쉽다.
밤 늦게까지 같이 팀프로젝트를 하다 보니 자정 넘어서까지 남아있는 날도 많았는데 자정이 넘었으니 오늘의 스탠드업 미팅을 시작하자고 했던 날도 있었다 😂👍

개인적 목표✨

프론트로 뒤늦게 방향을 바꾸느라 진도가 늦어 리액트를 배운지 대략 1주일만에 리액트로 프로젝트를 진행하게 되었다.
따라서 거창한 목표보다는 내가 경험할 수 있는 것들에 집중하자는 생각이 들었다.

  • CRA, 초기세팅
  • GIT & GITHUB 과 친해지기
  • class 형 컴포넌트에 익숙해지기(State & Props 개념 제대로 잡기)
  • map 과 filter를 사용해보기
    : 원래대로라면 map과 filter를 공부한 뒤 진행해야 했는데 바로 프로젝트에 들어가게 되어서 나도 필터랑 맵 써보고 싶다! 는 생각이 간절했는데 다행히 커스텀에서 토핑페이지를 만들면서 두가지를 모두 자연스럽게 익혔다.
  • fetch를 사용해서 백엔드 데이터와 붙여보기
  • 내가 원하는 eventhandler를 다룰 줄 알게 되기

My Roles

1. 회원가입/로그인

1주 차에서는 프론트 3명이 각각 메인/메뉴/메뉴상세페이지를 맡기로 하였고 나는 이 중에서 메뉴상세페이지와 회원가입/로그인을 맡기로 하였다.
그런데 이전에 진행했던 인스타그램 미니 프로젝트에서 경험해봤던 것보다 복잡한 조건을 걸어 회원가입/로그인 기능을 구현하는 것이 상당히 어려웠다.
또한 실제 서브웨이 홈페이지에는 회원가입/로그인 기능이 없고 미국 사이트에는 있으나 너무나 심플하여 간단하지만 디자인까지 해야 했다.
최대한 심플하고도 서브웨이의 컨셉에 맞게 디자인했다.

부족하지만 내가 작성한 코드를 올려본다.
실제 코딩 공부를 시작한 지 갓 한달 반 된 초보자의 코드임을 감안해주시길 바라며..

import React from "react";
import { Link, withRouter } from "react-router-dom";
import Footer from "../../components/Footer/Footer";
import { URL } from "../../Config";
import "./SignUp.scss";

class SignUp extends React.Component {
  state = {
    email: "",
    isEmailValid: false,
    name: "",
    tel: "",
    pw1: "",
    pw2: "",
  };

  //입력창에 모든 정보가 입력되었는지 확인하고,
  //모든 정보가 입력 되었을때만 이메일 양식 체크로 넘어간다.
  checkValid = () => {
    const { email, name, tel, pw1, pw2 } = this.state;
    {
      !email || !name || !tel || !pw1 || !pw2
        ? alert("입력되지 않은 정보가 있습니다. 다시 확인해주세요")
        : this.emailCheck();
    }
  };

  //입력된 값을 state에 저장해주기
  handleInput = (e) => {
    this.setState({ [e.target.name]: e.target.value });
  };

  //비밀번호&비밀번호 확인이 일치하는지 확인해서 ture or false 를 반환한다.
  doesPasswordMatch() {
    const { pw1, pw2 } = this.state;
    return pw1 === pw2;
  }
  //패스워드가 일치하는지 확인해서 유저에게 피드백을 준다.
  renderFeedbackMessage() {
    const { pw1, pw2 } = this.state;

    if (pw2) {
      if (!this.doesPasswordMatch()) {
        return (
          <div className="invalid-feedback">비밀번호가 일치하지 않습니다.</div>
        );
      }
    }
  }

  //비밀번호&비밀번호 확인이 일치하는지 확인해서 알려준다.
  doesPasswordMatch() {
    const { pw1, pw2 } = this.state;
    return pw1 === pw2;
  }

  //이메일&패스워드 유효성 검사
  emailCheck() {
    const { email, pw1, pw2 } = this.state;

    if (!email.includes("@")) {
      alert("이메일 형식이 잘못되었습니다.");
    } else if (pw1 !== pw2) {
      alert("비밀번호/비밀번호 재입력이 맞지 않습니다");
    } else if (pw1.length <= 5) {
      alert("비밀번호가 짧습니다.");
    } else {
      this.firstCheck();
    }
  }
  // 가입하기 버튼을 눌렀을 때 먼저 모든 창이 입력되었는지 확인
  signupHandler = () => {
    this.checkValid();
  };

  firstCheck() {
    fetch(`${URL}/signup`, {
      method: "POST",
      headers: {
        "content-Type": "application/json",
      },
      body: JSON.stringify({
        email: this.state.email,
        username: this.state.name,
        phone: this.state.tel,
        password: this.state.pw1,
      }),
    }).then((res) => {
      if (res.ok) {
        alert("회원가입에 성공하였습니다. 로그인을 해주세요.");
        this.props.history.push("/login");
      } else {
        alert("중복된 회원정보가 있습니다. 다시 확인해주세요.");
        this.props.history.push("/signup");
      }
    });
  }

  render() {
    return (
      // <div className="signp-main">
      <div className="SignUp">
        <div className="signup-wrap">
          <div className="input-info">
            <Link to="/">
              <img
                src="https://id-content.subway.com/content/assets/images/logo-myrewards/
      Subway-MyWayRewards.png?v=d5b4027e-c994-4fd8-ba0f-9d53dd0c7437"
                alt="reward"
              ></img>
            </Link>
            <div className="label">이메일</div>
            <input
              className="input-box"
              onChange={this.typed}
              type="email"
              placeholder="이메일"
              value={this.state.email}
              name="email"
              onChange={this.handleInput}
            />
            <div className="invalid-feedback">{this.state.wrongEmail}</div>
            <div className="label">이름</div>
            <input
              className="input-box"
              type="text"
              placeholder="이름"
              value={this.state.name}
              name="name"
              onChange={this.handleInput}
            />
            <div className="label">전화번호</div>
            <input
              className="input-box"
              type="tel"
              placeholder="전화번호"
              value={this.state.tel}
              name="tel"
              onChange={this.handleInput}
            />
            <div className="label">비밀번호</div>
            <input
              className="input-box"
              type="password"
              placeholder="비밀번호(6자리 이상)"
              value={this.state.pw1}
              onChange={this.handleInput}
              name="pw1"
            />
            <div className="label">비밀번호 재입력</div>
            <input
              className="input-box"
              type="password"
              placeholder="비밀번호 재입력(6자리 이상)"
              value={this.state.pw2}
              onChange={this.handleInput}
              name="pw2"
            />
            {this.renderFeedbackMessage()}
            <button onClick={this.signupHandler} className="signup-bt">
              가입하기
            </button>
          </div>
        </div>
        <Footer />
      </div>
      // </div>
    );
  }
}
export default SignUp;

2. 제품 상세페이지

아쉽게 현제 백엔드 서버가 끊어져 있어서 첨부사진을 올리지 못하는데 원래 구현하고자 하는 원래의 상세페이지는 이렇다.

  • 좌우의 버튼을 누르게 되면 앞이나 뒤의 샌드위치 상세 페이지로 이동하게 되면서 url 파라메터도 변하게 된다.
    이 기능을 사용하기 위해서 가장 어려웠던 것은 컴포넌트의 라이프사이클에 따른 componentDidUpdate 를 사용하는 것이었다.
    (실제로 이 기능을 사용하기 위해 코드를 잘못 작성했다가 말로만 듣던 비행기 돌아가는 소리를 경험하고 윈도우를 강제종료했었다..)
import React from "react";
import { Link, withRouter } from "react-router-dom";
import Header from "../../components/Header/Header";
import MenuNav from "./Menu-nav/MenuNav";
import MenuTitle from "./MenuTitle/MenuTitle";
import OrderButton from "./OrderButton/OrderButton";
import MenuSelector from "./MenuSelector/MenuSelector";
import MenuRecipe from "./MenuRecipe/MenuRecipe";
import CommonChart from "./CommonChart/CommonChart";
import CommonRules from "./CommonRules/CommonRules";
import Footer from "../../components/Footer/Footer";
import { URL } from "../../Config";
// import { faHeart } from "@fortawesome/free-solid-svg-icons";
import "./Menu_Details.scss";

class Menu_Details extends React.Component {
  state = {
    sandwich: [],
    nutrition: [],
    prev: [],
    next: [],
    id: "",
  };

  componentDidMount = () => {
    //이전페이지에서 라우팅될때 스크롤이 최상위로 올라올수 있게 한다.
    window.scrollTo(0, 0);
    this.getData();
  };

	//다른 샌드위치 메뉴를 누르면 url이 바뀌면서 해당 샌드위치의 상세페이지로 이동해야 하는데 이 때 데이터를 다시 fetch해야 한다.
  componentDidUpdate = (prevProps) => {
    if (prevProps.match.params.key !== this.props.match.params.key) {
      this.getData();
    }
  };

//컴포넌트가 마운트되면 백엔드 api 에서 샌드위치에 대한 데이터를 가져오는데 이전/현재/다음메뉴로 3가지씩 데이터가 묶여 있다.
//state에 각각의 샌드위치/영양정보를 담아준다.


  getData = () => {
    const num = this.props.match.params.key;
    fetch(`${URL}/product/sandwich/?product_id=${num}`)
      .then((res) => res.json())
      .then((res) =>
        this.setState({
          sandwich: res.product,
          nutrition: res.nutrition,
          prev: res.all_subcategory_products[0],
          next: res.all_subcategory_products[1],
          id: res.product.id,
        })
      );
  };

  render() {
    const { sandwich, nutrition, prev, next, id } = this.state;

    return (
      <>
        <Header />
        <div className="Menu_Details">
          <div className="sub-header">
            <MenuNav />
            <div className="main">
              <div className="menu-view-wrapper">
                <MenuTitle
                  name={sandwich.name}
                  eng={sandwich.name_en}
                  kcal={nutrition.calories_kcal}
                />
                <OrderButton id={id} />
                  //샌드위치 셀렉터라는 component에 state를 전달해준다.
                <MenuSelector
                  centerId={sandwich.id}
                  image={sandwich.image_url}
                  des={sandwich.description}
                  name={sandwich.name}
                  prevName={prev.name}
                  prevImage={prev.image_url}
                  prevId={prev.id}
                  nextName={next.name}
                  nextImage={next.image_url}
                  nextId={next.id}
                  prevNext={this.prevNext}
                  clickHandler={this.clickHandler}
                />
                <MenuRecipe />
                 //commonchart component에도 state를 전달한다.
                <CommonChart
                  weight={nutrition.size_g}
                  kcal={nutrition.calories_kcal}
                  sugar={nutrition.sugar_g}
                  protein={nutrition.protein_g}
                  fat={nutrition.saturated_fat_g}
                  sodium={nutrition.sodium_g}
                />
                <CommonRules />
              </div>
            </div>
            <Footer />
          </div>
        </div>
      </>
    );
  }
}
export default Menu_Details;
import React from "react";
import { Link } from "react-router-dom";
import "./MenuSelector.scss";

class MenuSelector extends React.Component {
  state = {
    //마우스가 호버되었을 때 opacity를 변하게 하기 위해 state를 지정해준다.
    hoverLeft: true,
    hoverRight: true,
  };

  hoverHandlerLeft = () => {
    this.setState({ hoverLeft: !this.state.hoverLeft });
  };

  hoverHandlerRight = () => {
    this.setState({ hoverRight: !this.state.hoverRight });
  };

  render() {
    const { hoverLeft, hoverRight } = this.state;
    const {
      image,
      prevId,
      nextId,
      prevImage,
      nextImage,
      prevName,
      nextName,
    } = this.props;

    return (
      <div className="menu-content">
        <div className="MenuSelector">
          <div className="menu-info">
            <div className="menu-img">
              <img alt="vegi" src={image} />
              <div className="MenuRotate">
                <div className="menu_nav_prev">
                  //menu_details.js 에서 내려받은 props/ prevId가 url주소가 된다.
                  <Link to={`/menu_details/${prevId}`}>
                    <div
                      onMouseOver={this.hoverHandlerLeft}
                      onMouseLeave={this.hoverHandlerLeft}
                      className={hoverLeft ? "rotate-img" : "rotate-img-change"}
                    >
                      <img alt="이전메뉴" src={prevImage} />
                    </div>
                  </Link>
                </div>
                <div className="menu_nav_next">
                  <Link to={`/menu_details/${nextId}`}>
                    <div
                      onMouseOver={this.hoverHandlerRight}
                      onMouseLeave={this.hoverHandlerRight}
                      className={
                        hoverRight ? "rotate-img" : "rotate-img-change-next"
                      }
                    >
                      <img alt="다음메뉴" src={nextImage} />
                    </div>
                  </Link>
                </div>
              </div>
            </div>
            <div className="arr-prev">
              <span>{prevName}</span>
            </div>
            <div className="arr-next">
              <span>{nextName}</span>
            </div>
          </div>
          <p className="summary">{this.props.des}</p>
        </div>
      </div>
    );
  }
}

export default MenuSelector;

3. Customize 중 토핑선택 페이지

시간과 실력이 부족하여 미흡하게 끝나 아쉬움이 가장 많이 남는 부분이다.
그래도 나름 map 과 filter 를 사용해 본 것에 의의를 두기로 하고 남은 2차 프로젝트가 끝난 후 팀원들과 다시 완벽하게 구현하기로 했다.


class Toppings extends Component {
  state = {
    toppings: [],
    selectedToppings: [],
    isActive: 2,
    addedToppings: [],
    addedToppingList: [],
  };

	//컴포넌트가 마운트되면 모든토핑/필터될 토핑들을 state 에 담아준다.
  getData = () => {
    fetch(`${URL}/product/sandwich/customization/topping/`)
      .then((res) => res.json())
      .then((res) =>
        this.setState({
          toppings: res.all_toppings,
          page_key: this.props.match.params.key,
          selectedToppings: res.all_toppings.filter(
            (topping) => topping.ingredient_category_id === 2
          ),
        })
      );
  };

  componentDidMount = () => {
    this.getData();
  };

	//토핑 종류를 선택할 수 있는 탭에 온클릭 이벤트가 발생하면 그에 따라 필터된 아이템들을 보여준다.
  handleClick = (clickedTopping) => {
    const { toppings } = this.state;
    this.setState({
      selectedToppings: toppings.filter(
        (item) => item.ingredient_category_id === clickedTopping
      ),
      isActive: clickedTopping,
    });
  };

///...중략...

render() {
    const { selectedToppings } = this.state;
    return (
      <div className="Toppings">
        <Header />
        <div className="topping-wrap">
          <div className="main">
            <ul className="selectTab">
              <li onClick={() => this.handleClick(2)} className="meat">
                Meat
              </li>
              <li onClick={() => this.handleClick(4)} className="cheese">
                Cheese
              </li>
              <li onClick={() => this.handleClick(3)} className="veggies">
                Veggies
              </li>
              <li onClick={() => this.handleClick(5)} className="sauces">
                Sauces
              </li>
            </ul>
            <div className="toppingbox-wrap">
              //선택된 토핑들을 실시간으로 list로 띄워서 보여준다. 
              {selectedToppings.map((topping) => (
                <ToppingBox
                  id={topping.id}
                  image_url={topping.image_url}
                  name={topping.name}
                  price={topping.price}
                  clickToppings={this.clickToppings}
                  ingredient_category_id={topping.ingredient_category_id}
                  // kcal={selectedToppings.kcal}
                />
              ))}
            </div>

프로젝트를 마치며

처음 경험해 본 팀 프로젝트라 미숙한 점이 많았지만 느낀바가 많았다. 문제 해결 능력을 기르기 위해서는 누군가에게 물어보기보다 혼자서 풀릴때까지 붙잡고 고민하는게 맞다고 생각했는데, 팀플을 하다보니 한 문제를 두고 같이 고민하거나 의견을 나누는 경우가 많이 생겼다. 그렇게 하다보니 팀원의 의견을 들으며 아 이렇게도 생각할 수 있네? 하는 깨달음을 얻거나 같이 이야기를 하면서 문제의 실마리를 찾는 경우도 많았다. 서로 계속 질문하고 내가 아는 것을 가르쳐주면서 서로가 성장할 수 있는 기회가 되었던 것 같다.

특히 우리 팀 같은 경우 팀웍도 분위기도 좋아서 항상 웃음이 끊이질 않았는데, 이런 팀 분위기가 밤을 거의 새다시피 하면서도 즐겁게 프로젝트를 진행할 수 있는 원동력이 되어 주었다.

또한 git과 github을 정말 제대로 사용해 볼 수 있어서 좋았다. 늘 두렵기만 했던 git이었는데 이제는 브랜치도 잘 따고 머지도 잘 하고 conflict가 나도 덜 두려워졌다. 😊감사하게도 프로젝트 전에 세웠던 개인적 목표들은 다 이루었던 것 같아서 기쁘다.

하지만 백엔드와 좀 더 디테일한 부분들도 소통하고 진행할 수 있었다면 좋았을텐데 하는 아쉬움도 남는다. 각자 작업을 하는 시간이 많다보니 프론트가 필요한 데이터와 백엔드가 준비한 데이터가 달라서 백엔드가 다시 작업을 하는 경우도 있었고, 이러한 문제로 인하여 결국 계획했던 장바구니는 구현하지 못했다.(꼭 2차 프로젝트 이후에 함께 보완할 예정이다). 충분한 회의와 사전 계획의 중요성을 몸소 체험하는 계기가 되었다.

여러가지 아쉬움이 많이 남는 프로젝트였지만, 짧은 2주 동안 성장을 이룬 것 같아 의미있는 시간이었다. 물론 시간과 실력이 부족하여 결과물이 우리가 원했던 만큼 완성도가 있지는 못했지만 내가 혼자 공부해서는 얻을 수 없는 많은 부분들이 있었고, 이것이 프로젝트의 목적이었다면 목적한 바를 이룬것이 아닌가 라고 긍정적으로 생각해 본다. 내가 하는 만큼 발전할 수 있는 것이 개발분야가 아닐까 싶다. 앞으로 하루하루 충실하게, 성실하게 꾸준히 (그리고 계획성 있게)임하고자 한다.

profile
나만의 속도로

0개의 댓글