드디어 쓰는 첫 프로젝트 회고 ☁️
첫 프로젝트는 7월 말에 시작했던 인스타그램 클론 코딩이다.
우리팀은 똑같이 클론하는 게 아니라 그 안에서도 각자 할 수 있는 부분을 좀 더 디벨롭하기로 했다.
예컨대 CICD나 기획, 디자인 같은 부분을 우리의 입맛대로 수정해서 하면 조금 더 애정이 갈 것 같았다.
이 프로젝트는 어떤... 진짜 공식적인 프로젝트까진 아니었고, 그냥 팀 빌딩 활동의 일환?
이전까지 개인 과제를 했다면 이번에는 좀 더 협업 중심의 팀 과제였다.
강제성은 없었지만, 우리는 본래 스터디 팀이어서 스터디와 병행하면서 프로젝트를 시작했다.
TO DO
- 로그인, 회원가입, 게시글 CRUD, 홈 화면
원래 회의를 거쳐 이런 저런 다양한 기능들이 나왔지만... 우리끼리 협업이 처음이다 보니 처음은 퀄리티보다 협업을 하는 경험에 초점을 맞추기로 했다. 지금 보면 저게 MVP인 것 같긴 한데, 일단은 저렇게 4가지만 구현해보기로 했고 목표가 소소하다 보니 실제로 구현은 성공했다.
기술 스택
- FE: React, bootstrap
- BE: Java Spring, MySQL, Jenkins, docker
- Github, Notion, discode
npm, yarn처럼 패키지 매니저도 맞춰야 하는 걸, axios인지 fetch인지 Ajax도 맞춰야 하는 걸 이 때 처음 알았다.
물론 여전히 같은 필드에서 협업 경험이 많지 않아 변수명이라든가 클린하게 코드를 짜진 못한다. ^_^
피그마로 어느 정도 뼈대를 잡았다. 지금 보니까 진짜 어수선했구나...
나는 회원가입과 로그인 페이지를 맡았는데, 사실 그냥 폼 만들기 & 백 단과 통신하기 자체가 챌린지였다.
(원래는 홈 화면까지 하는 거였는데, CRUD를 맡은 같은 프론트 갱갱규님이 같이 하시는 게 더 나을 것 같았다.)
부트스트랩을 써서 폼 자체는 그럴 듯 하게 만들었고, 정규 표현식도 얼추 해서 모양은 잘 나왔다.
다만 로그인 후에 로그인이 유지가 되지 않았고, 나중에는 CORS 오류까지 만나면서 최종적으로는 실패하게 되었다.
회원가입
회원가입 시 필요한 정보는 이메일, 닉네임, 비밀번호, 비밀번호 확인으로 총 4가지다.
따라서 이메일 유효성 검사와 이메일 중복 체크, 비밀번호 유효성 검사와 비밀번호 확인 일치를 체크해야 한다.
닉네임의 경우 별도의 조건이 없으므로 그냥 입력하면 된다. (🙂)
각 정보를 입력할 input을 아래와 같이 초기화 한다.
const [email, setEmail] = useState('');
const [displayName, setDisplayName] = useState('');
const [password, setPassword] = useState('');
const [rePassword, setRePassword] = useState('');
이메일은 중복 확인을 했으면 true, 하지 않았으면 false로 상태를 관리한다.
지금 코드를 보니 왜 저렇게 짰는지 약간 이해가 안 되네... 왜 여긴 focus를 또 안 줬을까... (🙂)
아무튼 일단 이메일을 입력한 후 유효하다면 서버로 POST 요청을 보낸다.
해당 이메일이 DB에 있는지 확인해서 있으면 얼럿 + 초기화를 해주고 없으면 사용 가능하니 true로 바꿔준다.
const [emailConfirmed, setEmailConfirmed] = useState(false);
// 유효한 경우 DB에서 해당 이메일 중복 체크
const response = axios.post(
`${serverURL}`,
{
email: email,
}
);
// 동일한 이메일을 가진 정보가 있을 경우 리셋
if (response.data) {
alert('중복된 이메일입니다.');
setEmail('');
} else {
setEmailConfirmed(true);
alert('사용 가능한 이메일입니다.');
}
} catch (error) {
const firstError = error.response.data.errors[0]; // 에러 중 첫 번째 에러
const defaultMessage = firstError.defaultMessage; // defaultMessage에 접근
// 오류 메세지
if (defaultMessage) {
alert(defaultMessage);
setEmail('');
}
}
input에는 focus를 준다. (displayName에도 주지...🙂)
const emailInput = useRef(null);
const passwordInput = useRef(null);
const RepasswordInput = useRef(null);
정규 표현식은 아래와 같다.
아래의 정규 표현식과 유저가 입력한 input의 정보가 일치하는지 test 메서드를 사용한다.
만약 일치하지 않으면 얼럿을 띄우고, 해당 input을 리셋한 뒤 다시 입력하도록 focus를 주고 return한다.
const emailRegEx = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const passwordRegEx = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,16}$/;
즉 회원 가입 버튼을 누를 때 이메일은 중복 확인이 된 상태여야 하고, 이메일과 비밀번호는 유효성 검사를 통과해야 한다.
// 이메일 중복 확인 여부
if (!emailConfirmed) {
alert('이메일 중복 확인을 해주세요.');
return;
}
// 비밀번호 유효성 검사
if (!passwordRegEx.test(password)) {
alert('비밀번호를 다시 설정해주세요.');
setPassword('');
setRePassword('');
passwordInput.current.focus();
return;
}
// 비밀번호 일치 여부
if (password !== rePassword) {
alert('비밀번호와 비밀번호 확인이 일치하지 않습니다.');
setRePassword('');
RepasswordInput.current.focus();
return;
}
모든 정보가 제대로 입력됐다면 해당 정보를 객체로 묶어서 serverURL에 POST한다.
정상적으로 응답이 오면 회원가입이 완료되고 메인 화면으로 navigate가 된다.
반면 비정상적인 응답이 온다면 에러를 콘솔에 찍어준다. (왜 이렇게 첫 번째 에러에 집착했지? 🙂)
// 모든 정보가 제대로 입력될 경우 DB로 정보 전송
axios
.post(
`${serverURL}`,
{
email: email,
displayName: displayName,
password: password,
}
)
.then((response) => {
if (response.data) {
// true
alert('회원가입이 완료되었습니다.');
navigate('/');
}
})
// 회원 가입 오류 발생
.catch((error) => {
console.log(error);
if (error) {
const firstError = error.response.data.errors[0]; // 에러 중 첫 번째 에러
const defaultMessage = firstError.defaultMessage; // defaultMessage에 접근
// 오류 메세지
if (defaultMessage) {
alert(defaultMessage);
setEmail('');
} else {
alert('회원가입에 실패하였습니다. 다시 시도해주세요.');
}
} else {
console.error('Error:', error);
}
});
또 각 input에 무언가를 작성해 onChange 될 때마다 그걸 반영했다.
이게 input이 4개가 똑같이 있다 보니 아래와 같이 inputType으로 묶어서 코드를 정리했다.
근데 이건 어디서 찾은 리팩토링 방식이라 아직 내 손에 안 익는다.
// input 변경 시 useState 적용
const ChangeJoinInfo = (e, inputType) => {
const value = e.target.value;
if (inputType === 'id') {
setEmail(value);
} else if (inputType === 'displayName') {
setDisplayName(value);
} else if (inputType === 'password') {
setPassword(value);
} else if (inputType === 'rePassword') {
setRePassword(value);
}
};
이메일 확인을 하지 않고 회원 가입 완료 버튼을 누르면 setEmailConfirmed가 false라서 얼럿을 띄운다.
그 외에 모든 정보를 잘 입력하고 회원 가입 완료 버튼을 누르면 정상적으로 회원가입이 완료된다.
로그인
로그인부터 약간 내가 잘못 생각한 게 보인다.
세션이나 쿠키에 대한 개념이 없다 보니(지금도...) 앞단에서 뭔갈 하려고 했던 거 같다.
우선은 여기도 똑같이 loginid, loginpw로 아이디와 비밀번호를 받을 input을 만든다.
e와 inputType로 유저가 입력한 값을 set~ 에 넣어주고 해당 정보를 user라는 객체로 묶어 서버에 POST한다.
이런 개인 정보는 withCredentials: true로 체크해주어야 한다.
이 때 서버에서 loginResult 1을 주면 존재하지 않은 회원, 즉 아이디조차 없는 회원으로 간주된다.
따라서 아이디와 비밀번호 input을 모두 초기화 하고 얼럿으로 존재하지 않는 아이디라고 알려준다.
왜 여기는 또 focus를 안 줬는지 모르겠다... ㅋㅋㅋㅋㅋ 왜 이렇게 통일을 못 했지...
loginResult 2을 주면 비밀번호가 틀린 것으로 비밀번호 input만 초기화 하고 얼럿으로 다시 입력하라고 한다.
loginResult 3을 주면 정상적으로 로그인이 되었으므로 setIsLogin이라는 상태 변화 함수를 true로 바꿔준다.
const Login = ({ setIsLogin }) => {
// input 기본값
const [loginid, setLoginId] = useState('');
const [loginpw, setLoginPw] = useState('');
// input 변경 시 useState 적용
const ChangeLoginInfo = (e, inputType) => {
const value = e.target.value;
if (inputType === 'id') {
setLoginId(value);
} else if (inputType === 'pw') {
setLoginPw(value);
}
};
// 로그인 시 입력한 정보와 DB의 정보 대조
const SubmitLogin = (e) => {
e.preventDefault();
const user = {
email: loginid,
password: loginpw,
};
axios
.post(
$`{serverURL}`,
user,
{ withCredentials: true }
)
.then((response) => {
console.log(response);
if (response.data.loginResult === 1) {
// 존재하지 않는 회원
setLoginId('');
setLoginPw('');
alert('존재하지 않는 아이디입니다.');
} else if (response.data.loginResult === 2) {
// 비밀번호 틀림
setLoginPw('');
alert('비밀번호를 다시 확인해주세요.');
} else if (response.data.loginResult === 3) {
// 로그인 성공
alert('로그인이 완료되었습니다.');
setIsLogin(true);
}
})
.catch((error) => {
console.error('Error:', error);
});
};
로그인 유지?
지금까지 우리 팀은 총 3번의 프로젝트를 진행하면서 세션으로 로그인을 구현했다.
따라서 클라이언트는 서버에게 GET 요청으로 세션이 있는지 확인한다.
// 로그인 세션 여부에 따라 false (로그인) & true (홈)
const [isLogin, setIsLogin] = useState(false);
const navigator = useNavigate();
// 로그인 세션 확인
useEffect(() => {
axios({
method: 'get',
url: $`{serverURL}`,
withCredentials: true, // 이 옵션을 설정하여 쿠키와 인증 정보를 함께 보냅니다.
})
.then((response) => {
console.log(response);
const data = response.data;
setIsLogin(data);
})
.catch((error) => {
console.error('에러:', error);
});
}, []);
<Routes>
{isLogin ? (
<Route
path="/"
element={
<>
<Main
items={items}
setItems={setItems}
deleteItem={deleteItem}
></Main>
</>
}
></Route>
) : (
<Route path="/" element={<Login setIsLogin={setIsLogin} />} />
)}
<Route path="/join" element={<Join />} />
로그인을 했다면 메인 화면, 하지 않았다면 Login 컴포넌트를 보여주기로 했다.
따라서 가장 처음 setIsLogin은 false라서 Login 컴포넌트를 보게 되지만, 로그인을 하면 true로 바뀌게 된다.
true로 바뀐 후 Main 컴포넌트를 볼 수 있다.
완성본과 홈에서 CRUD
CRUD 부분은 내가 하지 않았지만 그래도 기록을 위해 남겨둔다.
배운 점
1. Tailwind는 생각보다 가독성이 좋지 않구나. 또 프레임워크라 바꾸려면 인라인을 줘야 하는 점이 번거로웠다.
그냥 어떤 포맷으로 만드는지만 대충 보고 비슷하게 CSS로 작업해도 되지 않을까 하는 생각이 들었다.
2. 기획/개발할 때는 각자의 R&R을 잘 생각해서 해야겠다. 이번에는 협업이 처음이다 보니 정말 최소한의 MVP로 작업을 해서 기능을 말도 안되게 찢어놨다. 백엔드 1분이 CR, 다른 한 분이 UD 맡는 둥... 우리도 로그인/회원가입과 홈/CRUD로 이걸 억지로 쪼개다 보니 나 개인은 능률적이지 못했던 것 같다.
3. 네트워크 및 배포에 대한 지식이 필요하다. 물론 여전히 잘 모르지만... 기껏해야 내가 알고 있는 배포 방법은 github action으로 하는 것 뿐이었고 그마저도 혼자 하는 거라 배포에 대한 개념이 아예 없었다. 프론트 서버랑 백엔드 서버를 띄워야 하는 것도, 프론트 서버를 local이 아니라 어디에 띄워야 하는지...? 우리는 ngrok을 썼는데 이것도 거의 처음 써보는 거라 한참을 얼탔다. 🥲
또 로그인을 어떻게 구현하는지 잘 이해를 못한 것 같다. 갑자기 반성을 많이 하게 된다.
4. 기술적으로 고도화를 하지 못한다면 최소한 유저 친화적인 UIUX를 고민하자.
일상 생활에서 너무 아무렇지 않게 쓰고 있어서 몰랐는데, 예컨대 포커스나 input 초기화, 엔터 === 클릭과 같은...
폼의 경우 라이브로 유효성 검사의 결과가 나오게 하는 거나, 비밀번호의 경우 눈을 누르면 오픈이 되는 것 같은.
뇌 빼고 이용해서 몰랐던 사이트들을 조금 더 깊게 들여다 볼 필요가 있는 것 같다.
5. 유효성 검사를 비롯한 보안 측면은 프론트 혼자 절대 할 수 없다.
이걸 계속 놓치고 있었는데... (당연함 모름) 프론트는 절대적으로 유저와 맞닿는 표면이다.
그 이상으로 절대 들어갈 수 없으므로 반드시 서버의 보안이나 검사가 필요하다.
물론 프론트로 1차적인 보안은 할 수 있지만 본질적인 보안이 아니므로...
그래도 우선 처음으로 협업을 해봤고, git의 무시무시함을 맛 봤다.
이 때 계속 CORS를 만나면서 결국에는 실패 아닌 실패를 하게 되었는데 오히려 이게 원동력이 됐다.
두 달 전이어도 이렇게 보기가 힘든데 ㅋㅋㅋㅋ 그만큼 성장했다는 거겠지... 🥹
아무튼 첫 프로젝트는 이렇게 끝! 고생했다... 🌱
'🛠️ 프로젝트 > ☁️ 구름톤 Hongflix' 카테고리의 다른 글
[Hongflix] 모달 & 마이 페이지 & 카카오 결제하기 API (5) | 2023.10.28 |
---|---|
[Hongfilx] 장르별 콘텐츠 보여주기 & debounce로 실시간 검색 결과 보여주기 (1) | 2023.10.28 |
[Hongflix] 조건부 렌더링 헤더 & Slick으로 배너와 캐러셀 만들기 (0) | 2023.10.18 |