가장 크게 비중을 두었던 사항은 이전 프로젝트와의 차이점이였다.
E-commerce보다 다양한 기능들과 새로운 UI를 작업해보고 싶었기 때문에 많이 고민했던 것 같다.
Header를 보면 main페이지와 page별 header의 스타일이 다르고 탭, 모달등 많은 기능이 들어가 있다.
전체적으로 사용할수 있는 UI를 구현해 보고 싶어서 header 전체를 맡았다.
또한 기존 프로젝트의 회원가입은 하나의 페이지 내에 정보를 전부 입력후 DB로 보냈다면
이번 프로젝트의 회원가입은 본인인증부터 시작해서 step별로 조건이 충족될때 버튼이 활성화 되며 다음 step으로 넘어가는 방식이다.
같은 회원가입이지만 여러 기능이 복합으로 사용되는 페이지를 구현했다.
아이디 / 비밀번호 찾기는 회원가입 후 작업하니 수월하게 했던 부분인것 같다.
언어 : React js, JavaScript,
style : sass, styled-component
Community Tools : Trello, Notion, Zep, Zoom, Slack
Version Control Tool : Git
- state를 이용한 모달과 탭구현 & hover시 메뉴구현
Login : 모달 창(modal window) 로그인
Sub-nav : Tab 버튼을 이용한 Drop-down menu
const tabHandler = () => {
setSubNavMenu(prev => !prev);
};
시현 영상
- 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;
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: '영화',
},
...
시현 영상
- 회원가입 : step별 페이지 컴포넌트
- 아이디/비밀번호 찾기 : 아이디 찾기/비밀번호 찾기 카테고리별 페이지
* 인증후 다음 step으로 이동
const navigate = useNavigate();
navigate('/signup/consent');
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');
});
};
- api명세서 : POST메서드를 이용하여 id와 password를 body로 담아서 DB에 보냄
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.
const logout = () => {
localStorage.removeItem('token');
navigate('/');
console.log('로그아웃');
};
시현 영상
아이디 찾기 / 비밀번호 찾기 api명세서
- 필요한 body를 확인하고 POST메서드로 DB에 보내준다.
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('입력정보를 확인하세요!');
}
});
};
시현 영상
: step별로 컴포넌트를 나눈후 조건을 처리하고 성공시 다음 step으로 이동
- step01. 휴대폰 인증 api명세서
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();
};
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);
});
};
시현 영상
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}
const handlerCheck = e => {
e.target.checked
? setCheck([...check, e.target.name])
: setCheck(check.filter(el => el !== e.target.name));
};
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');
}
};
시현 영상
step03. 정보입력
- step03. 정보입력 api명세서
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,
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();
}
});
};
- 아이디 중복확인 api명세서
new RegExp(/^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,20}$)/) //비밀번호: 영문, 숫자, 특수기호 중 2가지 이상 조합
new RegExp(/^[A-Za-z0-9_\.\-]+@[A-Za-z0-9\-]+\.[A-Za-z0-9\-]+$/) //이메일: '@'와 . 이 들어가야한다.
시현 영상
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>
// 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 문제
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));
🧐 느낀점
같은 FE개발자가 같은 기능을 구현하더라도 스타일이 각각 다르듯.. BE개발자도 다르구나 ..
역시 개발은 정해진 틀은 없는 것같다!! 이번에 웹스토리지 제대로 배웠다@@
매일 오전 10시 스탠딩 회의를 시작으로 프로젝트를 진행하고 트렐로로 각 팀원들의 작업진행체크를 하였습니다. 다들 내성인이여서 회의시 본인이 진행을 하였고, 시연영상 촬영과 마무리 레이아웃을 수정작업하였다. FE들은 담당 UI와 기능을 BE에 설명해주며 api를 맞추고 BE이 만든 모델링을 보며 서로 피드백을 주고 받았다.
첫 프로젝트가 문제없이 마무리 되고나니 1차 팀원간에 신뢰가 당연하게 느껴졌을 정도로 너무 다른 스타일인 팀원들과 같이 작업을 하게 되었는데 팀원 모두 끌고 가려는 저의 성격에 맞지 않게 개인의 작업을 더 중요시 하는 팀원들이 주였던 터라 이번 프로젝트 진행을 어떻게 해야할지 많이 고민 했던것 같다. 회의시간에 팀원들의 생각을 더 많이 끄집어 내려 노력하였고 서로의 의견을 맞추어 개인이 맡은 UI와 기능에 완성도에 더 중점을 두었다.
프로젝트 완료까지 각자의 역량에 맞춰 작업을 진행하였지만 사이트 전체적인 완성도에 대해서는 많이 아쉬웠던 프로젝트였다. 내가 좀 더 이끌었다면 이라는 생각이 들지만 나는 최선을 다했다고 말할수 있다. 그래도 개개인이 담당한 역할에 최선을 다해준 팀원들에게 고마움을 표한다.