Form Validation 구현 중 느낀점

1. 모든 코드를 setState로 틀을 고정하여 생각하지 말자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
let state = {
id: "pending",
pw: "pending",
};

const [$idSuccessIcon, $pwSuccessIcon] = [
...document.querySelectorAll(".icon-success"),
];
const [$idErrorIcon, $pwErrorIcon] = [
...document.querySelectorAll(".icon-error"),
];
const [$idErrorMessage, $pwErrorMessage] = [
...document.querySelectorAll(".error"),
];
const $signinButton = document.querySelector(".button");

const render = () => {
$idSuccessIcon.classList.toggle("hidden", state.id !== "success");
$idErrorIcon.classList.toggle("hidden", state.id !== "error");

$pwSuccessIcon.classList.toggle("hidden", state.pw !== "success");
$pwErrorIcon.classList.toggle("hidden", state.pw !== "error");

$idErrorMessage.textContent =
state.id === "error" ? "이메일 형식에 맞게 입력해 주세요." : "";
$pwErrorMessage.textContent =
state.pw === "error" ? "영문 또는 숫자를 6~12자 입력하세요." : "";

if (state.id === "success" && state.pw === "success")
$signinButton.removeAttribute("disabled");
else $signinButton.setAttribute("disabled", "");
};

const setState = (newState) => {
state = { ...state, ...newState };
render();
};
const checkIdValidation = (input) => {
if (input === "") {
setState({ id: "pending" });
return;
}
const regExp =
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/;

setState({ id: regExp.test(input) ? "success" : "error" });
};

const $signIdInput = document.querySelector("#signin-userid");

$signIdInput.addEventListener(
"keyup",
_.debounce(() => {
checkIdValidation($signIdInput.value);
}, 500)
);
  • 위 코드는 만약 인풋에 입력해야할 값이 늘어 유효성 검사 항목이 늘어나게 된다면 유지보수가 어려워진다. 또한, DOM API에 의존적인 코드이므로 HTML 코드가 변경될 시 유지보수가 어려워질 수 있다.

  • 위 코드는 처음부터 구조를 setState로 잡고 데이터를 변경하는 함수는 setState가 도맡아 하고 render는 화면에 보여주는 역할을 모은 함수 이런 식으로 코드를 작성하였다.

하지만 위와 같이 틀을 미리 잡고 코드를 작성하는 것은 별로 좋지 않는 습관이다.

왜냐하면, state를 사용해야하는 상황이 아님에도 setState를 사용하여 그 구조에 억지로 끼워맞추게 되고 그렇게 되면 코드의 가독성과 이해도가 떨어질 수 있고 발전이 없는 코드를 작성하게 된다.

그러므로 코드를 작성할 때는, 우선 구현이 되도록 작성을 하되, 생각을 하면서 그 때 그 때 상황에 맞게 코드를 개선해나가야 한다.

위 코드를 리팩터링한 결과는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const signinSchema = {
userid: {
value: "",
get valid() {
return /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/.test(
this.value
);
},
message: "이메일 형식에 맞게 입력해 주세요.",
},
password: {
value: "",
get valid() {
return /\w{6,12}$/.test(this.value);
},
message: "영문 또는 숫자를 6~12자 입력하세요.",
},
get valid() {
return this.userid.valid && this.password.valid;
},
};

const checkValidation = (target) => {
signinSchema[target.name].value = target.value;

if (signinSchema[target.name].value === "") {
document
.querySelector(`#${target.id} ~ .icon-success`)
.classList.toggle("hidden", true);
document
.querySelector(`#${target.id} ~ .icon-error`)
.classList.toggle("hidden", true);
document.querySelector(`#${target.id} ~ .error`).textContent = "";
return;
}

document
.querySelector(`#${target.id} ~ .icon-success`)
.classList.toggle("hidden", !signinSchema[target.name].valid);
document
.querySelector(`#${target.id} ~ .icon-error`)
.classList.toggle("hidden", signinSchema[target.name].valid);
document.querySelector(`#${target.id} ~ .error`).textContent = !signinSchema[
target.name
].valid
? signinSchema[target.name].message
: "";
};

document.querySelector(".signin").addEventListener(
"input",
_.debounce((e) => {
checkValidation(e.target);
}, 500)
);
  • 리팩터링한 코드는 우선 setState로 틀을 구성하지 않았다. 굳이 처음부터 setState 함수로 데이터를 변경하고 렌더링까지 해주는 구조를 가질 필요가 없다.

마치 setState로 정해놓고 코드를 작성하는 것은 아이가 어른 흉내를 내는 듯한 느낌이 든다.

  • email과 password의 input에 관련이 있는 데이터를 객체로 관리하고 있다.

  • input에 이벤트가 발생할 때 마다 input의 value 값을 객체의 데이터에 변경해줘야한다.

  • 접근자 프로퍼티를 사용하여 접근자 프로퍼티를 참조하면 input.value 값과 정규 표현식이 같은지 확인하여 유효성 검사를 결과를 반환한다. 접근자 프로퍼티를 사용한 이유는 해당 접근자 프로퍼티를 참조할 때마다 갱신된 해당 프로퍼티의 유효성 검사 결과를 얻기 위해서 get을 붙여 접근자 프로퍼티로 만들었다.

각 객체에 필요한 데이터를 한곳에서 관리하고 이들을 이용하여 요구사항을 어떻게 충족시킬지 생각해보자.

2. DOM API 사용을 줄이자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const checkValidation = (target) => {
signinSchema[target.name].value = target.value;

if (signinSchema[target.name].value === "") {
document
.querySelector(`#${target.id} ~ .icon-success`)
.classList.toggle("hidden", true);
document
.querySelector(`#${target.id} ~ .icon-error`)
.classList.toggle("hidden", true);
document.querySelector(`#${target.id} ~ .error`).textContent = "";
return;
}

document
.querySelector(`#${target.id} ~ .icon-success`)
.classList.toggle("hidden", !signinSchema[target.name].valid);
document
.querySelector(`#${target.id} ~ .icon-error`)
.classList.toggle("hidden", signinSchema[target.name].valid);
document.querySelector(`#${target.id} ~ .error`).textContent = !signinSchema[
target.name
].valid
? signinSchema[target.name].message
: "";
};
  • 최대한 DOM API를 적게 사용하기 위해 해당 이벤트 핸들러의 이벤트 객체를 통해서 다른 요소에 접근할 수 있도록 코드를 작성하였다.

  • document.querySelector(`#${target.id} ~ .icon-success`) 의 의미는 이벤트가 발생한 요소의 id값을 선택자로 가져와서 해당 선택자의 일반 형제 요소 중 클래스가 icon-success인 요소를 선택하라는 뜻이다.

DOM API로 요소를 선택할 때 마다 시간이 오래 걸리고 어려운 작업이므로, 이러한 과정을 줄이도록 생각하며 코드를 작성한다.

3. input 이벤트 타입

1
2
3
4
5
6
document.querySelector(".signin").addEventListener(
"input",
_.debounce((e) => {
checkValidation(e.target);
}, 500)
);
  • 리팩터링 이전 코드에서는 keyup 이벤트를 사용하여 email과 password의 input에서 발생하는 이벤트를 이벤트 핸들러로 각각 등록해주었다.

  • 리팩터링한 코드에서는 이벤트 위임을 통해서 이벤트를 다뤄주었다.

  • 또한, 인수로 이벤트가 발생한 객체를 전달해주고 이벤트 핸들러 함수 내부에서는 target 매개변수를 해당 인수를 받아 배치해주었다.

  • e.target.name은 HTML 문서에서 name 어트리뷰트를 가리킨다. 이러한 점을 알게되어 HTML이 매우 중요하다는 것을 깨달았다.