1. 프로젝트 소개
나이키 공식 홈페이지의 클론 코딩
2. 프로젝트 설명
프로젝트 기간
2024년 10월 14일 ~ 2024년 11월 1일
사용 기술
Programming Language | JavaScript |
Styling | CSS |
Formatting | ESLint, Prettier |
Version Control | Git, Github |
Design | Figma |
Communication | Notion, Discord |
3. 프로젝트에서 맡은 역할
3-1. 로그인
나이키는 로그인 시 위와 같이 이메일 입력이 나오는데, 이메일이 기존에 존재하다면 바로 패스워드 입력 페이지로
그게 아니라면 체크 약관 페이지로 넘어간다.
3-1-1. 이메일이 존재하지 않을 때
이렇게 이메일이 존재하지 않는다면 약관 페이지로 가는데, 위와 같이 체크에 대한 유효성도 추가해놨다.
function checkEvent() {
const allChecked = Array.from(checkboxes).every(checkbox => checkbox.checked);
if (allChecked) {
checkboxes.forEach(checkbox => {
checkbox.nextElementSibling.style.color = 'black';
});
checkError.style.display = 'none';
}
}
이렇게 forEach를 사용하여, 모두 체크가 된다면 black 색상으로 변하고,
agreeBtn.addEventListener('click', function (e) {
e.preventDefault();
const allChecked = checkAll.checked === true;
if (allChecked) {
window.location.href = 'signup.html';
} else {
checkboxes.forEach(checkbox => {
if (!checkbox.checked) {
checkbox.nextElementSibling.style.color = 'red';
checkError.style.display = 'block';
}
});
}
});
계속 버튼을 눌렀을 때, 모두 체크가 되었다면, 회원가입 페이지로 이동, 그렇지 않다면 체크가 되지 않은 박스를 빨간색으로 표시하는 방법을 사용했다. 사실 forEach와 저기 사용된 every는 평소 공부는 해봤지만 사용을 한 것은 이번이 처음인 것 같다.
JavaScript로 프로젝트를 한 것도 이번이 처음이었기 때문에, 배운 것들이 있었지만 많이 생소한 부분이 많았다.
같은 주제지만 다른 팀들의 코드를 보니 체크박스는 내 코드와 거의 동일하게 작성이 되어있었다. 아마 다들 같은 방법으로 해결을 한 것 같다. 다음에는 이 방법 말고 다른 방법이 있는지 찾아봐야겠다.
3-1-2. 이메일이 존재할 때
위와 같이 이메일이 존재하면 이렇게 패스워드 입력 페이지로 넘어온다.
이렇게 로그인 버튼을 눌렀을 때, 옳지 않은 비밀빈호를 입력하게 된다면 위와 같이 유효성 검사를 통해 서버의 데이터와 비교하여 에러 메세지를 출력한다. 또한 옆 토글 버튼을 클릭 하면 온오프 형식으로, 사용자 경험을 개선하였다. 가끔 패스워드를 입력하다보면 내가 어떻게 입력했는지 살짝 헷갈릴 때가 있다. 그리고 분명 올바르게 입력했다는 생각을 했지만 막상 저 토글 버튼을 이용해 확인을 해보면 잘못 입력된 경우가 많았다.
toggleClose.addEventListener('click', function () {
passwordInput.type = 'text';
toggleClose.style.display = 'none';
toggleOpen.style.display = 'block';
});
toggleOpen.addEventListener('click', function () {
passwordInput.type = 'password';
toggleClose.style.display = 'block';
toggleOpen.style.display = 'none';
});
토글의 경우는 이렇게 하드 코딩식으로 해놨는데, 이 것은 하나로 묶어서 삼항 연산자로 처리를 했어도 괜찮았을 거 같다. 이 코드를 입력할 당시는 삼항 연산자가 떠오르지 않아... 이렇게 하드코딩을 해놨다... 다음에는 좀 더 간편한 방식으로 처리를 해야겠다.
여기서 이메일을 입력하고 이 페이지로 넘어오면 저기 편집 옆에 이메일이 저장되는데, 이는 세션 스토리지로 저장을 했다.
처음 이메일을 입력하여 존재 유무를 떠나 일단 입력값에 대해 setItem으로 sessionStorage에 저장하여, 동적으로 출력하였다.
그리고 이전 버튼 혹은 편집이라는 것을 클릭하면 즉시 sessionStorage 데이터를 비웠다. 그래서 다시 입력하게끔 했다.
위와 같이 편집을 누르면 바로 로그인 페이지로 이동하게 했다.
아 그리고 원래는 로그인 페이지와 패스워드 페이지는 서로 분리된 html에서 관리를 하고 있었다.
과거에 찍은 사진이 없어, 새로 만들었는데 이렇게 다른 파일에서 관리를 하고 있었다. 중간 피드백 시간이 있었는데, 어차피 같은 디자인에 입력폼만 조금 다른 것 뿐이었다. 그래서 그냥 한 페이지에서 사용하는 것이 어떠냐는 의견이 있었다. 근데 나도 처음에 생각해보니 관리하기 편하게 다른 파일로 관리하려고 했지만, 굳이 그럴 필요가 없었던 것이다. 그래서 피드백을 듣고 아래와 같이 로그인과 패스워드 페이지를 한 곳에 관리를 했다.
그래서 이것도 토글과 같은 하드코딩 느낌으로 했는데 로그인 페이지, 패스워드 페이지를 각각 하나로 묶어서, 이메일이 있으면 패스워드 페이지로 이동하는 것이 아닌 로그인 페이지를 숨기고, 만약 이메일이 없다면 패스워드 페이지를 숨기고 로그인 페이지를 보여주는 형식으로 구현하였다. 이 또한 삼항 연사자로 가능했었던 것 같기도 하다....
3-2. 회원 가입
위는 회원가입 페이지이다. 체크 약관 페이지에서 넘어가면 나오는 페이지이다. 여기서는 유효성을 비밀번호만 유효성을 했다. 실시간으로 input에 입력하면 아래의 조건에 부합하면 초록색, 부합하지 않으면 빨간색으로 변하게 했다. 아래와 같이 아이콘도 변하게 했는데, 원래는 저 V와 X는 아이콘 이미지여야 하는데, 어떻게 적용시켜야할 지 감이 잡히지 않아 엄청난 하드코딩을 했다...
줄만 봐도 엄청난 하드코딩을 해버렸다... 조건이 저 둘다 맞을때, 둘다 맞지 않을때, 첫 번째 조건만 맞을때, 혹은 두 번째 조건만 맞을 때 그리고 아무것도 입력하지 않았을 때 이렇게 5가지 조건이었다.. 이거는 좀 더 많은 고민을 해봐야 할 거 같다. 피드백을 받아보니, 함수화를 하라는 조언을 받았다. 그래서 일단은 함수화를 사용했다... 좀 더 고민을 해봐야 하는 코드이다...
function regPassword(userPw) {
if (userPw === '') {
loginTxtFirst.innerHTML = `<p style="color:var(--color-gray-500)">X 최소 8자 이상 *</p>`;
loginTxtSecond.innerHTML =
'<p style="color:var(--color-gray-500)">X 알파벳 대문자 및 소문자 조합, 최소 1개 이상의 숫자 *</p>';
} else if (!regEight.test(userPw)) {
loginTxtFirst.innerHTML = '<p style="color: red">X 최소 8자 이상 *</p>';
loginTxtSecond.innerHTML =
'<p style="color: red">X 알파벳 대문자 및 소문자 조합, 최소 1개 이상의 숫자 *</p>';
} else if (!regMin.test(userPw)) {
loginTxtFirst.innerHTML =
'<p style="color: var(--color-secondary)">V 최소 8자 이상 *</p>';
loginTxtSecond.innerHTML =
'<p style="color: red">X 알파벳 대문자 및 소문자 조합, 최소 1개 이상의 숫자 *</p>';
} else {
loginTxtFirst.innerHTML =
'<p style="color: var(--color-secondary)">V 최소 8자 이상 *</p>';
loginTxtSecond.innerHTML =
'<p style="color: var(--color-secondary)">V 알파벳 대문자 및 소문자 조합, 최소 1개 이상의 숫자 *</p>';
}
}
그리고 달력 부분인데, 처음에는 이렇게 간략하게 input 태그로 type은 date로 했다. 근데 이렇게 하면 한가지의 문제가 생겼다. 그것은 미래의 날짜 또한 출력이 된다는 사실이다. 아마 나이키에서는 생일을 위해 이것을 넣어놨을 텐데, 미래 날짜를 선택하면 안되는 것이다. 그래서 방법을 찾아보다.. 사실 AI의 도움을 받았는데.
<input class="auth-input" id="inputCalendar" data-placeholder="생년원일" type="date" required tabindex="1" />
// 날짜
function setDate() {
const today = new Date().toISOString().split('T')[0];
document.querySelector('#inputCalendar').setAttribute('max', today);
}
이렇게 하여 new Date()는 현재 날짜와 시간을 나타내고, toISOString() 메서드는 ISO 8601 형식에 2024-11-02T10:20:30.000Z 로 문자열로 변환하는 것이다. 그리고 split('T')[0]는 중간에 들어가있는 T 문자를 기준으로 분할하여 배열로 나눈다. 그리고 우리는 필요한 게 시간이 아닌 년월일이므로 [0]을 사용하여 날짜 부분만 추출한다.
그리고 이를 today라는 변수에 할당하는 것이다.
그리고 document.querySelector('#inputCalendar').setAttribute('max', today) 이 부분은 inputCalendar인 요소를 선택하고 위에 한 것 처럼 type을 date로 하여 캘린더 형식의 입력 필드를 보여준다. 그리고 이 선택한 요소에 max 속성을 추가하여 최대 날짜를 설정 한다. 그래서 today의 값을 max 속성으로 설정하면서 사용자가 선택할 수 있는 날짜를 제한하는 것이다.
이제 중요한 회원가입에 대한 로직인데, 여기서 위에 말했듯이 성, 이름, 비밀번호, 날짜를 서버로 보내야한다. 그럴려면 우리는 post 요청을 하여 서버에 데이터를 저장하는 것에 대한 요청을 해야한다. 이렇게 했는데 저 api 경우는 공통 컴포넌트로 만들어서 끌어다 쓰기만 하면된다. 여기는 헤더 값도 입력이 되어있어 따로 입력을 안해도 된다. 그래서 요청 방식 get, post 등과 엔드포인트 그리고 보낼 데이터를 입력하면 된다.
여기에 이제 회원가입을 하고 자동 로그인이 되게 구현을 해놨느데 여기서 중요한 것은 토큰 발급이다. 단순하게 회원정보를 세션 스토리지에 저장하여 관리 하는 것이 아닌, 서버에 데이터를 보내고 이를 인가해주는 액세스 토큰을 활용하여 데이터를 관리하는 것이다. 그래서 토큰을 이용하여 서버와 통신을 통해 인증이 된 사용자를 식별 하는 것이다.
async function userSign(password, name, userBirth, email) {
try {
const response = await api('post', 'users/', null, {
email,
password,
name,
type: 'user',
extra: {
userBirth,
},
});
if (response.data) {
loginUser(email, password);
}
} catch (error) {
if (error.response) {
console.error('서버 응답 오류:', error.response.data);
console.error('상태 코드:', error.response.status);
} else {
console.error('정보가 없습니다.', error);
}
}
}
위는 회원정보를 저장을 위한 로직이고, 토큰을 발급은 아래와 같다.
async function loginUser(email, password) {
try {
const response = await api('post', 'users/login', null, {
email,
password,
});
if (response.data.item.token) {
const { accessToken, refreshToken } = response.data.item.token;
const userName = response.data.item.name;
if (accessToken && refreshToken) {
sessionStorage.setItem('accessToken', accessToken);
sessionStorage.setItem('refreshToken', refreshToken);
sessionStorage.setItem('name', userName);
window.location.href = import.meta.env.VITE_REDIRECT_URL;
resetInputs();
sessionStorage.removeItem('email');
} else {
console.error('로그인 실패');
checkPassword(password);
}
} else {
console.error('정보가 응답에 없습니다.');
}
} catch (error) {
if (error.status) {
const status = error.response.status;
if (status === 422 || status === 403) {
checkPassword(password);
} else if (status === 401) {
const reToken = await issueToken();
if (reToken) {
sessionStorage.setItem('accessToken', reToken);
return loginUser(email, password);
} else {
localStorage.clear();
tokenError(error);
}
}
}
}
}
async awai로 비동기적으로 처리를 하고, 만약 패스워드와 이메일를 api 함수를 통해 서버에 post 요청을 보내고, 이 데이터를 전달한다. 그리고 만약 서버에 존재한다. 그러면 서버 응답에 토큰 객체가 존재하는 지 확인하여, 유효하다면 로그인이 되는 것이다. 그리고 응답 객체에서 액세스토큰, 리프레쉬 토큰을 추출하고, 여기에 userName 또한 데이터를 담는다. 그리고 이 추출한 데이터 및 토큰을 세션 스토리지에 저장하여 새로고침 및 페이지 이동을 했을 때, 로그아웃이 되는 불상사를 막아준다. 그리고 resetInputs() 함수는 혹시 뒤로가기 되었을 때, 회원가입 폼에서 입력 되었던 정보를 초기화 시켜주는 것이다. 그리고 이메일은 필요가 없으므로 세션 스토리지에서 삭제를 해준다. 그리고 try catch를 통해 에러 또한 관리 하였다.
async function issueToken() {
try {
const response = await axios.get('https://11.fesp.shop/auth/refresh', {
headers: {
Authorization: `Bearer ${sessionStorage.getItem('refreshToken')}`,
'Content-Type': 'application/json',
'client-id': 'vanilla01',
},
});
return response.data.item.accessToken;
} catch (error) {
tokenError(error);
}
}
이 윗 부분은 리프레쉬 토큰 발급 함수이다. 이 함수를 바탕으로 이 아래의 에러를 통해 401 응답 코드를 받는다면, 리프레쉬 토큰으로 액세스 토큰을 재 발급 해주며 로그아웃이 되지 않는 방법으로 했다. 근데, 이것은 결코 좋은 방식이 아니었다.
} catch (error) {
if (error.status) {
const status = error.response.status;
if (status === 422 || status === 403) {
checkPassword(password);
} else if (status === 401) {
const reToken = await issueToken();
if (reToken) {
sessionStorage.setItem('accessToken', reToken);
return loginUser(email, password);
} else {
localStorage.clear();
tokenError(error);
}
}
}
}
}
다른 웹 사이트를 보다보면, 자동 로그인으로 해놨지만, 어느 정도 기간이 지나면 이 자동 로그인이 풀리며 재 로그인을 하라는 메세지가 나오거나 로그아웃이 되어있다. 이처럼 액세스 토큰 가지고 있는 시간을 정하여 만료가 되면 바로 리프레쉬로 재발급 하는 것이 아닌, 로그인 시에 다시 발급해주는 형식으로 했어야 했다. 이번 발표하고 피드백 시간에 이러한 지적을 받았다. 액세스토큰이 만료되면 로그아웃이 되어, 재 로그인 하라는 메세지를 띄우는 기능울 구현 했냐는 말씀에 나는 대답을 할 수가 없었다. 처음에 리프레쉬 토큰에 관련된 것인 줄 알고 대답을 했다가 이것이 아니였다.... 처음에 구현을 할까 말까 고민을 하던 차에 다른 문제점이 생겨 그거에 정신이 팔려 못했던 것도 있고 나의 나태였던 거 같기도 하다.. 같은게 아니라 나태이다.... 다음 로그인 회원가입을 구현하게 된다면 그때는 만료에 대한 것도 구현을 해봐야겠다. 아니면 이번 시간이 될 때 리펙토링 시간에 한번 구현을 해봐야겠다.
3-3. 소셜 로그인
이번 필수 기능을 제외한 추가 기능에는 소셜 로그인 있었다. 처음 주제를 정할 때 2가지 선택지가 있었다. 브런치 스토리 라는 선택지와 나이키가 있었는데, 우리는 나이키를 선택했었다. 근데 나이키에서는 추가 기능에는 소셜 로그인이 없었고, 이커머스 답게 메인 혹은 상세 페이지에 대한 추가 기능들이 있었다. 그래서 고민 하다가 나이키에도 추가하자는 마음에 소셜 로그인을 구현하게 되었다. 로컬 로그인도 좋지만, 소셜 로그인으로 좀 더 간편한 회원가입 및 로그인을 할 수 있도록 사용자 경험을 개선하는 생각을 했다. 여러 소셜로그인이 있지만 처음 생각난 것은 카카오 소셜 로그인이었다. 그래서 소셜 로그인에 대한 정보를 찾아보았다. 공식 홈페이지와 블로그를 찾아보는데 처음에는 이해할 수가 없었다. 각종 방법들을 알려줬지만 그래서 뭘 이걸 어떻게 하라고?? 라는 생각이 컸다.. 이것은 나의 지식의 한계라 생각한다.. 그러다 찾아보다 어떤 은인인 블로그를 발견하여 그것을 바탕으로 구현을 시작했다. 그래서 처음 카카오에서 제공하는 api 과 앱 키 등을 저장하고 아래와 같은 것도 입력을 했어야 했다.
<script src="https://developers.kakao.com/sdk/js/kakao.js"></script>
그리고 카카오에서 자체적으로 해주는 가입 팝업을 띄워주며, 여기에 카카오 이메일, 비밀번호를 입력하여 회원가입, 혹은 로그인을 하는 것이다. 즉 사용자에게 인증을 요청하고, 인증에 성공하면 액세스 토큰을 받아온다. 그리고 정보를 가져올 때, 카카오에서 정해준 요청 방식을 하고, 그리고 카카오에서 그 사용자에 대한 정보를 조회하고 나는 그 조회된 정보를 가져오는 것이다. 요청에 성공했다면 그 카카오에서 정해준 속성명을 통해 정보를 추출한다.
그리고 이 가져온 정보를 세션 스토리지에 저장하는 방식으로 했다. 이 방식은 로그인 회원가입 로직과 매우 같다. 처음 소셜 로그인을 구현하고자 했을 때, 많이 어렵다 라는 생각을 했지만, 이렇게 구현하고 보니 그렇게 내 생각만큼 어려운 내용이 아니었다.. 처음 이를 알기가 힘들었을 뿐.... 많이 어려웠다.
근데 여기서 한 두가지 문제가 생겼었는데, 처음 키를 어디에 저장해야하나 라는 생각을 했다. 처음에는 멋도 모르고 그냥 JS 파일에 바로 보여지게 저장해두고 깃허브에 바로 커밋하여 올렸었다. 근데 생각을 해보니 이것은 보이면 안되는 것이 아닌가.. 만약 이 키 말고 진짜 중요한 키를 올렸다면.... 진짜 큰 일이었을 것이다. 그래서 이것 저것 찾아보고 AI에도 물어보니, config파일을 만들어 거기서 관리하면 된다 라는 조언을 받았다. 그래서 config 파일에 키를 저장하고 이를 gitignore 파일에 추가를 하여 깃허브에 올라가지 않도록 했다. 그러면 나에게는 적용이 되지만, 만약 dev로 머지를 하게 되면 이는 적용이 되지 않는 것이었다. 내 로컬 파일에만 저장이 되어있기 때문이다. 그래서 이것 저것 찾아보다가, .env 파일에 추가하여 불러올 때 process.env를 하여 키를 가져오는 방식이 있었다.
근데 이 또한 되지 않았던 게 이는 node 환경에서 사용하는 방법이었다.. 그래서 결국 조언을 구하러 강사님께 갔다. 그래서 강사님께서 말씀해주신 방법은 import.meta.env.VITE.식별자 를 활용하는 것이다.
이는 위와 같이 .env에 추가를 하지만
VITE_JAVASCRIPT_APP_KAKAO_API_KEY = key 값
이렇게 추가를 하고 이 키를 가져오고자 하면, 이런식으로 가져오면 된다.
const KaApiKey = import.meta.env.VITE_JAVASCRIPT_APP_KAKAO_API_KEY;
근데 여기에도 문제가 있었던 게, 이는 임시 방편이었다. 그 이유는 콘솔에서는 확인이 불가능하지만, 소스를 확인하면 키 값이 보인다. 그래서 제일 좋은 방법은 이러한 중요한 키는 백엔드에서 관리하는 것이다. 피드백을 받아보니, 보통 이런 것들은 백엔드에서 관리한다고 하니 크게 걱정할 필요가 없다고 했다...
4. 회고
처음 구현을 시작할 때, 내가 과연 할 수 있을까... 과거 처럼 너무 욕심만 부리다가 아무것도 못하는 게 아닐까 라는 생각을 했었다. 이러한 걱정과 달리 많은 사람들이 도와줬으며, 강사님께서 많은 도움을 주셨다. 내가 막히는 부분이나 의문이 가는 부분에 대해 질문을 하면 자신의 코드인 것처럼 열정적으로 같이 문제를 해결하는데 도움을 주셨다. 이번 프로젝트를 하면서 구현을 떠나 소통이 중요하다는 사실을 깨달았다. 어떤 문제가 있다면 그것을 팀원과 공유를 하고 이를 해결하는 해야 하는데, 잘 된거 같기도 하고, 그렇지도 않은 거 같다. 사실 이 부분에 대해 많은 아쉬움이 있었다. 그리고 여기에 사실 우리는 문서화를 중요하게 생각하지 않아 정리를 하지 않았는데, 다른 조나 아니면 다른 블로그를 보면 문서 정리를 너무나도 깔끔하게 되어있었다. 코드는 남아있지만 그외의 흔적은 남아있지가 않았다. 라고 말할 수 있을 거 같다. 이를 바탕으로 다음 프로젝트때에는 문서화를 열심히 해야겠다는 생각이 들었다. 다음 프로젝트에서는 소통과 문서화를 잘하는 사람들과 같이 진행을 해보고 싶다. 좋은 점도 있었지만, 솔직하게 아쉬운 점도 많았다....
그리고 구현을 하다보니 많은 부족함 부분들이 보였고, 이를 보충하기 위해 역시 열심히 공부를 해야하는 수 밖에 없는 것 같다. 그리고 구현하는 과정을 블로그에 남겼어야했는데.. 역시 남기지 못하였다. 다음 프로젝트에는 매일 조금씩이라도 과정에 대해 적는 시간을 가져야 겠다. 블로그와 깃허브 매일 작성해보는 목표를 다시 다지며.. 남아 있는 날에 대해 다시 화이팅 해보자!!
- header 스타일링
'프로젝트 > Vanilla Project' 카테고리의 다른 글
[vanilla project] NIKE 클론 코딩하기 3일차 (0) | 2024.10.19 |
---|---|
[vanilla project] NIKE 클론 코딩하기 2일차 (1) | 2024.10.17 |
[vanilla project] NIKE 클론 코딩하기 1일차 (0) | 2024.10.17 |