부트캠프에서 회원가입 페이지의 유효성 검사를 하는 과제가 주어졌다. 요구사항은 아주 간단했다.
- 아이디가 4글자 이상이어야 함
- 비밀번호와 비밀번호 확인창의 두 값이 일치해야 함
- 아이디 길이와 비밀번호 일치 여부에 따른 메시지를 각각의 상황에 적합하게 화면에 출력해야 함
기본 과제는
1) 위의 두 가지 요구사항을 확인하기 위해 true, false 두 가지 boolean 값을 반환하는 함수를 각각 만들고,
2) 해당 유효성 검사 함수 통과 여부에 따라 화면에 적합한 메시지를 출력하는 핸들러 함수를 만들어서
3) 아이디 입력창과 비밀번호 확인 입력창에 2)의 핸들러가 동작하게 하는 keyup 이벤트 리스너를 달면 통과였다.
이벤트 리스너를 달지 않고 onkeyup 으로 처리해도 과제 통과가 되긴 했는데, 기본적으로 인라인으로 뭘 다는 것보다 마크업, 스타일, javascript 작동을 최대한 분리하는 걸 선호해서 둘 다 이벤트 리스너를 달아서 기본 과제는 손쉽게 통과했다. 메시지 화면 출력은 hide 라는 클래스명을 사용해서 css에서 display: none 으로 제어해줬다.
(사족으로 이런 분리를 관심사 분리 seperation of concerns 라고 부른다는 걸 이번 과제를 하면서 처음으로 알았다.)
//DOM으로부터 필요한 엘리먼트를 불러와서 변수 지정
const $username = document.querySelector("#username");
const $successMsg = document.querySelector(".success-message");
const $failuresMsg = document.querySelector(".failure-message");
const $pwd = document.querySelector("#password");
const $repwd = document.querySelector("#password-retype");
const $mismatchMsg = document.querySelector(".mismatch-message");
const $signupBtn = document.querySelector(".signup button");
function isMoreThan4Length(value) {
return value.length >= 4;
}
function handleUserNameValidation() {
if (isMoreThan4Length($username.value)) {
$successMsg.classList.remove("hide");
$failuresMsg.classList.add("hide");
} else {
$successMsg.classList.add("hide");
$failuresMsg.classList.remove("hide");
}
}
$username.addEventListener("keyup", handleUserNameValidation);
// isMatch 함수는 문자열 두 개를 입력으로 받고, boolean 타입을 리턴하는 함수
function isMatch(password1, password2) {
if (password1 === password2) return true;
else return false;
}
function handlePasswordValidation() {
if (isMatch($pwd.value, $repwd.value)) { // 비밀번호 일치
$mismatchMsg.classList.add("hide");
} else { // 비밀번호 불일치
$mismatchMsg.classList.remove("hide");
}
}
$repwd.addEventListener("keyup", handlePasswordValidation);
따로 테스트 통과 여부를 확인할 수 있는 코드는 없었지만 기본 과제를 통과한 수강생은 심화 학습으로 회원가입 버튼을 눌렀을 때 유효성 검사까지 진행해 보면 좋겠다는 추가 과제 instruction이 있어서 도전해 보기로 했다.
아래는 회원가입 유효성 검사를 향해 가는 이틀 간의 여정 일지다.
1차 시도 : 회원가입 버튼을 disabled 속성으로 비활성화를 시켜놓고, 요구사항을 충족하면 disabled을 풀어주자!
1차 의사 코드 :
- 유효성 검사를 두 개 다 통과하면
- 회원가입 버튼의 disabled 속성 제거
if (
$successMsg.className === "success-message" &&
$failuresMsg.className === "failure-message hide" &&
$repwd.value.length !== 0 &&
$mismatchMsg.className === "mismatch-message hide"
) {
console.log("well done!"); // 제대로 동작하는지 알아보기 확인차 console에 꼭 뭔가를 찍어본다.
$signupBtn.setAttribute("disabled", "false");
}
1차 코드는 이렇게 시작했다. 유효성 검사를 통과한 시점의 화면 최종 상태(메시지 출력 여부)로 회원가입 버튼의 disabled 속성값을 제어하려고 했다. 하지만 마크업에 버튼 태그에 disabled 속성을 걸어놓고 시작하니까 아이디와 비밀번호 창에 유효한 값을 입력해도 한 번 닫혀버린 회원가입 버튼의 disabled 속성이 계속 풀리지 않아서 원하는 대로 동작하지 않았다.
📌 문제점: 요소의 className 값을 실시간으로 받아오는 게 아니다.
처음 페이지를 로딩하면서 코드를 쭉 읽어내려가기 때문에 시작하자마자 세팅된 값에서 바뀐 값을 읽어와서 if 조건문 통과 여부에 따라 분기를 줄 수가 없었다.
💡 해결 시도:
그렇다면 뭔가 이벤트가 발생했을 때 그 시점의 값을 받아오는 이벤트 리스너를 달아주자!
그런데 또 난관에 봉착했다. 버튼을 비활성화를 시킨 상태에서 페이지를 시작하면 회원가입 버튼을 어떻게 누르지...?
지금 돌이켜보니 여기서부터 잘못된 길이라 처음 접근법부터 다시 짜야 하는 건데 나는 이 길을 우회해서 갈 수 있다고 믿었다. 🧐
버튼에 이벤트 리스너를 달지 말고 마지막 입력창, 즉 비밀번호 확인창에 비밀번호 확인을 끝냈을 때 아이디와 비밀번호 검사를 둘 다 통과한 값이 입력되어 있으면 회원가입 버튼의 disabled를 풀어줘서 버튼을 클릭할 수 있게 하면 되지 않을까?!
그런데 이미 비밀번호 확인창에 keyup 이벤트 리스너가 달려 있는데, 같은 요소에 이벤트 리스터 2개를 달아도 되나?
(검색 또 검색...👩🏻💻)
된다! $repwd(비밀번호 확인창)에 다른 핸들러 함수를 달아서 해결하면 되겠군!
onkeyup과 keyup 이벤트 리스너의 차이 :
- onkeyup은 이벤트를 여러 개 적용하는 것이 불가능하다. 새로운 이벤트를 추가하면 추가한 이벤트가 기존 이벤트를 덮어 쓴다.
- 반면 이벤트 리스너는 같은 요소에 다른 핸들러 함수를 정의해서 여러 개의 이벤트를 동작하게 할 수 있다.
2차 시도 : 비밀번호 확인창에 이벤트 리스너 하나 더 달기
function handleValidationPass() {
if (
$successMsg.className === "success-message" &&
$failuresMsg.className === "failure-message hide" &&
$repwd.value.length !== 0 &&
$mismatchMsg.className === "mismatch-message hide"
) {
$signupBtn.removeAttribute("disabled");
}
}
$repwd.addEventListener("keyup", handleValidationPass);
일단 2차 시도의 근본적인 문제점을 이야기하기에 앞서 1차 시도에서 setAttribute('disabled', 'false') 로 제어하려던 것에서 removeAttribute('disabled') 를 쓰는 방식으로 바꾼 이유는 회원가입 버튼의 활성화/비활성화 상태를 시각적으로 바로 표현하기 위해서 disabled 속성 유무에 따라 커서를 바꿔주는 css를 아래와 같이 적용한 상태였다.
button {
cursor: pointer;
/* 그 외 많은 property와 value */
}
button:disabled {
cursor: not-allowed
}
이렇게 하니까 disabled의 값이 true인지 false인지와 상관없이 커서가 not-allowed 여서 버튼 클릭 자체를 할 수가 없었다...! setAttribute로으로 disabled의 값을 true, false로 조정해주는 게 아니라 disabled 속성 자체를 없애주든지, css의 선택자를 button[disabled='true'] 로 바꿔주든지 둘 중 하나를 택해야 했다. 후자가 더 복잡해지는 것 같아서 removeAttribute를 하는 방법으로 가기로 했다.
하지만 이 방법도 문제가 있었다.
📌 문제점: 비밀번호 재입력 창에서 keyup 이벤트로 유효성 검사를 한 번 통과해서 disabled 속성이 제거된 뒤에 다시 아이디나 비밀번호 입력창을 수정하는 경우, 해당 입력값이 유효성 검사를 통과하지 못하는 값이더라도 disabled 속성이 다시 실시간 값에 맞춰 세팅되었다 제거되었다 하는 코드가 아니다.
💡 해결 시도: 결국 회원가입 버튼을 누르는 시점에서 최종적으로 유효성 검사를 하는 게 필요하다. 즉, 회원가입 버튼에 이벤트 리스너를 달아야 한다.
+) 유효성 검사를 하는 함수 2개를 기껏 만들어놓고 왜 안 쓰고 있지?! 라는 생각이 이때 떠오름....
너무 당연한 결론 아냐...? 🤷🏻♀️ 라고 어깨를 으쓱하실 지도 모르겠다 ㅋㅋㅋㅋㅋ
회원가입 버튼의 disabled 속성을 위해 비밀번호 재입력 창으로 뭔가를 제어하려고 시도하다보니 이런 삽질을 해버렸다. 퀘스트 이름 자체가 회원가입 버튼 유효성 검사인데 뭘 한거니... 그래도 여기까지 오느라 수고했다, 내 자신... 그리고 그 와중에 소소하게 배운 것들도 있었다.
3차 시도: 회원가입 버튼에 이벤트 리스너를 달아서 disabled 속성을 제어해보자...!
function validate() {
if (
!isMatch($pwd.value, $repwd.value) &&
!isMoreThan4Length($username.value)
) {
$signupBtn.setAttribute("disabled", "true");
} else {
$signupBtn.removeAttribute("disabled");
}
}
$signupBtn.addEventListener("click", validate);
하지만 회원가입 버튼에 이벤트 리스너를 단다고 만사 오케이는 아니었다.
회원가입 버튼을 누르는 시점에 두 가지 유효성 검사 함수(ismorethan4 && isMatch)를 통과했는지 여부에 따라 버튼의 disabled 속성을 제어하려고 해도 마찬가지였다.
📌 문제점:
한 번 잘못된 상태, 즉 유효성 검사 함수를 둘 다 통과하지 못하는 상태에서 회원가입 버튼을 클릭해버리면 회원가입버튼이 비활성화되어서 그때부터는 버튼 클릭을 시도할 수가 없었다.
💡 해결 시도:
아, 근본적으로 잘못 되었구나...! disabled 속성을 가져다 버리자...! ㅋㅋㅋㅋㅋ
사실 맨처음 회원가입 유효성 검사를 검색해 봤을 때 몇몇 포스팅을 후루룩 살펴보았는데, 정규표현식부터 그외에도 알 수 없는 코드들이 난무해서 일단 내가 가진 지식으로 문제를 해결해 보고 싶었다. 하지만 모두 처-참-히 실패했다.
그러니 정석으로 돌아가서 해결 방법을 분석하고 내 코드에 적용하자.
4차 시도 : 회원 가입 유효성 검사란 무엇인지 분석
회원가입 버튼의 disabled 속성으로 유효성 검사를 해보려던 시도는 가져다 버리자. 모두 다 잊고 새로 시작해 보자...! 😇
기본적으로 회원가입 페이지를 만든다는 것은 아이디, 비밀번호 등등 사용자가 입력한 값을 받아 서버에 전송하는 것이다. 버튼을 입력하는 시점에 각종 입력값들을 서버에 넘기려면 어떻게 해야 하지?
💡 해결 시도 1 : 우리에게는 <form> 태그가 필요하다!!!
과제에서 주어진 마크업에는 회원가입 버튼 유효성 검사까지 테스트 코드에 들어가 있지 않았기 때문에 위의 화면은 처음에 <body> 아래에 <main> 태그로 감싸져 있었다. 아이디, 비밀번호, 비밀번호 재확인 입력창, 회원가입 버튼까지 <form> 태그로 감싸주는 마크업 수정 작업을 했다.
💡 해결 시도 2 : 회원가입 버튼의 핸들러 함수인 validate 함수 재정의
📍 유효성 검사의 핵심
1) 회원가입 버튼을 '클릭'했을 때
2) 입력값들이 유효한지
- 다시 말해 필수 입력값들이 빈 값은 아닌지
- 유효성 검사 함수(예제의 isMoreThan4Length, isMatch 함수)가 true를 반환하는지 여부를 체크한 뒤에
3) 유효성 검사를 모두 통과했다면 입력값들을 서버에 전송해준다.
이를 위해서는 회원가입 버튼의 type 속성 값이 중요했다.
type 속성을 지정해 주지 않을 경우 submit이 디폴트 값인데, 유효성 검사를 하기 위해서는 버튼의 타입을 button으로 지정해 줘서 회원가입 버튼을 클릭했을 때는 아무 일도 일어나지 않고, validate 함수가 클릭 이벤트로 인해 호출되었을 때 유효성 검사 통과 여부에 따라 submit 해주는 과정이 유효성 검사의 핵심이었다!
정규표현식의 거대한 벽이 날 겁부터 먹게 했지만, 실상 정규표현식은 입력값 유효 여부를 거르는 하나의 체에 불과했을 뿐 핵심사항은 아니었던 것이다.
자, 이제 마법의 코드를 공개한다. 짜잔...!
function validate() {
console.log("btn clicked");
if (
!$username.value ||
!$pwd.value ||
!isMoreThan4Length($username.value) ||
!isMatch($pwd.value, $repwd.value)
)
return false;
document.signup_form.submit(); // 여기에 밑줄 긋고 별표 세 번 그리세요!!!
}
$signupBtn.addEventListener("click", validate);
위의 코드 14줄을 말로 풀어서 설명해 보자.
1) 회원가입 버튼을 클릭하면 validate 함수가 실행된다.
2) 클릭이 잘 된 것인지 확인하기 위해 'btn clicked'를 콘솔에 찍어준다. (당연히 이 과정은 필요 없는 부수적인 라인이다)
3) 다음의 네 가지 경우에 validate 함수는 무조건 false를 return 한다. 즉 return 이후의 코드를 실행하지 않는다!! (중요)
- 아이디 입력창이 빈 값인 경우
- 비밀번호 입력창이 빈 값인 경우
- 아이디가 4글자 미만인 경우 (isMoreThan4Length 함수가 false를 반환)
- 비밀번호와 비밀번호 확인 창의 값이 일치하지 않는 경우 (isMatch 함수가 false를 반환)
4) 반대로 4가지 경우 중 어느 하나에도 해당하지 않는다면:
document.signup_form.submit() 을 실행한다!!!
그런데 signup_form 은 뭘까?
해결 시도 1에서 form 태그를 만들어주면서 form 태그의 name 속성 값으로 signup_form을 지정해 주었다. 이렇게 form 태그의 name 값을 지정해주면 querySelector로 따로 변수 지정을 하지 않고도 javascript가 바로 form 태그를 인식한다. 바로 아래와 같이...!
회원가입 버튼을 클릭하는 시점이 아닌, 유효성 검사를 모두 통과했을 때 그제서야 form 요소가 submit() 호출로 인해 입력값들이 서버에 제출되는 것이다!
form 요소를 쓸 때는 method 라는 속성을 써서 서버에 데이터를 어떻게 전송할 것인지 결정한다.
method의 속성값으로는 post 와 get 이 있는데, post가 정보를 숨겨서 전송할 수 있기에 실제 회원가입 양식 등에서 쓰는 값이지만 우리는 지금으로서는 데이터를 넘길 서버가 없기 때문에 get을 써서 실제 회원정보 버튼을 누르고, 유효성 검사를 모두 통과했을 때 어떤 일이 일어나는지 살펴보자.
처음 화면을 불러와서 아무 것도 입력하지 않은 상태에서 회원가입 버튼을 눌러보았다. 콘솔에 btn clicked가 찍히는 걸로 봐서 회원가입 버튼에 달린 이벤트 리스너가 잘 작동하고 있지만 유효성 검사를 통과하지 못했기에 아무 일도 일어나지 않았다. 즉, 데이터가 서버에 제출이 되지 않는다.
이번에는 유효성 검사를 통과하지 못하는 값들을 입력해 보았다. btn clicked의 수가 올라간 걸로 봐서 회원가입 버튼은 눌려졌지만 마찬가지로 아무 일도 일어나지 않는다. (메시지 출력 관련 스타일링은 안 한 거는 모르는 척 눈 감고 넘어가 주시길 🙄)
유효한 값들을 입력하고 회원가입 버튼을 누르기 전 화면이다. 콘솔에서 isMoreThan4Length와 isMatch 두 함수를 모두 잘 통과했다는 걸 확인할 수 있다.
회원가입 버튼을 눌렀더니 페이지가 새로고침되면서 입력한 값들이 모두 사라졌다.
이제 빨간색으로 표시한 브라우저의 url 입력창에 주목해 보자.
index.html 파일의 경로(.../index.html)가 나온 뒤에 ? 가 붙으면서 id=user&pwd=12345 라는 문자가 주소창에 붙었다!
<form> 태그에 name 값을 주었을 때 document.signup_form으로 바로 해당 요소에 접근할 수 있었던 것처럼 내가 서버에 전송하고 싶은 input 창에 name 속성과 그 값을 지정해 주면 데이터 제출시 해당 input의 입력값 즉, input.value 가 서버로 전송된다.
위의 캡처 화면에서 볼 수 있듯이 마크업 수정을 하면서 아이디 입력창의 name을 id로, 비밀번호 입력창의 name을 pwd로 세팅해주었고, 회원버튼 가입을 누르는 시점에 내가 입력한 값은 각각 user, 12345였으며 이 두 값이 모두 유효성 검사를 통과해서 서버로 전송된 것이다.
마무리
길고 긴 이틀 간의 여정의 마침표가 찍히는 순간이었다.
사실 validate.js 라는 라이브러리가 있는 것도 페어 분이 알려주셨지만 지금은 일단 내가 할 수 있는 선에서 할 수 있는 만큼을 해보고 싶었다. 그리고 그 과정에서 어렴풋하던 것들이 착실히 내 것으로 정리가 되고, 새롭게 배운 것들도 너무 많았다. 모든 과제에 이만큼 시간과 여력을 들일 수 있으면 좋겠지만 현실은 그렇지가 않다. 🥲 다행히 이번 과제는 기본 과제를 무척 수월하게 진행할 수 있었던 데다가 과제에 배정된 시간도 많아서 이런저런 시도들을 마음껏 해 볼 수 있었다.
물론 위의 시도는 실제 회원가입 페이지를 만들기에는 너무나 부족한 수준이다. 하지만 입력한 아이디가 숫자+영문 조합인지, 비밀번호는 숫자와 영문 대소문자, 특수문자의 조합인지 등을 정규표현식으로 검사하는 방법은 앞으로 차차 채워나가면 될 일이다. (정규표현식 test mdn)
마음껏 삽질할 자유가 있는 이 시간이 너무 소중하고, 또 부족하나마 이 과정을 통해서 내가 배운 것들이 누군가에게도 도움이 되었으면 좋겠다!
참고자료:
'돌멩이 하나 > 에러는 미래의 연봉' 카테고리의 다른 글
배열의 push() 메소드는 무엇을 return할까? (0) | 2022.12.27 |
---|---|
[React] form 데이터 서버에 전송하기 (0) | 2022.12.18 |
export와 import : 로컬 vs. 서버 (0) | 2022.12.04 |
[React] map() 메소드로 여러 개의 html 엘리먼트 표시할 때 JSX key 속성과 싸운 이야기 (0) | 2022.11.30 |
나만의 아고라 스테이츠 만들기 과제 되돌아보기 (3) | 2022.11.20 |