Skip to content

Parzival8971/35-1st-9roomdroom9-frontend

 
 

Repository files navigation

🐥 프론트, 백엔드 깃허브

웹 배포 : https://9roomdroom9.netlify.app/ | AWS 요금부과 문제로 Netlify 재배포
유튜브 영상 링크 : https://www.youtube.com/watch?v=MYvWzDs-gTM | V1
팀 프로젝트 프론트엔드 : GitHub | 팀 프로젝트 백엔드 : GitHub

🐥 앞뒤가 똑같은 구방문방구!

  • 앞뒤가 똑같은 구방문방구를 팀명으로 팀원들의 물건을 재미있게 판매하는 서비스 홈페이지 구현했어요!
  • 짧은 프로젝트 기간, 적은 인원으로 개발에 집중해야 하므로, 디자인/기획 부분만 모티브 삼아 진행했어요!
  • 외부 라이브러리를 최대한 활용하지 않고 초기세팅부터 직접 구현했으며, 백엔드와 API까지하여 기능을 구현했어요!

🐥 하루같이 느껴 졌던 프로젝트 기간

2022.07.18(월) ~ 2022.07.29(금)

🐥 한분한분이 소중한 우리 팀 일원!

Front-End : 👑엄성훈님(PM), ❤️김광희님, 🐥정예빈님
Back-end : ⚽️이정훈님, 🐱음정민님

🐥 Front-End 사용한 기술 스택!

🐥 열심히 만들어본 페이지! 그래서 누가했는가!

  • Navigation Bar ❤️ 👑
  • Footer ❤️
  • 회원가입 페이지 👑
  • 로그인 페이지 👑
  • 메인 페이지 🐥
  • 상품 리스트 페이지 👑
  • 상품 디테일 페이지 👑
  • 장바구니 페이지 ❤️

👑 저의 담당페이지 입니다!

1. 카테고리 메뉴 페이지 이동 기능 구현

  • categoryList 객체를 만들고 페이지 고유 ID를 부여하였고, text로 메뉴리스트를 구현하였습니다.
  • categoryHandle()함수를 만들고 navigat()를 사용하여, 직접적으로 해당 ID 카테고리 페이지로 이동하도록 구현하였습니다.
  • 예)엔드포인트 http://localhost:3000/products/<카테고리 id>

카테고리페이지

const categoryList = [
  { id: 1000, text: '전체' },
  { id: 1, text: '문구' },
  { id: 2, text: '책/매거진F' },
  { id: 3, text: '구방그린' },
  { id: 4, text: '구방친구들' },
  { id: 5, text: '콜라보레이션' },
  { id: 6, text: '명예의 전당' },
];

const categoryHandle = (id) => {
  navigate(`/products/${id}`);
};

<nav className='category'>
  <ul className='nav-ul'>
    {categoryList.map((categoryList, i) => {
      return (
        <li
          onClick={() => {
            CategoryHandle(categoryList.id);
          }}
          key={categoryList.id}
        >
          {categoryList.text}
        </li>
      );
    })}
  </ul>
</nav>;

2. 상품 리스트 정렬 기능 구현 / 상품 Badge (True or False)에 따른 적용 구현

  • 백엔드에 fetch로 data를 요청, 응답 결과를 받아, useParams() 사용하여, id를 받아 Query String으로 onClick() Event로 적용하였습니다.
  • 상품 밑에 표시되는 is_best(베스트 제품), is_green(친환경 제품) 불린값의 따라 화면에 나오도록, 삼항연산자를 적용하였습니다.

정렬

const sortLow = () => {
  fetch(`${BASE_URL}/products/${params.id}?sorting=LOW_PRICE`)
    .then(res => res.json())
    .then(data => {
      setList(data);
    });
};

 <button className="button-recommend"
   onClick={() => { sortLow(); }}>낮은 가격순
 </button>

 <div className="badge">
   {listData.is_best === true ? (<span className="badge-best">BEST&nbsp;</span>) : (<span className="badge-green" />)}
   {listData.is_green === true ? (<span className="badge-green">GREEN</span>) : (<span className="badge-best" />)}
 </div>

3. 회원가입 유효성 검사 기능 구현

  • 폼(Form) 핸들링

회원가입

  • JOIN_INPUT_DATA 상수 타입을 지정하여, 각 input의 리스트요소를 만들었습니다.
  • Input의 리스트들은 map()을 사용하여, 필요한 값들을 지정해 주었습니다.
const JOIN_INPUT_DATA = [
{
  id: 1,
  name: 'userId',
  type: 'eamil',
  placeholder: '이메일',
  valid: '아이디로 사용 할 이메일을 입력해주세요',
  autoFocus: true,
},
{
  id: 2,
  name: 'userPw',
  type: 'password',
  placeholder: '비밀번호',
  valid: '새 비밀번호를 꼭 입력해주세요.',
  autoFocus: false,
},
....
];

{JOIN_INPUT_DATA.map(input => {
 return (
   <li key={input.id}>
     <div className="input-box-error">
      <input
       onChange={handleInput}
       name={input.name}
       type={input.type}
       placeholder={input.placeholder}
       autoFocus={input.autoFocus}
       autocomplete="off"
      >
      <p className="text-valid">{input.valid}</p>
     </div>
   </li>);})
}

  • User의 입력값은 input 필드에 바인딩 하기위해 useState()로 초기값을 연결하였고, (ES6)구조분해할당으로 사용할 객체 값들을 분해하였습니다.
  • Input의 필드가 변경되면 handleInput() 함수가 inputValue 갱신하도록 하였습니다.
  • (ES6)계산된 속성명을 사용하여, 불변성을 유지 후, [name]:value의 name을 동적으로 사용하였습니다.
const [inputValue, setInputValue] = useState({
  userId: '',
  userPw: '',
  pwCheck: '',
  userName: '',
  userPhoneNumber: '',
  userBirthday: null,
});

const { userId, userPw, pwCheck, userName, userPhoneNumber, userBirthday } =
  inputValue;

const handleInput = (e) => {
  const { name, value } = e.target;
  setInputValue({ ...inputValue, [name]: value });
};

  • 동적으로 오류메시지를 띄우고 싶었으나, 공부가 더 필요하고 향후 업데이트 하였습니다.
  • 구글링을 통해 정규식을 간단히 학습하였고, &&로 필드 값을 검사하도록 하였습니다.
const REGEX_EMAIL = /^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
const REGEX_PASSWORD =
  /^(?=.*[A-Za-z])(?=.*\d)(?=.*[?!@#$%*&])[A-Za-z\d?!@#$%*&]{8,}$/;
const REGEX_NAME = /^[-]{2,5}$/;
const REGEX_PHONE_NUMBER = /^01([0|1|6|7|8|9])-([0-9]{3,4})-([0-9]{4})$/;
const REGEX_BIRTHDAY =
  /^(19\d{2}|20\d{2})-(0[0-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/;

const isValid =
  REGEX_PASSWORD.test(userPw) &&
  pwCheck === userPw &&
  REGEX_NAME.test(userName) &&
  REGEX_EMAIL.test(userId) &&
  REGEX_PHONE_NUMBER.test(userPhoneNumber) &&
  (REGEX_BIRTHDAY.test(userBirthday) || userBirthday === null);

  • form 최상위 요소에 onSubmit()으로 바인딩 된 Data를 백엔드에게 제출을 처리하였습니다.
  • SyntheticEvent의 기본동작 새로고침을 막기위해, e.preventDefault() 적용하였습니다.
  • 모든 필드값의 정규식을 통과하면, css 버튼 속성의 색상이 바뀌도록 button:disabled 활용하였습니다.
  • 백엔드의 응답 결과에 따라 alert으로 user값이 있는지 없는지 구분처리 하였습니다.
<form className='join-form' onSubmit={postUserData}>
  <button className='join-button-submit' disabled={!isValid}>
    회원가입
  </button>
</form>;

const postUserData = (e) => {
  e.preventDefault();
  fetch(`${BASE_URL}/users/signup`, {
    method: 'POST',
    body: JSON.stringify({
      name: userName,
      email: userId,
      mobile_number: userPhoneNumber,
      password: userPw,
      birthday: userBirthday,
    }),
  })
    .then((res) => res.json())
    .then((result) => {
      if (result.message === 'SUCCESS') {
        alert('회원 가입을 환영합니다!');
        navigate('/Login');
      } else if (result.message === 'KEY_ERROR') {
        alert('무슨 에러일까요? 테스트해봐요');
      } else if (result.message === 'EXIST_MOBILE_NUMBER') {
        alert('이미 존재하는 핸드폰 번호입니다.');
      } else if (result.message === 'EXIST_EMAIL') {
        alert('이미 존재하는 이메일 입니다.');
      } else if (result.message === 'INVALID_NAME') {
        alert('이름을 다시 확인해주세요.');
      }
    });
};

4. 로그인시 Navbar 유저의 닉네임 상태 변화

  • 로그인시 localStorage에 유저토큰을 저장하여, useEffect로 state값으로 할당 해주었습니다.
  • user & 로그인 닉네임, 조건부 랜더링과 삼항연산자로 로그인 로그아웃을 구현하였습니다.

로그인

fetch(`${BASE_URL}/users/login`, {
    method: 'POST',
    body: JSON.stringify({
      email: userId,
      password: userPw,
    }),
  })
    .then(res => res.json())
    .then(result => {
      if (result.access_token) {
        localStorage.setItem('token', result.access_token);
        localStorage.setItem('name', result.name);
        alert(`로그인을 환영합니다 ${userId}님`);
        navigate('/');

useEffect(() => {
  setToken(localStorage.getItem('token'));
  setUser(localStorage.getItem('name'));
}, []);

 <ul className="nav-icon">
   {user &&
     (<li className="nav-username">
       <span>{user}</span></li>
    )}
 </ul>

5. Path parameters를 이용한 제품 상세페이지와 탭 메뉴 구현

  • 동적 라우팅을 위해 파라미터로 해당 번호의 :id를 연결해 주었습니다.
  • 페이지에 필요한 데이터를 useEffect 에서 fetching하였고, state에 담아 컴포넌트 UI를 render 처리해 주었습니다.

탭메뉴

<Route path='/Products/detail/:id' element={<ItemDetail />} />;

const params = useParams();

useEffect(() => {
  fetch(`${BASE_URL}/products/detail/${params.id}`)
    .then((res) => res.json())
    .then((data) => {
      setItemInfo(data.result);
    });
}, [params.id]);

  • 탭 메뉴 구현을 위하여 상수데이터를 만들었고, 탭을 클릭했을때를 위해 state 초기값으로 1을 넣어주었습니다.
  • onClick 이벤트로 onClick={() => clickHandler(idx + 1) setState 처리를 해주었습니다.
const CATEGORY_ARR = [
 {
  id: 1,
  category: '상품정보',
  className: 'productInfo',
 },
 {
  id: 2,
  category: '기본정보',
  className: 'productInfoBasic',
  }, ...

const MAPPING_OBJ = {
  1: <First data={itemData} />,
  2: <Second />,
  3: <Third />,
};

const [currentId, setCurrentId] = useState(1);

const clickHandler = id => {
  setCurrentId(id);
};

<div className="item-detail-tabs">
  <ul>
   {CATEGORY_ARR.map((category, idx) => {
     return (
       <li
         key={category + idx}
         className={category.className}
         onClick={() => clickHandler(idx + 1)}
       >
       {category.category}
       </li>);
    })}
   </ul>
   {MAPPING_OBJ[currentId]}
</div>

🔥 <10월 ~ 추가내용> 리액트 기본기를 다시 공부했으며, 못했던 것들을 추가로 기록합니다.

회원가입 유효성 검사 동적 UI 오류메세지를 표현 못한 부분.

  • 조건부 랜더링과 삼항연산자를 잘 활용하지 못하여, 동적으로 오류를 보여주는 컴포넌트를 만들지 못하였다.
  • 정규식표현.test 메서드를 배열화 시켜주었고, map의 index를 이용해 REGEX_ARRAY[i]를 삼항연산자로 처리해 주었다.

re로그인

const REGEX_ARRAY = [
 REGEX_EMAIL.test(userId),
 REGEX_PASSWORD.test(userPw),
 pwCheck.length === 0 ? false : pwCheck === userPw,
 REGEX_NAME.test(userName),
 REGEX_PHONE_NUMBER.test(userPhoneNumber),
];

<ul className="join-form-list">
  {JOIN_INPUT_DATA.map((input, i) => {
    return (
     <li key={input.id}>
       <div className="input-box-error">
          <input
            onChange={handleInput}
            name={input.name}
            type={input.type}
            placeholder={input.placeholder}
            autoFocus={input.autoFocus}
            autocomplete="off"
           />
        </div>
         {REGEX_ARRAY[i] ? ('') : (
          <p className="text-valid">{input.valid}</p>)}
       </li>);
   })}
</ul

카테고리 메뉴에 동적인 UI를 CSS를 주지 못한 부분. +10.12

  • 기존에 동적인 className을 이용하여 동적인 CSS 부분을 잘 다루지 못한것이 문제점임을 알게 되었다.
  • state에 초기값을 전체로 주고, 클릭시 삼항연산자를 이용해 className에 active CSS를 주었다.

동적UI메뉴

const categoryList = [
  { id: 1000, text: '전체' },
  { id: 1, text: '문구' },
  { id: 2, text: '책/매거진F' },
];

const [category, setCategory] = useState('전체');

const CategoryHandle = (id, category) => {
  navigate(`/products/${id}`);
  setCategory(category);
};

<ul className='nav-ul'>
  {categoryList.map((categoryList, i) => {
    return (
      <li
        className={category === categoryList.text ? 'active' : ''}
        onClick={() => {
          CategoryHandle(categoryList.id, categoryList.text);
        }}
        key={categoryList.id}
      >
        {categoryList.text}
      </li>
    );
  })}
</ul>;

서버에서 데이터 fetching 과정에서의 !data <div>로딩중..<div> 으로만 표현한 부분. +10.19

  • 비동기 API 요청 동안, 무엇을 나타내야하는지 잘 몰랐고, 어떻게 처리하면 좋은 방법들이 있는지 용어도 잘 몰랐다.
  • 구글링을 잘 활용을 못한 것 같고, 공부후에는 Suspense, Spinners, Skeleton 등 여러표현이 있는걸 알게되었다.
  • 가장 자주 보이는 skeleton를 직접 만들기 위해 구글링 결과 keyframes과 gif 방식으로 넣는 것을 알 수 있었다.
  • 라이브러리도 써봐야하지 않겠는가 싶어서 react-loading-skeleton 설치후 적용하였다.
  • Array.from({ length: 10 }, v=> {e}) 간단히 유사배열을 만들고 갯수만큼 처리 하였다.

스켈로톤UI

import Skeleton from 'react-loading-skeleton';

!data === 0 || undefined (
  <div className="item-list-img-list">
    {Array.from({ length: 10 }, v => (
      <div className="item-list-img">
        <div className="links-wrap">
          <div className="links">
          <Skeleton width="282px" height="282px" />
        </div>
      </div>
    ))}
  </div> )

상품 리스트 정렬 기능과 상품 Badge(True or False)에 따른 적용 구현하는 부분 +10.21

  • 기존은 템플릿 리터럴과 함수 인자 활용 그리고 조건부 랜더링을 미숙함을 보여주는 부분이였다.
  • 인자에 sort('NEW'), sort('HIGH_PRICE')를 넣어서 해결하였고, 동시에 ?sorting=${sortPrice}로도 처리할 수 있었다.
  • 기존 뱃지의 화면은 삼항연산자를 우겨넣어서, 어떤상황이든 무엇이든 badge-green과 badge-best의 빈값이 남아있었다.
  • 그결과 CSS가 빈값에도 스타일값이 들어가고, 가운데 정렬이 안되는것이였다.
  • 조건부랜더링으로 값이 있고 없고 할 때만 처리를 하여서 해결할 수 있었다.
  • 공부하다 보이는 아쉬운 점들 비동기처리의 try..catch 예외처리와 버튼 같은것들은 하나의 컴포넌트로 props로 내려주는것에 따라 바뀌게 처리하면 더 좋은 코드가 될 수 있을 것 같다. BEST

const sortLow = () => {
  fetch(`${BASE_URL}/products/${params.id}?sorting=LOW_PRICE`)
    .then(res => res.json())
    .then(data => {
      setList(data);
    });
};

 <button className="button-recommend"
   onClick={() => {sortLow()}}>낮은 가격순
 </button>

 <div className="badge">
   {listData.is_best === true ? (<span className="badge-best">BEST&nbsp;</span>) : (<span className="badge-green" />)}
   {listData.is_green === true ? (<span className="badge-green">GREEN</span>) : (<span className="badge-best" />)}
 </div>

  const sort = sortPrice => {
    fetch(`${BASE_URL}/products/${params.id}?sorting=${sortPrice}`)
      .then(res => res.json())
      .then(data => {
        setList(data);
      });
  };

  <button className="button-least" onClick={() => {sort('NEW')}}>최신순</button>
  <button className="button-high" onClick={() => {sort('HIGH_PRICE')}}>높은 가격순</button>
  <button className="button-low" onClick={() => {sort('LOW_PRICE')}}>낮은 가격순</button>

  <div className="badge">
    {listData.is_best && <span className="badge-best">BEST</span>}
    {listData.is_green && (<span className="badge-green">GREEN</span>)}
  </div>

데이터 검색기능 +10.21

  • 사용자가 입력하는 inputState 값과 비동기로 받아오는 SearchState로 관리를 하였다.
  • setter변수에 받아오는 값들을 getter변수인 userSearch으로 넣어주었고, 그 값들을 filter함수와 retrun 값으로 includes 함수를 넣어주어 검색어가 입력되게 처리하였다.
  • 상품의 값이 보이는 것은 input.length > 0 ? 길이와 삼항연산자로 처리하였다.
  • 검색을 테스트 하다보니 잘 작동되지 않았던 것은 빠르게 검색을하면 값을 불러오지 못했다. 아직 해결하지 못한 부분.
  • 공부를 하다보니 검색은 실시간으로 요청을 하기 때문에, 성능을 향상 시키려면 Debounce처리릃 하면 성능을 향상시킬 수 있다고 한다. 공부해두자.

search

const [userInput, setUserInput] = useState('');
const [userSearch, setUserSearch] = useState([]);

useEffect(() => {
  fetch(`${BASE_URL}/products/1000?search=${userInput}`)
    .then((response) => response.json())
    .then((result) => {
      setUserSearch(result.products_data);
    });
}, [userInput]);

const handleChange = (e) => {
  setUserInput(e.target.value);
};

const filterInputValue = userSearch.filter((search) => {
  return search.name.includes(userInput);
});

<div className='searchResultContainer'>
  {userInput.length > 0 ? (
    filterInputValue.map((list) => {
      return (
        <SearchItems
          key={list.id}
          list={list}
          handleSearchBarOn={handleSearchBarOn}
        />
      );
    })
  ) : (
    <div className='searchBarRecentContainer'>
      <section className='searchBarRecentItems'>
        <h3>검색어를 입력해주세요</h3>
      </section>
    </div>
  )}
</div>;

Netlify로 배포하며 겪은 Mixed content ? +10.22

  • 해당 문제는 mixed content는 암호화된 HTTPS 기반의 사이트에서 암호화되지 않은 HTTP 사이트에 요청을 보내서 Mixed content 에러가 발생한 것이라고 한다.
  • 엔드포인트의 경우 http였고, Netlify의 요청은 https를 요청하고 있었다.
  • 이 오류도 CORS 처럼 백엔드에서의 처리방법과 프론트서버에서의 처리 방법이 있었다.
  • netlify에서 지원하는 proxy를 설정하여 우회처리 할 수 있었다.
  • root에 netlify.toml 파일을 생성후 redirect 처리.

error

[[redirects]];
from = '/proxy/*';
to = 'http://0.000.000.00:8000/:splat';
status = 200;
force = true;

const PROXY = window.location.hostname === 'localhost' ? '' : '/proxy';

useEffect(() => {
  fetch(`${PROXY}/products/1000?search=${userInput}`)
    .then((response) => response.json())
    .then((result) => {
      setUserSearch(result.products_data);
    });
}, [PROXY, userInput]);

비동기 fetch를 async,await,axios로 변환과 예외처리 ? +11.06

  • 기존 로그인API 처리는 fetch와 Promise체이닝으로 then을 붙혀서 사용하였다.
  • then의 연속 사용성과 JSON의 변환과정도 필요 없이 코드의 가독성 편의성, 보안성을 갖춘 axios를 적용해보기로 했다.
  • 'Content-Type': 'application/json' 도 전혀 몰랐는데... 요청과 응답에 데이터 형식을 알려주기 위해서 필요함도 알게 되었다.
  • 첫 fetch에 대해 배웠을땐, if문으로 result.message === 'INVALID_USER' 결과 값을 alert으로 띄어주면 끝? 으로 생각했다.
  • 공부를 하게되고 조금씩 더 알게 되면서, 예외처리의 중요성도 알게 되고, http메서드에도 부가 설정을 많이 할 수 있음을 알게 되었다.
  • 가장 기초가 되는 예외처리를 방법으로 try~catch를 사용하여 처리하였다. 더 좋은 예외처리 방법들에 대해서도 접해보고 싶다.
  • catch문에 console.error('에러발생', error.response.data.message)를 적어 런타임에 어떠한 오류인지 확인할 수 있었다.

try

[기존 코드]
const postUserData = (e) => {
  e.preventDefault();
  fetch(`${BASE_URL}/users/login`, {method: 'POST',
    body: JSON.stringify({
      email: userId,
      password: userPw,
    }),
  }).then((res) => res.json())
    .then((result) => {
      if (result.access_token) {
        localStorage.setItem('token', result.access_token);
        localStorage.setItem('name', result.name);
        alert(`로그인을 환영합니다 ${userId}님`);
        navigate('/');
        window.location.reload();
      } else if (result.message === 'INVALID_USER') {
        alert('아이디 또는 비밀번호를 잘못 입력했습니다. 입력하신 내용을 다시 확인해주세요.');
      } else if (result.message === 'User matching query does not exist.') {
        alert('아이디 또는 비밀번호를 잘못 입력했습니다. 입력하신 내용을 다시 확인해주세요.');
      }
    });
};
[axios 적용 코드]
const postUserData = async (e) => {
  e.preventDefault();
  try {
    const response = await axios.post(`${BASE_URL}/users/login`,
      { email: userId, password: userPw,},
      { headers: { 'Content-Type': 'application/json' } });
    if (response.data.access_token) {
      localStorage.setItem('token', response.data.access_token);
      localStorage.setItem('name', response.data.name);
      alert(`로그인을 환영합니다 ${userId}님`);
      navigate('/');
      window.location.reload();
    }
  } catch (error) {
    if (error.response.data.message === 'INVALID_USER') {
      alert('아이디 또는 비밀번호를 잘못 입력했습니다. 입력하신 내용을 다시 확인해주세요.');
    }
    if (error.response.data.message === 'User_DoseNotExist') {
      alert('아이디 또는 비밀번호를 잘못 입력했습니다. 입력하신 내용을 다시 확인해주세요.');
    }
    console.error('에러발생', error.response.data.message);
  }
};

🐥 재밌게 만들었어요!

메인상단 메인하단
image image
회원가입 로그인
image image
상세페이지 상세페이지 별 탭
image image
장바구니 페이지 장바구니 텅
image image

🐥 우리 어떻게 일 한 한거야?

  • GitHub : 각 페이지마다 branch를 생성하여 관리했어요!

  • Slack : 슬랙과 깃허브를 연동하여 상시 상황을 공유하도록 했어요!

  • Trello : 각자 맡은 구현부분을 티켓으로 만들어 진행 상황을 공유했어요!

  • Notion : 매일 아침 30분정도 갖는 회의에 원활한 소통을 위해 하루계획, 현재진행상황, 팀원칭찬 등 여러부분을 작성했어요!

트렐로 노션
깃허브 슬랙
image image"
image image

🐥 Reference

  • 이 프로젝트는 배민 문방구 사이트를 참조하여 학습목적으로 만들었습니다.
  • 학습용으로 만들었기 때문에 이 코드를 활용하여 이득을 취하거나 무단 배포할 경우 법적으로 문제될 수 있습니다.
  • 이 프로젝트에서 사용하고 있는 사진 대부분은 위코드에서 구매한 것이므로 해당 프로젝트 외부인이 사용할 수 없습니다.

About

[Team Project] 팀 프로젝트 구방문방구

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 63.4%
  • SCSS 36.3%
  • HTML 0.3%