JUSTCODE 2nd Project : STARBOX

Miog Yang·2022년 10월 11일
0
post-thumbnail

JUSTCODE 2번째 프로젝트 STARBOX

다양한 기능과 UI가 사용되고 있는 MEGABOX

👆 데모영상 바로가기




📌 프로젝트 소개


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

  • 이전 프로젝트와 차이점은 무엇인가
  • 구현해야 하는 UI와 기능은 적절한가
  • 기간내에 구현할 부분은 어느정도 인가

가장 크게 비중을 두었던 사항은 이전 프로젝트와의 차이점이였다.
E-commerce보다 다양한 기능들과 새로운 UI를 작업해보고 싶었기 때문에 많이 고민했던 것 같다.
Header를 보면 main페이지와 page별 header의 스타일이 다르고 탭, 모달등 많은 기능이 들어가 있다.
전체적으로 사용할수 있는 UI를 구현해 보고 싶어서 header 전체를 맡았다.
또한 기존 프로젝트의 회원가입은 하나의 페이지 내에 정보를 전부 입력후 DB로 보냈다면
이번 프로젝트의 회원가입은 본인인증부터 시작해서 step별로 조건이 충족될때 버튼이 활성화 되며 다음 step으로 넘어가는 방식이다.
같은 회원가입이지만 여러 기능이 복합으로 사용되는 페이지를 구현했다.
아이디 / 비밀번호 찾기는 회원가입 후 작업하니 수월하게 했던 부분인것 같다.

2. 적용 기술

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




3. 프로젝트 구조

1) Header

  • Header js에 탭구현 함수와 Navbar, location, Login, Signup등 모든 컴포넌트가 들어있다. category는 left / right를 나눠서 작업하였다.
  • login UI는 모달로 작업하였고 Login컴포넌트 내에서 api요청과 e.target.value를 onChange이벤트로 받아 유효성 검사후 token을 저장하는 방식으로 구현하였다.

2) User find

  • user의 정보를 찾는 페이지에서는 아이디 찾기와 비밀번호찾기의 경로를 Router에서 지정해주고 각 컴포넌트 내에서 onChang로 e.target.value를 받는 함수와 api호출하고 POST메서드로 DB에 보내면 해당 정보가 있는 user의 경우 아이디를 반환하거나 임시비밀번호를 생성하여 반환하는 방법으로 구현하였다.

3) Signup

  • singup js를 메인으로 step별 카테고리와 컴포넌트로 구성되어있다.
  • 컴포넌트는 Router에서 경로를 지정하고 step별 인증에 성공해야 경로가 이동되며 다음 step컴포넌트가 구현되게 작업하였다.

3. 담당역할

1) UI : Header

  • state를 이용한 모달과 탭구현 & hover시 메뉴구현
    Login : 모달 창(modal window) 로그인
    Sub-nav : Tab 버튼을 이용한 Drop-down menu

const tabHandler = () => {
    setSubNavMenu(prev => !prev);
  };
  • 📌 prev => !prev : boolean 값 반대를 넣는단 의미
  • 탭으로 구현하고자 하는 컴포넌트를 state로 관리하고 false를 초기값으로 지정한다.
  • modal window 또는 drop-down을 구현하기위해 클릭해야하는 버튼이 있다.
  • 버튼에 onClick이벤트를 이용하여 위의 tabHandler함수를 담고 삼항연산자를 사용하여 이벤트 발생시 불리언값으로 open 또는 close를 구현한다.

시현 영상

  • location.pathname을 이용하여 location데이터 반환
    Location : 페이지 이동시 user의 이동 경로 UI구현
const location = useLocation();
Location컴포넌트 : location={location.pathname}
function Location({ location }) {
  return (
    <>
      {locationArr.map(list => {
        return (
          <div key={list.id}>
            {location === list.url &&
              list.location.map(link => {
                return (
                  <div key={link.id}>
                    <NavLink to={link.url}>
                      {link.link}
                    </NavLink>
                  </div>
                );
              })}
          </div>
        );
      })}
    </>
  );
}
export default Location;
  • props로 받은 location.pathname을 이용하여 데이터로 만든 location배열안에 경로값과 삼항연자로 비교후 반환한다.
import { AiTwotoneHome } from 'react-icons/ai'; //react-icons

const locationArr = [
  {
    id: 1,
    url: '/movie',
    location: [
      {
        id: 1,
        url: '/',
        link: <AiTwotoneHome color='#999' />,
      },
      {
        id: 2,
        url: '/movie',
        link: '영화',
      },
      ...
      
  • 삼항연산자를 이용하여 비교하는 조건에 id값은 물론 입력값을 onChange로 받아서 e.target.value와 다른 키값의 비교도 가능한데 경로로 비교도 가능하다!
    어떤 조건을 갖춰 로직을 짤지 잘 생각해서 계획해야겠다.

시현 영상

  • 회원가입 : step별 페이지 컴포넌트
  • 아이디/비밀번호 찾기 : 아이디 찾기/비밀번호 찾기 카테고리별 페이지

* 인증후 다음 step으로 이동

const navigate = useNavigate();
navigate('/signup/consent');
  • 인증하기 버튼 또는 다음 페이지 이동버튼을 비활성화하고 모든 입력값을 가질경우 버튼활성화를 한다. 또한 다음 step으로 이동하기위해 조건에 만족할 경우 useNivigate();를 사용하여 다음 step으로 이동할수 있게 하였다.
 
  const verifyCode = e => {
    
    fetch('http://localhost:10010/user/check/verify', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        ...
      }),
    })
      .then(res => res.json())
      .then(res => {
        alert('인증성공');
        navigate('/signup/consent');
      });
  };
  • step별 컴포넌트 내에 navigate로 경로 이동을 지정해준다.



2) 로그인 인증후 token저장

  • api명세서 : POST메서드를 이용하여 id와 password를 body로 담아서 DB에 보냄
  • api명세서를 확인하여 필요한 body값과 주소를 확인한다.
  • 명세서에 message로 조건을 처리하여 반환 하였다.
const body = {
    account_id: id,
    password: password, 
  };
  const loginSuccess = e => {
    e.preventDefault();
    fetch('http://localhost:10010/user/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    })
      .then(res => res.json())
      .then(result => {
        if (result.message === 'LOGIN_SUCCESS') {
          localStorage.setItem('token', JSON.stringify(result.token));
          navigate('/');
          setModal(false);
        } else {
          alert('로그인 정보 확인');
        }
      });
  };

: fetch함수를 이용하여 POST메서드로 user의 정보를 DB에 보낸후 token을 localStorage에 저장. 로그아웃시 token을 localStorage를 이용하여 clear.

  • 여기서 if문에 message로 조건처리하는 것 외에 res.ok로 해도 실행된다.
  • 로그아웃은 따로 컴포넌트를 생성하지 않고 Header js에서 localStorage.removeItem를 담은 함수를 만들고 로그인후 바뀐 로그아웃 태그에 onClick이벤트에 담아 실행하였다.
const logout = () => {
    localStorage.removeItem('token');
    navigate('/');
    console.log('로그아웃');
  };

시현 영상




3) 아이디 찾기 / 비밀번호 찾기

아이디 찾기 / 비밀번호 찾기 api명세서

  • 필요한 body를 확인하고 POST메서드로 DB에 보내준다.
  • fetch와 POST메서드로 user의 정보를 입력해서 DB로 보내면 DB에서 확인하여 해당 user의 id를 받는다. 또는 랜덤으로 비밀번호를 생성후 새로운 임시비밀번호를 받는다.
const findClick = e => {
    e.preventDefault();
    fetch('http://localhost:10010/user/find/id', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: userName.current.value, 
        birth: userBirth.current.value,
        phone: userPhone.current.value,
      }),
    })
      .then(res => res.json())
      .then(result => {
        if (result.account_id) {
          alert(`${userName.current.value} 님의 아이디는 ${result.account_id} 입니다.`);
          userName.current.value = '';
          userBirth.current.value = '';
          userPhone.current.value = '';
        } else {
          alert('입력정보를 확인하세요!');
        }
      });
  };
  • useRef()를 이용하여 입력값의 e.target.value를 body에 담아 DB에 보내준다.
  • body를 확인하여 아이디찾기와 비밀번호 찾기에서 DB에 보내주면 alert메세지로 아이디 및 임시 비밀번호를 받을 수 있다.

시현 영상




4) 회원가입

: step별로 컴포넌트를 나눈후 조건을 처리하고 성공시 다음 step으로 이동

  • step01. 휴대폰 인증 api명세서
  • 인증번호를 보내는 동시에 인증번호 확인 toggle생성, 휴대폰으로 발송된 번호를 입력후 DB에 보내면 확인후 다음페이지로 이동된다.
    user가 입력한 기본 정보 값은 localStorage로 담아 정보입력에 해당하는 step에서 넣어준다.
const phoneSend = e => {
    setToggle(true);
    fetch('http://localhost:10010/user/send/verify', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        phone: phoneRef.current.value,
        name: userName.current.value,
        birth: birthValue.current.value,
      }),
    })
      .then(res => res.json())
      .then(res => {
        localStorage.setItem('token', res.data.jwt);
      });
    e.preventDefault();
  };
  • localStorage.setItem('token', res.data.jwt) : 휴대폰번호를 보내면 인증 토큰과 함께 휴대폰 번호로 인증번호를 발송한다.
const verifyCode = e => {
    fetch('http://localhost:10010/user/check/verify', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: userName.current.value,
        phone: phoneRef.current.value,
        birth: birthValue.current.value,
        verificationCode: verifyRef.current.value,
      }),
    })
      .then(res => res.json())
      .then(res => {
        alert('인증성공');
        navigate('/signup/consent');
        window.localStorage.setItem('name', userName.current.value);
        window.localStorage.setItem('birth', birthValue.current.value);
        window.localStorage.setItem('phone', phoneRef.current.value);
      });
  };
  • 인증 번호를 보낼때 한번더 휴대폰인증시 입력한 user의 정보를 같이 보내주어야한다.
    또한 window.localStorage.setItem을 이용하여 현재 step에서 입력한 user의 정보를 담아 놓고 정보입력 step에서 user의 정보란에 넣어준다.

시현 영상

step02. 약관동의
: 전체 체크박스를 선택시 전체 선택 , 체크박스 해제시 전체 체크박스 해제

const checkAll = e => {
    if (check.length === 5) {
      setNextStep('button');
    } else {
      setNextStep('nextBtn');
    }
    e.target.checked ? setCheck(['check1', 'check2', 'check3', 'check4', 'check5']) : setCheck([]);
  };  
  // 각 체크박스input태그에 삼항연산자 이용 checked={check.includes('check1') ? true : false}
  • 전체 체크박스선택인 input에 onChange이벤트를 이용하여 위의 함수를 실행하면 checked에 담긴 includes('check')를 확인하고 true로 변경됨.
const handlerCheck = e => {
    e.target.checked
      ? setCheck([...check, e.target.name])
      : setCheck(check.filter(el => el !== e.target.name));
  };
  • 각 체크박스에 해당하는 input택그에 OnClick이벤트를 적용하여 위의 함수를 담아 checkAll함수가 실행되게 한다.
 useEffect(() => {
    if (check.includes('check1') && check.includes('check2')) {
      setNextStep('nextBtn');
    } else {
      setNextStep('button');
    }
  }, [check]);
  
  const nextClick = e => {
    e.preventDefault();
    if (nextStep === 'nextBtn') {
      navigate('/signup' + '/info');
    }
  };
  • 필수 체크박스 선택시 또는 위에 전체 체크박스가 선택이 되면 state로 담은 nextStep이 'nextBtn'으로 변경된다. 이를 조건으로 버튼활성화를 실행하여 다음 페이지로 이동한다.

시현 영상

step03. 정보입력

  • step03. 정보입력 api명세서

const name = window.localStorage.getItem('name');
const birth = window.localStorage.getItem('birth');
const phone = window.localStorage.getItem('phone');
  • 정보입력 : 휴대폰인증 절차에 입력된 user정보를 window.localStorage.setItem를 이용하여 저장후 해당 절차 페이지에 getItem으로 불러온다.
const register = () => {
    fetch('http://localhost:10010/user/signup', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: name,
        phone: phone,
        birth: birth,
        email: email.current.value,
        account_id: id.current.value,
        password: password.current.value,
        pwConfirm: pwConfirm.current.value,
      }),
    })
      .then(res => res.json())
      .then(res => {
        if (res.message === 'USER_CREATED') {
          alert('성공');
          navigate('/signup/complete');
        } else {
          alert('가입실패');
          navigate('');
          window.localStorage.clear();
        }
      });
  };
  • 정보를 입력하는 input태그는 useRef로 value값을 받고 POST메서드로 DB에 보내주는 body에 넣어준다.
  • 아이디 중복확인 api명세서
  • POST메서드를 이용하여 id값을 DB로 보내준후 정보가 있으면 에러를 보낸다.
new RegExp(/^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,20}$)/) //비밀번호: 영문, 숫자, 특수기호 중 2가지 이상 조합

new RegExp(/^[A-Za-z0-9_\.\-]+@[A-Za-z0-9\-]+\.[A-Za-z0-9\-]+$/) //이메일: '@'와 . 이 들어가야한다.
  • 유효성 검사 정규식 표현을 찾아보게 되었는데 필요한 부분이니 기록하자.
  • 해당 입력칸이 전부 채워지고 마지막 체크박스까지 체크하면 가입완료

시현 영상




🧐 재미있었던 기능

1. location.pathname을 이용한 카테고리 UI & Tab구현


import { Link, useLocation } from 'react-router-dom';

function SignUp() {
  const location = useLocation();
  
  ...
  
  return (
  
    ...
    
    // step별 카테고리
    
    <ul>
    	<li className={location.pathname === '/signup' 
        	? `${styles.activeTitle}` 
        	: `${styles.stepTitle}`
        		}> STEP1.본인인증
      	</li>
      	<li className={location.pathname === '/signup/consent'
        	? `${styles.activeTitle}`
        	: `${styles.stepTitle}`
         		}> STEP2.약관동의
       </li>
       <li className={location.pathname === '/signup/info'
         	? `${styles.activeTitle}`
          	: `${styles.stepTitle}`
          		}> STEP3.정보입력
       	</li>
        <li className={ location.pathname === '/signup/complete'
        	? `${styles.activeTitle}`
        	: `${styles.stepTitle}`
              	}> STEP4.가입완료
         </li>
     </ul>
  
	// 카테고리 조건에 따른 content
	
	
    	<ul>
        	<li>{location.pathname === '/signup' 
				? <Auth /> : null}
			</li>
            <li>{location.pathname === '/signup/consent' 
				? <Consent /> : null}
			</li>
            <li>{location.pathname === '/signup/info' 
				? <Info /> : null}
			</li>
            <li>{location.pathname === '/signup/complete' 
				? <Complete /> : null}
			</li>
        </ul>
  
  )
}
export default SignUp;

// Router.js

	<Route path='/signup' element={<SignUp />}>
    	<Route path='consent' element={<Consent />} />
        <Route path='info' element={<Info />} />
        <Route path='complete' element={<Complete />} />
    </Route>

2. localStorage로 token외에도 user의 정보를 담을수 있다.


// Phone.js : 폰 인증시 기재한 user의 정보를 담는다.

import { useState, useRef, useEffect } from 'react';

function Phone() {
  const phoneRef = useRef();	//user의 phone정보
  const userName = useRef();	//user의 이름
  const birthValue = useRef();	//user의 생년월일
  
  ...
  
  	//인증성공시 localStorage.setItem을 이용하여 value(user의 정보)값 담기
  
  window.localStorage.setItem('name', userName.current.value);
  window.localStorage.setItem('birth', birthValue.current.value);
  window.localStorage.setItem('phone', phoneRef.current.value);
  
  
  render(
  
    //input => ref적용
    <div className={styles.inputBox}>
    	<label className={styles.title} for='name'
			> 이름 </label>
        <input
        	id='name'
            ref={userName}
            type='text'
            placeholder='성명입력'
            className={styles.inputBorder}
          />
      </div>
      <div className={styles.inputBox}>
          <label className={styles.title} for='birth'
			> 생년월일 </label>
          <input
            id='birth'
            ref={birthValue}
            type='text'
            placeholder='생년월일'
            className={styles.inputBorder}
          />
        </div>
        <div className={styles.inputBox}>
          <label className={styles.title} for='phone'
			> 휴대폰 번호 </label>
          <input
            id='phone'
            ref={phoneRef}
            type='tel'
            placeholder='" - " 없이 입력해주세요.'
            className={styles.inputBorder}
          />
  
  )
}
export default Phone;



// Info.js : localStorage로 담은 user의 정보를 가져온다.

function Info() {
	const name = window.localStorage.getItem('name');
  	const birth = window.localStorage.getItem('birth');
  	const phone = window.localStorage.getItem('phone');
  
  const register = () => {
    fetch('http://localhost:10010/user/signup', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: name,		// {중괄호}안에 getItem('name')으로 불러온 name을 담는다.
        phone: phone,	// {중괄호}안에 getItem('phone')으로 불러온 phone을 담는다.
        birth: birth,	// {중괄호}안에 getItem('birth')으로 불러온 birth를 담는다.
        email: email.current.value,
        account_id: id.current.value,
        password: password.current.value,
        pwConfirm: pwConfirm.current.value,
      }),
    })
  
  return (
    	...
    <h1> {name}님 안녕하세요.</h1> // {중괄호}안에 getItem('name')으로 불러온 name을 담는다.
    	
    	...
     <span>{birth}</span>	// {중괄호}안에 getItem('birth')으로 불러온 birth를 담는다.
        
       	...
     <dd className={styles.inputBlock}>{phone}</dd>	// {중괄호}안에 getItem('phone')으로 불러온 phone을 담는다.

		)
}
export default Info;
         



🚧 힘들었던 기능? 문제가 되었던 기능?

2차 프로젝트는 API 명세서를 보는데 좀 더 공부가 된것 같다.
명세서를 보고 필요한 body를 넣고, 성공시 massage 또는 error시 400번대인지 500번인지 확인하게 되었다.

🚨 Error 문제

로그인은 되는데 token이 담기지 않는다?? 😱

console에서 localStorage를 확인해본결과 객체로 나온다... 바로 명세서를 확인해보았다.
1차 프로젝트에서 보았던 명세서에서는 token을 문자형으로 받은것과 달리 2차 프로젝트에서는 객체형으로 id와 token이 담겨있었다. 문제는 로그인은 되지만 token을 저장하는것이 되지 않는 다는 것!!

💡 해결
JSON.stringify(token)

: 웹 스토리지를 사용할 때 주의해야 할 부분, 오직 문자형(string) 데이터 타입만 지원한다는 것이다. 이러한 성질 때문에 객체형인 token을 받을때에는 JSON 형태로 데이터를 읽어야함으로 JSON.stringify를 사용하여 불러온 데이터 result.token으로 지정해준다.
localStorage.setItem('token', JSON.stringify(result.token));

📌 refer : 웹스토리지 사용법

🧐 느낀점

같은 FE개발자가 같은 기능을 구현하더라도 스타일이 각각 다르듯.. BE개발자도 다르구나 ..
역시 개발은 정해진 틀은 없는 것같다!! 이번에 웹스토리지 제대로 배웠다@@




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


진행은 어떻게 하였나?

매일 오전 10시 스탠딩 회의를 시작으로 프로젝트를 진행하고 트렐로로 각 팀원들의 작업진행체크를 하였습니다. 다들 내성인이여서 회의시 본인이 진행을 하였고, 시연영상 촬영과 마무리 레이아웃을 수정작업하였다. FE들은 담당 UI와 기능을 BE에 설명해주며 api를 맞추고 BE이 만든 모델링을 보며 서로 피드백을 주고 받았다.

작업시 문제는 없었나?

첫 프로젝트가 문제없이 마무리 되고나니 1차 팀원간에 신뢰가 당연하게 느껴졌을 정도로 너무 다른 스타일인 팀원들과 같이 작업을 하게 되었는데 팀원 모두 끌고 가려는 저의 성격에 맞지 않게 개인의 작업을 더 중요시 하는 팀원들이 주였던 터라 이번 프로젝트 진행을 어떻게 해야할지 많이 고민 했던것 같다. 회의시간에 팀원들의 생각을 더 많이 끄집어 내려 노력하였고 서로의 의견을 맞추어 개인이 맡은 UI와 기능에 완성도에 더 중점을 두었다.

마지막 한마디

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

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

0개의 댓글