JUSTCODE 1st Project : ALE6IX

Miog Yang·2022년 9월 12일
0

Project

목록 보기
1/7
post-thumbnail

JUSTCODE 첫 프로젝트 E-commerce

home

👆 데모영상 바로보기

Team repo : FE / BE

개발기간 : 2022/8/29 ~ 2022/9/8
개발 인원 : 프론트엔드 4명, 백엔드 2명




📌 프로젝트 소개

1. 사이트 선정시 고려사항

  • 기존의 구현했던 기능 외 새로운 기능은 무엇인가
  • 구현해야 하는 UI와 기능은 팀원들의 역량에 맞는가
  • 각 팀원별 기간내에 구현할 부분은 어느정도 인가

먼저 첫 회의때 진행한 추천 사이트에서 React학습시 만들어 보았던 인스타그램이나 소셜SNS처럼 피드 & 댓글 , 로그인 기능으로 구현할 사이트를 정할지 Basic이였던 E-commerce를 할지 고민을 했다.
지난 학습을 복습하는 것도 좋지만 해보지 않았지만 우리가 학습한 커리큘럼을 베이스로 조금만 욕심을 더 부리자는 생각에 E-commerce를 선정하였고 여러 후보 중 ALESSI로 하게 되었다.

2. 적용 기술

언어 : React js, javascript
Style : sass, styled-component
Community Tools : Notion, Zep, Zoom, Slack
Version Control Tool : Git


3. 담당역할

1) 전체 프로젝트 구현 기능

메인페이지
제품 페이지 , 상세페이지
로그인, 회원가입, 장바구니, 검색 페이지

2) 팀원과의 공동 작업

[Main] : 메인 페이지 내에 섹션별 컴포넌트화 작업
[상세 페이지] : 상세 페이지 내에 구매부분과 상품문의/후기 컴포넌트화 작업
[Nav bar] : 레이아웃, 스타일링, mock-up데이터 카테고리 연결후 페이지네이션

3) 담당 기능

[Main] 이미지 슬라이드 : next & prev버튼, Dot, 무한 슬라이드
[Nav bar] : mock-up데이터를 이용하여 hover시 서브메뉴 UI구현
[Main] Section : 신제품 및 추천 제품 섹션 별 제품 card 구현,
[상세페이지] 상세페이지 내 상품 후기 : token을 이용하여 제품 구매 user일 경우 리뷰작성 또는 해당 user의 글을 삭제 (fetch함수와 POST & DELETE & GET메서드 이용),
[상세페이지] 상세페이지 내 상품 문의 : fetch함수를 이용하여 POST메서드로 문의글 등록 및 DELETE메서드로 user의 token을 이용한 게시글 삭제, GET메서드를 이용하여 리스트구현




4. 구현기능

1) 이미지 슬라이드

: 이미지를 imgSlider로 객체형 데이터로 만들고, 이미지의 index를 state로 담아 슬라이드를 구현한다. setInterval을 이용하여 무한 슬라이드구현.

이미지 슬라이드구조

  • Button : Prev & Next 버튼
  • Dots : 슬라이드제어 및 해당이미지의 index시각화
  • Slider : 이미지 슬라이드에 필요한 함수 및 imgSlider데이터, SliderContent, Button, Dots의 컴포넌트들이 들어있다.
  • SliderContent : 슬라이드이미지 콘텐츠 컴포넌트

function Slider() {
  const [activeIndex, setActiveIndex] = useState(0);
  
  						...

  return (
    <>
      <SliderContent activeIndex={activeIndex} imgSlider={imgSlider} />
      <Buttons/>
      <Dots/>
    </>
  );
}
export default Slider;

const imgSlider = [
  {
    title: "ALE6IX",
    url: "http://alessi.co.kr/_dj/img/mainbanner/2103-1.gif",
  },
    ...
  {
    url: "http://alessi.co.kr/_dj/img/mainbanner/2101-5.jpg",
  },
];
  • SliderContent에 이미지를 담은 state와 이미지 데이터를 props로 넘겨준다.
function SliderContent({activeIndex,imgSlider}){
  return(
    <>
      {imgSlider.map((slide,index)=>(
        <div key={index}>
          <img src={slide.url} alt="img"/>
        </div>
      ))}
    </>
  )
}
export default SliderContent;
  • SliderContent컴포넌트에서 이미지 데이터의 id를 키값으로 map()를 이용하여 이미지의 순서대로 반환한다.



삼항연산자를 이용한 Slide Button과 Dots구현

function Slider() {
      				...
                    
  const prevSlide = () => {
    setActiveIndex(activeIndex < 1 ? imgSlider.length - 1 : activeIndex - 1);
  };
  const nextSlide = () => {
    setActiveIndex(activeIndex === imgSlider.length - 1 ? 0 : activeIndex + 1);
  };

  return (
    <>
      <SliderContent activeIndex={activeIndex} imgSlider={imgSlider} />
      <Buttons prevSlide={prevSlide} nextSlide={nextSlide} />
      <Dots/>
    </div>
  );
}
export default Slider;
  • Button 컴포넌트 내에 props로 넘겨준 함수들을 onClick이벤트에 넣어준다.
  • Dots컴포넌트에 이미지state를 담고 onClick이벤트에 들어가는 함수를 담는다.
    activeIndex={activeIndex}
    imgSlider={imgSlider}
    onclick={(activeIndex) => setActiveIndex(activeIndex)}
function Dots({activeIndex, onclick, imgSlider}){
  return (
    <div className="all-dots">
      {imgSlider.map((slide, index) => (
        <span key={index} className={`${activeIndex === index
          ? "dot active-dot"
          : "dot" }`}
          onClick={()=>onclick(index)}
          ></span>
        ))}
    </div>
  )
}
export default Dots;
  • 해당이미지의 index번호와 클릭시 해당 이미지가 같을경우 Dots가 구분이될수 있도록 스타일적용을 해준다.
  • setInterval 무한슬라이드
    useEffect(() => {
    const interval = setInterval(() => {
    setActiveIndex(
    activeIndex === imgSlider.length - 1 ? 0 : activeIndex + 1
    );
    }, 3000);
    return () => clearInterval(interval);
    }, [activeIndex]);

시현 영상

  • 완성을 하고보니 불필요한 컴포넌트가 보이고 이미지 데이터는 변경되는 경우도 있으니 따로 관리를 해야할 것 같다. 프로젝트 이후 다시 한번 만들어 보았다.

📌 다시 만들어보는 이미지 슬라이드




2) 메인페이지 내에 제품섹션

: 데이터를 호출받고 state로 담아준후 map()메서드를 이용하여 필요한 데이터를 반환한다.

function Section(){

  const [productData,setProductData] = useState([]);

  useEffect(()=>{
    fetch('/data/recommandList.json')
    .then((res)=>res.json())
    .then((data)=>{
      setProductData(data.recommendProductList)
    })
  })
  
  return(
    <>
      <h3>신상품</h3>
      <span>알레시에서 꾸준한 사랑을 받는 베스트셀러</span>
        <ul>
        {productData.map((itemlist,index)=>{
          return <li className="prd-box"
          key={index}
          >
            <figure>
                <img src={itemlist.main_image_url} alt="item" />\
              <figcaption>
                <dl>
                  <dt>{itemlist.product_name}</dt>
                  <dd>{Number(itemlist.price).toLocaleString()}<span>won</span></dd>
                </dl>
              </figcaption>
            </figure>
            </li>
            })}
        </ul>
    </>
  )
}
export default Section;
  • 데이터배열을 state에 담고 fetch함수로 호출한 데이터를 map()메서드를 이용하여
    반환한다. Numder()메서드를 이용하여 가격을 숫자로 변경해준다.

구현 이미지

같은 컴포넌트 내에 섹션별 데이터를 담고 싶었는데 생각대로 로직이 짜여지지 않아 컴포넌트를 분리하여 각각 데이터를 담아서 작업하였다. 추후 리팩토링 할때 한 컴포넌트를 재사용하는 방법은 고민해봐야겠다.




3) NavBar

: 마우스 hover시 서브메뉴가 보이고 카테고리에 모든 메뉴들은 fetch함수로 받은 mock-up데이터로 구현하였다.

function Header() {
 
  const [nav, setNav] = useState([]);

  useEffect(() => {
    fetch("/data/navCategories.json")
      .then((res) => res.json())
      .then((data) => {
        setNav(data.categoryData);
      });
  }, []);
  
	return (
	<>
      <ul>
      {nav.map((navlist) => {
      return (
        <li key={navlist.LargeCategoryId}>
        {navlist.LargeCategoryName}
  	<div>
        <SubNav 
  			largeCateId={navlist.LargeCategoryId} 
  			sublist={navlist.smallCategories}
  		/></div>
  		</li>
  	);
  	})}
    </ul>
	</>
);
}

export default Header;
  • Navbar에 담을 메뉴들을 state에 배열로 담고 fetch함수를 이용하여 데이터를 호출한다.
    SubNav컴포넌트에 props로 넘겨준다.
function SubNav({ largeCateId, sublist }) {
  return (
    <>
      <ul>
        {sublist.map((subNav) => {
          return (
            <li key={subNav.smallCategoryId}>
              <NavLink
                to={"/product/" + largeCateId + "/" + subNav.smallCategoryId}
              >
                {subNav.smallCategoryName}
              </NavLink>
            </li>
          );
        })}
      </ul>
    </>
  );
}
export default SubNav;
  • largeCateId : id값으로 받은 props는 NavLink에 사용한다.
  • sublist : 서브메뉴의 데이터들을 호출하고 NavLink에 id를 넣어주어 클릭시 해당 경로로 이동할수 있게 구현한다.
  • Router에 지정된 NavLink경로
    <Route
    path="/product/:bigId/:smallId"
    element={}
    />




4) 상품문의 및 후기

상세페이지 내에 상품문의, 후기를 컴포넌트화하여 작업하였다.

  • 리뷰 리스트 호출
    useEffect(()=>{
    fetch(http://localhost:8000/products/detail/${productId}/review,{
    method:"GET",
    headers:{
    "Content-Type" : "application/json"
    }
    })
    .then(res => res.json())
    .then((data)=>{
    setReview(data.reviewData)
    })
    },[newReview])

POST메서드로 리뷰 보내기

const addReview = ()=>{

    const data={
      title : titleValue.current.value,
      content : textValue.current.value
    }
    fetch(`http://localhost:8000/products/detail/${productId}/review`,{
      method:"POST",
      headers:{
        "Content-Type" : "application/json",
        "Authorization" : localStorage.getItem("token")
      },
      body : JSON.stringify(data)
    })
  //보내기
    .then(res => res.json())
    .then((data)=>{
      titleValue.current.value = "";
      textValue.current.value = "";
      setNewReview(data.reviewData)
    })
}
  • useRef를 이용하여 입력값을 받는 input에 value를 지정하고 POST메서드로 보내는 body에 ref의 value.current.value를 담아준다.
    여기서 token을 받고 token이 있는 경우에 등록할수 있게 구현하였다.
    문제는 결제기능을 구현하지 않아 해당 상품의 결제를 백엔드에서 지정해주고 해당 상품의 리뷰를 작성하게 만들었다. 삭제기능은 POST메서드 대신 DELETE메서드를 이용하면 된다.

상품후기

상품문의




기억에 남는 ERROR 및 충돌

🚨 위험!! 경고!!

첫번째 찾아온 시련은 깃헙이였다. 초기 설정에서 에러가 발생하면서 에러를 잡고 시작한 팀원과
해당 라이브러리를 제거후 새로 설정을 하고 시작한 팀원이 나뉘면서 우리의 경고는 시작되었고
첫 merge를 했을 때 일이 터졌다. 다행히 초반에 알게 되어 새로 초기설정을 하고 클론받아서
현재 진행중인 작업들을 다시 옮기고 merge > pull을 하였다. 이날은 정말 팀원 모두가 고생하였다.


두번째 시련은 위의 언급했던 바와 같이 진행 계획을 확실히 나누지 않은채 시작하여 레이아웃과 스타일 후 기능 구현 부분에서 다시 나눠야 했던 점이다. 팀원 모두 할수 있는 한 최대한으로 구현하려 노력하였고 기능이 들어가는 부분은 어느정도 완성 했던 것 같다.


🚧 ERROR!!

🚨 처음 직면한 에러 : 이미지 슬라이드에 prev버튼이 안 먹히네???
한참 코드를 다시 살펴보고 다시 코딩을 해도 이상하게 한 버튼이 먹히지 않는다...
몇일을 팀원과 함께 봐도 답이 나오지 않아서 멘토에게 도움을 요청하였다.
원인은 버튼 컴포넌트!! prev와 next 양쪽이라는 생각에 컴포넌트를 각각 Button컴포넌트로 나눠서 작업을 하고는 prev와 next버튼의 함수를 한 컴포넌트씩 넘겨줘야하는데.. 각 컴포넌트당 함수를 전부 넘겨줘서 문제 였다. 나의 코드를 한번 더 의심하자!



🧐 재미있었던 기능?

지난 세션중 인스타그램을 만들어보면서 피드에 올리는 댓글기능을 할때였다. mock-up데이터가 아닌 DB에 연결해서 인증&인가를 통해 댓글을 추가하고 삭제하고 리스트를 불러오는 것이 궁금했다. 그때 멘토님은 DB로 연결하면 이렇게 하지 않는다고 하시고 세션을 마쳤는데 드디어 내가 구현할 때가 되었다!!
이 얼마나 설레는 일인가!!

이 기능을 하기 앞서 인증 & 인가 세션을 다시 보고 정보를 찾아보았다.
📌 refer : 인증 & 인가 이론

내가 이 기능을 구현하기 위해 필요한 것은 무엇인가?

  1. token이 필요하다. => token을 어떻게 받아서 사용할까?
  2. 서버에 연결 => DB에 연결하기 위해 사용하는 fetch함수와 메서드들
  3. 게시글 추가 = POST메서드 => 필요한 body는?
  4. 리스트 구현 = GET메서드 => API명세서에 있는 json형식의 data를 받아오기
  5. 게식글 삭제 = DELETE메서드 => 삭제를 위해 필요한 것은?

🧐 modal형식으로 카테고리를 구현하면 될까??

먼저 카테고리 탭을 이용하여 상세페이지 내에 컴포넌트가 바뀌게 작업하였다.


function ProductBottom(){

  const [review,setReview]=useState(false);
  const reviewBtn = ()=>{
    setReview(true)
  }
  const qnaBtn = ()=>{
    setReview(false)
  }
  
  return (
    <ul>
    <li className='border border-active' onClick={reviewBtn}
       >상품리뷰</li> 
       <li className='border border-active' onClick={qnaBtn}
       >상품문의</li>
	</ul>

// 리뷰 및 문의
	<div className='modal-container'
	>{review === true ? <Review /> : <Qna /> }</div>
    )
}
    export default ProductBottom;

하나의 모달창을 만들때는 버튼함수에 (!state)를 사용하였는데 두개의 탭으로 하려니 복잡한 느낌이다. 이것저것 만져보다 결국 이렇게 마무리... 카테고리 기능에 대해 더 공부해 봐야겠다.

본격적으로 서버연결!!


Review.js

//add list => get
  const [newReview,setNewReview]=useState([]);

  useEffect(()=>{
    fetch(`http://localhost:8000/products/detail/${productId}/review`,{
      method:"GET",
      headers:{
        "Content-Type" : "application/json"
      }
    })
    .then(res => res.json())
    .then((data)=>{
      setReview(data.reviewData)
    })
  },[newReview])


리뷰 추가를 만들어보자!!

✏️ 체크

  • 데이터를 보낼 바디는 JSON.stringify(data) 로 JSON형태로 보낸다고 지정한다.
  • 리뷰 등록시 token을 필요로 하므로 로그인시 setItem으로 저장한 token을 getItem으로 불러와서 header에 넣어준다. (token의 여부는 api명세서에 나와있으니 확인!!!)
  • 해당 제품마다의 상세페이지내에 있는 리뷰이므로 params를 이용하여 fetch안에 넣어준다.

// 리뷰등록

const addReview = ()=>{

    const data={
      title : titleValue.current.value,
      content : textValue.current.value
    }
    fetch(`http://localhost:8000/products/detail/${productId}/review`,{
      method:"POST",
      headers:{
        "Content-Type" : "application/json",
        "Authorization" : localStorage.getItem("token")
      },
      body : JSON.stringify(data)
    })
  //보내기
    .then(res => res.json())
    .then((data)=>{
      titleValue.current.value = "";
      textValue.current.value = "";
      setNewReview(data.reviewData)
    })
}

일단 fetch함수를 이용해서 서버에 연결하고 GET메서드로 리뷰 리스트를 불러온다.
리뷰 추가시 재렌더링되게 하고 POST메서드로 리뷰를 추가한다. 이걸 리뷰 등록에 onClick이벤트를 사용해서 넣어준다.




🚨 두번째 에러 : 상품후기에서 api호출하여 POST테스트 중 400번 에러가 발생!!

😱 검색을 해도 도통 확인이 안된다...
멘토님 도와주세요!!


네트워크에서 에러를 확인하고 서버에 에러메세지를 확인해 보니

결제하지 않은 유저가 작성하려 해서 생긴 에러..
결제 기능을 구현하지 않은 우리 팀에서는 테스트시 백엔드에서 구매한 토큰을 받아서 하다가 최종으로 구매한 제품을 따로 지정해서 작업을 하였다.
이 에러를 통해서 네트워크로 에러를 확인하는 법을 알게되었다.🧐




등록이 됐으니 그대로 삭제를 만들어보자!!

✏️ 체크

  • 리뷰 삭제시 해당 게시글 user여야 하므로 token을 필요한다. 등록과 마찬가지로 로그인시 setItem으로 저장한 token을 getItem으로 불러와서 header에 넣어준다. (token의 여부는 api명세서에 나와있으니 확인!!!)
  • 해당 제품마다의 상세페이지내에 있는 리뷰이므로 params를 이용하여 fetch안에 넣어준다.
  • 코드를 짜고 보니 POST에서 DELETE만 바뀐느낌.. 하지만 여기서 다른부분은 해당 리뷰의 아이디까지 fetch로 받아야 한다는것!

//deletes
  const deleteBtn = (id)=>{
    fetch(`http://localhost:8000/products/detail/${productId}/review?review_id=${id}`,{
      method:"DELETE",
      headers:{
        "Content-Type" : "application/json",
        "Authorization" : localStorage.getItem("token")
      },
    })
    .then(res => {
      if(res.ok){
        fetch(`http://localhost:8000/products/detail/${productId}/review`,{
      method:"GET",
      headers:{
        "Content-Type" : "application/json"
      }
    })
    .then(res => res.json())
    .then((data)=>{
      setReview(data.reviewData)
    })
  }

🚨 마지막 에러 : 상품후기에서 POST method로 등록후 useEffect안에 GET method로 재렌더링을 한후 DELETE method를 사용해서 삭제를 하는데 재렌더링을 어떻게 해야할까 고민하다 useEffect에 같이 넣었다. 백엔드에서 무한요청이 들어온다고 경고!!경고!!! 등록까지는 잘됐으니 삭제에서 문제인가? useEffect에서 삭제를 빼니 괜찮아졌다.. 그럼.. 이걸 어디에 넣어야 하나?? 계속 고민하고 있던 차에 함께한 팀원중 장바구니 구현을 한 지원님이 옆에서 DELETE에 바로 GET을 넣는게 어떤지 의견을 내주셨다. DELETE하면서 재렌더링이 될수 있게 GET method를 바로 넣으니 무한요청도 들어가지 않고 재렌더링이 된다! 감사합니다~ 쌩유~ 지원님!!🙇

✏️ 느낀점 : 서버 연결로 게시글을 등록하고 추가하는게 mock-up데이터를 만들어서 작업하는 것보다 훨씬 재밌구나!!! GET으로 리스트가 구현되는것, POST로 글을 등록하고 DELETE로 해당글 삭제까지 실시간으로 구현되는게 신기하고 뿌듯했다!! 다음엔 더 깔끔하게 코드를 짜서 게시글 등록을 만들어봐야겠다.




✏️ 1차 프로젝트를 마치고

진행은 어떻게 하였나?

매일 스탠딩 미팅으로 진행사항 체크를 하고 FE가 구현하고자 하는 디자인과 기능을 체크하여 BE에게 놓치는 부분이 없는지 체크하며 진행하였다. 팀원 모두 홈페이지를 완성하는데에 집중하여 각자 기능별 UI를 맡아서 작업하였다. 작업을 마친 팀원의 UI나 기능을 회의시간에 전체 진행된 부분을 구현해보면서 마지막 날까지 피드백을 주고 받으며 우리의 첫 프로젝트를 무사히 완성하게 되었다.

작업시 문제는 없었나?

작업 완료후 합치는 과정에서 처음 Git을 팀작업으로 다루는데 내가 초기 설정을 잘못해서 충돌발생! 다행히 첫 Pull 작업때 알게 되어 팀원중 기완님이 바로 다시 초기셋팅을 해주셨다. 이로써 PR의 중요성을 알게 되었고, 개인 브랜치를 따로 생성하여 작업해야하는 필요성을 알게 되었다.

팀원과의 작업은 어떠하였나?

프로젝트를 진행 할수록 의견이 맞지 않거나 불만을 가지는 팀원도 있었으나 UI를 주로 다룬 팀원에게 남은 기능을 위주로 작업을 나누고 기능을 주로 다룬 팀원에게 남은 UI부분을 주면서 서로의 맡은 역활에서 부족하게 느낀 부분을 채워가며 프로젝트를 진행 하였다. 이처럼 충분한 소통을 하고 각자 불만사항을 서로 대신 채워가며 작업을 진행하니 처음에는 의견을 내세우는것이 낯선 팀원들도 점점 스스럼없이 대화를 할수 있었다.

마지막 한마디

프로젝트 완료까지 각자의 역량에 맞춰 작업을 진행하였지만 사이트 전체적인 완성도에 대해서는 많이 아쉬웠던 프로젝트였다. 내가 좀 더 이끌었다면 이라는 생각이 들지만 나는 최선을 다했다고 말할수 있다. 그래도 개개인이 담당한 역할에 최선을 다해준 팀원들에게 고마움을 표한다.

profile
주니어 개발사전 & 프론트엔드 도전기

0개의 댓글