로그인과 회원가입 스크립트를 공유해본다.
요구사항
로그인
회원가입
체크리스트 [심화]
로그인/회원가입 유효성 스크립트 구현
우선 4개의 폴더로 정리했다.
form-validation.js //form validation 공통 함수,변수 정리한 폴더
login.js //로그인 세션 js
signup.js //회원가입 세션 js
password-toggle.js //비밀번호 보이고 감추는 토글버튼 js
예전에 jquery 플러그인?으로 이런 유효성 검사를 해본 기억이 나서 거기에서 나온 방식을 착안해서 구현해보았다.
객채화된 데이터에 이렇게 정리했다.
우선 재사용성과 한눈에 정리해보고 싶어서 이렇게 정리를 했다.
export const validate = {
errorPlacement: function (e, message) {
let errorDiv = e.nextElementSibling;
if (errorDiv && errorDiv.classList.contains('error-message')) {
errorDiv.innerHTML = message;
} else {
errorDiv = document.createElement('div');
errorDiv.classList.add('error-message');
errorDiv.innerHTML = message;
e.parentNode.insertBefore(errorDiv, e.nextSibling);
}
},
showError: function (e, message) {
e.parentNode.classList.remove('valid')
e.classList.add('error-border');
e.dataset.valid = 'false';
this.errorPlacement(e, message);
},
success: function (e) {
e.classList.remove('error-border');
e.parentNode.classList.add('valid');
e.dataset.valid = 'true';
let errorDiv = e.nextElementSibling;
if (errorDiv && errorDiv.classList.contains('error-message')) {
errorDiv.innerHTML = '';
}
},
messages: {
'val-email': {
required: '이메일을 입력해주세요.',
minlength: '잘못된 이메일 형식입니다.',
},
'val-nickname': '닉네임을 입력해주세요.',
'val-password': {
required: '비밀번호를 입력해주세요.',
minlength: '비밀번호를 8자 이상 입력해주세요.',
},
'val-confirm-password': {
required: '비밀번호를 입력해주세요.',
minlength: '비밀번호를 8자 이상 입력해주세요.',
equalTo: '비밀번호가 일치하지 않습니다.',
}
},
rules: {
'val-email': {
required: true,
email: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/,
},
'val-nickname': {
required: true,
},
'val-password': {
required: true,
minlength: 8,
},
'val-confirm-password': {
required: true,
minlength: 8,
equalTo: '#password',
}
},
}
export function validateEmail(){
let value = email.value.trim();
let rules = validate.rules['val-email'];
if (rules.required && !value) {
validate.showError(email, validate.messages['val-email'].required);
return false;
}
if (value && !rules.email.test(value)) {
validate.showError(email, validate.messages['val-email'].minlength);
return false;
}
validate.success(email);
return true;
}
위 함수는 이메일을 체크해주는 함수를 정의해보았다.
export function checkFormValidity() {
let formSubmitDone = [...form.querySelectorAll('input')].every(input => input.dataset.valid === 'true');
submitBtn.disabled = !formSubmitDone;
}
위 함수는 input에 유효성검사가 다 true로 변경된다면 버튼이 활성화 하도록 함수를 만들었다.
멘토님의 수정 사항
- const 사용의 개선점
`let`은 재할당되는 선언되는 것이기 때문에 `const`를 지향할 것
- 자주쓰는 조건문에 들어가는 사항은 변수에 담아서 사용하면 좋다.
const errorDiv = e.nextElementSibling;
const hasError = errorDiv && errorDiv.classList.contains('error-message');
if (hasError) {...}
- 얼리 리얼패턴
이 경우에는 if로 조건을 하나 검사하고, else 블록을 쓰는 대신 if블록에서 리턴하여 함수를 종료하는 방식.
얼리 리턴을 적용할 경우 else나 else if 블록을 없앨 수 있어 가독성이 조금 더 좋아질 수 있다.
if(hasError) {
errorDiv.innerHTML = message;
return; ---> 여기서 함수 종료
}
// else 블록 조건 실행
- 이름 개선점
val-email , val-password 등의 각 인풋의 유효성 관련 key들이 코드내에서 많이 사용될때
요런 항목들은 한 번더 상수화해서 관리하면, 오타 등의 휴먼 에러를 더 줄일 수 있다.
const VALIDATE_LIST = Object.freeze({
EMAIL: 'val-email',
NICKNAME: 'val-nickname',
CONFIRM_PASSWORD: 'val-confirm-password',
...
})
rules: {
[VALIDATE_LIST.EMAIL]: {...},
[VALIDATE_LIST.NICKNAME]: {...},
}
import {
form,
email,
password,
validateEmail,
validatePassword,
checkFormValidity,
submitBtn
} from './form-validation.js';
import { passwordToggleBtns, passwordToggle } from './password-toggle.js';
submitBtn.disabled = true;
email.addEventListener('blur', function () {
this.parentNode.classList.add('is-valid');
if (!validateEmail) { this.parentNode.classList.remove('is-valid'); }
});
password.addEventListener('blur', function () {
this.parentNode.classList.add('is-valid');
validatePassword();
});
email.addEventListener('keyup', function () {
if (this.parentNode.classList.contains('is-valid')) {
}
validateEmail();
checkFormValidity();
});
password.addEventListener('keyup', function () {
if (this.parentNode.classList.contains('is-valid')) {
}
validatePassword();
checkFormValidity();
});
form.addEventListener('submit', function (event) {
event.preventDefault();
if (validateEmail() && validatePassword()) {
alert('로그인성공');
window.location.href = '/items.html';
}
});
// 패스워드 보기
passwordToggleBtns.forEach(btn => {
btn.addEventListener('click', passwordToggle);
});
import {
form,
email,
password,
confirmPassword,
nickName,
validateEmail,
validateNickname,
validatePassword,
validateConfirmPassword,
checkFormValidity,
submitBtn
} from './form-validation.js';
import { passwordToggleBtns, passwordToggle } from './password-toggle.js';
submitBtn.disabled = true;
email.addEventListener('blur', function () {
this.parentNode.classList.add('is-valid');
validateEmail();
});
nickName.addEventListener('blur', function () {
this.parentNode.classList.add('is-valid');
validateNickname();
});
password.addEventListener('blur', function () {
this.parentNode.classList.add('is-valid');
validatePassword();
});
confirmPassword.addEventListener('blur', function () {
this.parentNode.classList.add('is-valid');
validateConfirmPassword();
});
email.addEventListener('keyup', function () {
if (this.parentNode.classList.contains('is-valid')) {
}
validateEmail();
checkFormValidity();
});
nickName.addEventListener('keyup', function () {
if (this.parentNode.classList.contains('is-valid')) {
}
validateNickname();
checkFormValidity();
});
password.addEventListener('keyup', function () {
if (this.parentNode.classList.contains('is-valid')) {
}
validatePassword();
checkFormValidity();
});
confirmPassword.addEventListener('keyup', function () {
if (this.parentNode.classList.contains('is-valid')) {
}
validateConfirmPassword();
checkFormValidity();
});
form.addEventListener('submit', function (event) {
if (validateEmail() && validatePassword() && validateConfirmPassword() && validateNickname()) {
event.preventDefault();
alert('회원가입성공');
window.location.href = '/signup.html';
}
});
// 패스워드 보기
passwordToggleBtns.forEach(btn => {
btn.addEventListener('click', passwordToggle);
});
멘토님의 수정 사항
- login과 signup 중복사용 관련
export function checkInputs(input) {
const inputType = input.name;
const inputValName = `val-${inputType}`;
const value = input.value.trim();
const rules = validate.rules[inputValName];
const message = validate.messages[inputValName];
// Specific validation checks
if (inputType === 'email') {...}
else if (inputType === 'nickname') {...}
else if (inputType === 'password') {...}
else if (inputType === 'confirm-password') {...}
}
inputList.forEach((input) => {
input.addEventListener('blur', function () {
this.parentNode.classList.add('is-error');
checkInputs(input);
});
input.addEventListener('keyup', () => {
checkInputs(input);
checkAllInput();
})
});
- submit에서 검사하는 부분을 checkAllInput()함수로 중복사용 줄일 수 있음
// form 전체에 대한 검증
export function checkAllInput() {
const formSubmitDone = [...form.querySelectorAll('input')].every(input => input.dataset.valid === 'true');
submitBtn.disabled = !formSubmitDone;
return formSubmitDone;
}
export let passwordToggleBtns = document.querySelectorAll('.password-toggle-button');
export function passwordToggle(e) {
let button = e.currentTarget;
let passwordInput = button.parentElement.querySelector('input');
let toggleIcon = document.createElement('i');
toggleIcon.classList.add('icon');
if (passwordInput.type == "password") {
passwordInput.type = "text";
toggleIcon.classList.remove('ic_visible_on');
toggleIcon.classList.add('ic_visible_off')
this.setAttribute("aria-label", "비밀번호 숨기기");
} else {
passwordInput.type = "password";
toggleIcon.classList.remove('ic_visible_off');
toggleIcon.classList.add('ic_visible_on');
this.setAttribute("aria-label", "비밀번호 보기");
}
button.innerHTML = '';
button.appendChild(toggleIcon);
}
멘토님의 수정 사항
- const 사용의 개선점
자꾸 let으로 쓰는 나 반성합니다.
- 삼항연산자 변경
export function passwordToggle(e) {
const button = e.currentTarget;
const passwordInput = button.parentElement.querySelector('input');
const toggleIcon = document.createElement('i');
toggleIcon.classList.add('icon');
const shouldShowPassword = passwordInput.type === 'password';
passwordInput.type = shouldShowPassword ? 'text' : 'password';
toggleIcon.classList.toggle('ic_visible_on', !shouldShowPassword);
toggleIcon.classList.toggle('ic_visible_off', shouldShowPassword);
const ariaLabel = shouldShowPassword ? '비밀번호 숨기기' : '비밀번호 보기';
this.setAttribute("aria-label", ariaLabel);
button.innerHTML = '';
button.appendChild(toggleIcon);
}
const form = document.querySelector("form");
const inputList = form.querySelectorAll("input");
const submitBtn = form.querySelector("#submitBtn");
submitBtn.disabled = true;
inputList.forEach((input) => {
input.addEventListener("keyup", function () {
checkInputs(input);
submitBtn.disabled = !checkAllInputs();
});
input.addEventListener("blur", () => checkInputs(input));
});
function checkInputs(input) {
const emailValue = form.email.value.trim();
const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
const nicknameValue = form.nickname.value.trim();
const passwordValue = form.password.value.trim();
const confirmPasswordValue = form.confirmPassword.value.trim();
if (input.name === 'email') {
if (emailValue === "") {
setErrorFor(input, "이메일을 입력해주세요.");
} else if (!emailPattern.test(emailValue)) {
setErrorFor(input, "잘못된 이메일 형식입니다.");
} else {
setSuccessFor(input);
}
} else if (input.name === 'password') {
if (passwordValue === "") {
setErrorFor(input, "비밀번호를 입력하세요.");
} else if (passwordValue.length < 8) {
setErrorFor(input, "비밀번호를 8자 이상 입력해주세요.");
} else {
setSuccessFor(input);
}
} else if (input.name === 'confirmPassword') {
if (confirmPasswordValue === "") {
setErrorFor(input, "비밀번호를 입력하세요.");
} else if (confirmPasswordValue.length < 8) {
setErrorFor(input, "비밀번호를 8자 이상 입력해주세요.");
} else if (confirmPasswordValue === passwordValue) {
setErrorFor(input, "비밀번호가 일치하지 않습니다.");
} else {
setSuccessFor(input);
}
} else if (input.name === 'nickname') {
if (nicknameValue === "") {
setErrorFor(input, "닉네임을 입력해주세요.");
} else {
setSuccessFor(input);
}
}
}
function setErrorFor(input, message) {
const inputFormDiv = input.parentElement;
let errorDiv = inputFormDiv.querySelector('.error-message');
if (!errorDiv) {
input.classList.add('error-border');
errorDiv = document.createElement('div');
errorDiv.classList.add('error-message');
}
errorDiv.innerHTML = message;
inputFormDiv.appendChild(errorDiv);
}
function setSuccessFor(input) {
const inputFormDiv = input.parentElement;
const errorDiv = inputFormDiv.querySelector('.error-message');
if (errorDiv) {
input.classList.remove('error-border');
inputFormDiv.removeChild(errorDiv);
}
}
function checkAllInputs() {
const emailValid = form.email.value.trim() !== "" && /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/.test(form.email.value.trim());
const passwordValid = form.password.value.trim() !== "" && form.password.value.trim().length >= 8;
const confirmPasswordValid = form.confirmPassword.value.trim() !== "" && form.password.value.trim() === form.confirmPassword.value.trim();
const nicknameValid = form.nickname.value.trim() !== "";
return emailValid && passwordValid && confirmPasswordValid && nicknameValid;
}
form.addEventListener("submit", (e) => {
e.preventDefault();
if (checkAllInputs()) {
alert('로그인 성공');
window.location.href = '/items.html';
} else {
alert('입력한 정보를 다시 확인하세요.');
}
});
// 패스워드 토글기능
let passwordToggleBtns = document.querySelectorAll('.password-toggle-button');
function passwordToggle(e) {
let button = e.currentTarget;
let passwordInput = button.parentElement.querySelector('input');
let toggleIcon = document.createElement('i');
toggleIcon.classList.add('icon');
if (passwordInput.type == "password") {
passwordInput.type = "text";
toggleIcon.classList.remove('ic_visible_on');
toggleIcon.classList.add('ic_visible_off')
this.setAttribute("aria-label", "비밀번호 숨기기");
} else {
passwordInput.type = "password";
toggleIcon.classList.remove('ic_visible_off');
toggleIcon.classList.add('ic_visible_on');
this.setAttribute("aria-label", "비밀번호 보기");
}
button.innerHTML = '';
button.appendChild(toggleIcon);
}
passwordToggleBtns.forEach(btn => {
btn.addEventListener('click', passwordToggle);
});
여기서도 코드에 대한 수정사항 작성해주셨는데... 이건 그냥 패스했다.
마무리
다음과 같이 처음 사용했을 때와 멘토님 개선 사항으로 반영해보았다.
아직은 자바스크립트 개념적인 부분만 공부하다보니
예전에 사용한 느낌 생각나는대로 적다보니 조금은 부족한 부분이 있다고 생각이 든다.
확실히 변수 설정과 반복된 부분 줄여본다면 다음 스크립트 자신감 가지고 연습해보다 보면 더 좋은 코드를 만들 수 있지 않을까?
다음(?) 기능을 넣으면서 점점 발전 하기를 기대해본다.
렉시컬 스코프 (0) | 2024.06.21 |
---|---|
리액트 명령어 정리 (0) | 2024.06.19 |
자바스크립트에서 this란? (0) | 2024.06.14 |
이벤트 버블링, 캡쳐링, 위임 대하어 (0) | 2024.06.14 |
Git Flow 브랜치 전략에 대해 (0) | 2024.06.07 |