form을 제출하는 방법에는 다음과 같이 여러 방법이 존재한다.
button의 type은 기본적으로 submit이기 때문에 form요소 안에 있는 button을 클릭할 경우 form이 자동으로 제출된다.
따라서 onClick을 통해 form요소를 사용하는 함수를 연결한 뒤 버튼만 클릭하면 form의 데이터를 사용할 수 있다.
하지만 이렇게 제출할 경우 웹사이트가 자동으로 새로고침이 되는 것을 확인할 수 있다.
이러한 새로고침을 막기 위해서는 button의 type을 button으로 설정하는 것과 다음에 소개할 방법을 사용하면 된다.
button의 onClick에서 form의 데이터를 사용하는 함수를 연결하는 것이 아닌 form요소의 onSubmit이벤트 핸들러를 사용하는 방법도 존재한다.
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
return (
<form onSubmit={handleSubmit}>
...
...
</form>
)
위의 코드와 같이 form요소의 onSubmit에 form내부의 데이터를 활용할 함수를 연결한 뒤 e.preventDefault()를 호출하면 해당 함수가 form제출시 페이지의 새로고침을 방지해준다.
form내부에 있는 데이터를 사용하기 위해서는 input요소에 연결한 state나 ref를 직접 사용하는 방법이 존재하지만 formData를 통해 form내부에 존재하는 입력 값을 가져오는 방법도 존재한다.
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const data: Partial<Record<SignupKeyTypes, FormDataEntryValue | FormDataEntryValue[]>> =
Object.fromEntries(fd.entries());
}
위와 같이 FormData객체를 새로 만들어준 뒤 해당 인자로 e.currentTarget을 넣어주면 해당하는 form의 입력값을 해당 태그의 name을 기준으로 가져올 수 있다.
또한 data객체를 만들어 해당 객체에 Object.FromEntries(fd.entries())를 통해 form의 입력값들을 가져올 수 있다.
하지만 위의 방법으로는 checkBox와 같은 요소의 데이터는 가져올 수 없기 때문에 다음과 같은 코드를 통해 직접 가져와 data객체에 넣어주어야 한다.
const acquistionChannel = fd.getAll("acquisition");
data.acquisition = acquistionChannel;
위의 코드에서 data의 타입 지정부분이 조금 복잡할 수 있지만, 타입스크립트의 장점을 최대한 활용하기 위해서는 꼭 알고 넘어가야 한다.
우선 Record를 사용한 이유는 객체의 타입을 정하기 위해 사용했는데 Record<K, V>의 형태는 K를 객체의 key, V를 객체의 value로 사용하는 타입을 만들어준다.
따라서 위의 코드에서는 SignupKeyTypes를 key로 가지고, FormDataEntryValue | FormDataEntryValue[]를 value로 가지는 타입을 지정해준다.
SignupKeyTypes의 경우에는 커스텀 type으로, form내부에 존재하는 입력태그의 name을 값으로 가지는 타입을 선언해주었다.
type SignupKeyTypes =
| "email"
| "password"
| "confirm-password"
| "first-name"
| "last-name"
| "role"
| "acquisition"
| "terms-and-conditions";
FormDataEntryValue는 FormData에서 가져올 수 있는 값의 타입으로, 미리 string | File 타입으로 정의되어있다.
여기서는 checkbox에 존재하는 데이터도 사용하기때문에 fd.getAll()을 통해 불러올 경우 FormDataEntryValue[]형태의 배열을 사용하기에 value의 타입으로 넣어주었다.
하지만 data의 타입을 Record<SignupKeyTypes, FormDataEntryValue | FormDataEntryValue[]>으로만 지정하게 될 경우 Object.fromEntries(fd.entries())를 호출하는 시점에서는 타입스크립트가 Object.fromEntries(fd.entries())의 결과가 SignupKeyType의 모든 키를 반드시 포함하지 않을 수도 있다고 판단하기 때문에 오류가 발생한다.
따라서 해당 타입을 Partial로 감싸주어 해당 타입의 프로퍼티를 optional하게 바꿔주었다.
입력값에 대한 유효성 검사를 진행할 때 사용자 경험 측면에서 고려해야할 점이 많다.
유효성 검사를 통한 오류메세지가 너무 빨리 뜨거나, 너무 오래 뜨거나, 너무 늦게 뜨는 경우가 존재하기 때문이다.
따라서 위의 상황을 충분히 고려하여 유효성 검사를 진행해야 한다.
입력 태그에는 모두 require이라는 속성을 통해 유효성을 검사할 수 있다.
또한 minLength와 같은 속성을 통해 최소 입력값을 정할 수 있다.
하지만 이러한 내장 속성을 사용하는 경우 사용자가 언제든지 개발자도구를 통해 해당 속성을 없앨 수 있기 때문에 해당 방법만 사용하는 것은 권장되지 않는다.
useState를 사용하여 입력 태그의 value와 onChange에 연결할 경우 매 입력시마다 유효성을 검사할 수 있다.
const [enteredValue, setEnteredValue] = useState<LoginType>({
email: "",
password: ""
});
const emailIsInvalid = !enteredValue.email.includes("@");
하지만 위와 같이 유효성 검사를 진행할 경우 사용자는 페이지에 접속하지마자 invalid메세지를 보게되고, email에 @가 들어갈때까지 사라지지 않는다.
따라서 사용자는 오류 메세지를 너무 빨리 보게되는 상황이 발생한다.
이런 경우 입력 태그의 onBlur를 통해 해당 태그의 포커스가 해제될 경우에만 유효성 검사를 할 수 있다.
const [enteredValue, setEnteredValue] = useState<LoginType>({
email: "",
password: ""
});
const [didEdit, setDidEdit] = useState<Record<LoginKeyTypes, boolean>>({
email: false,
password: false
});
const emailIsInvalid = didEdit.email && !enteredValue.email.includes("@");
const handleInputChange = (identifier: LoginKeyTypes, value: LoginType[LoginKeyTypes]) => {
setEnteredValue((prev) => ({ ...prev, [identifier]: value }));
setDidEdit((prev) => ({ ...prev, [identifier]: false }));
};
const handleInputBlur = (identifier: LoginKeyTypes) => {
setDidEdit((prev) => ({ ...prev, [identifier]: true }));
};
...
...
<input
id="email"
type="email"
name="email"
onBlur={() => handleInputBlur("email")}
onChange={(e) => {
handleInputChange("email", e.target.value);
}}
value={enteredValue.email}
/>
위의 코드를 통해 처음 포커스된 후 부터 유효성 검사를 시작하며, 사용자가 입력을 시작하는 시점에 유효성 검사를 멈추고 다시 포커스가 해제되면 유효성 검사를 시작하게 된다.
하지만 이렇게 할 경우 맨 처음 접속시에 바로 Login버튼을 클릭하게 될 경우 폼이 제출되어 버리기 때문에 다음과 같은 코드를 handleSubmit에 추가해야 한다.
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setDidEdit({ email: true, password: true });
if(enteredValue.email==="" || enteredValue.password===""){
return;
}
setEnteredValue({ email: "", password: "" });
setDidEdit({ email: false, password: false });
console.log(enteredValue);
};
코드를 살펴보면 setDidEdit({ email: true, password: true });를 통해 폼 제출시 didEdit을 모두 true로 만들어 const emailIsInvalid = didEdit.email && !enteredValue.email.includes("@");에 대한 검증을 진행하여 오류 메세지를 뜰 수 있도록 해준 뒤, email또는 password가 빈 문자열일 경우 return을 통해 함수의 동작을 정지시킨다.
위의 코드는 value를 관리하는 state와 포커스 되었던 상태인지를 관리하는 didEdit으로 이루어져있기 때문에 커스텀 hook을 통해 구현하는 것도 가능하다.
import { useState } from "react";
export function useInput<T>(defaultValue: T, validationFn: (value: T) => boolean) {
const [enteredValue, setEnteredValue] = useState<T>(defaultValue);
const [didEdit, setDidEdit] = useState<boolean>(false);
const valueIsVaild = validationFn(enteredValue);
const handleInputChange = (value: T) => {
setEnteredValue(value);
setDidEdit(false);
};
const handleInputBlur = () => {
setDidEdit(true);
};
return {
value: enteredValue,
handleInputBlur,
handleInputChange,
hasError: didEdit && !valueIsVaild
};
}
위와 같이 구현한 뒤, 다음과 같이 사용할 수 있다.
const {
value: emailValue,
handleInputBlur: handleEmailBlur,
handleInputChange: handleEmailChange,
hasError: emailHasError
} = useInput("", (value) => isEmail(value) && isNotEmpty(value));
const {
value: passwordVaule,
handleInputBlur: handlePasswordBlur,
handleInputChange: handlePasswordChange,
hasError: passwordHasError
} = useInput("", (value) => hasMinLength(value, 8));
더욱 간단하게 제출시에만 유효성 검사를 진행하여 복잡한 로직 없이 구현할수있다.
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const data: Partial<Record<SignupKeyTypes, FormDataEntryValue | FormDataEntryValue[]>> =
Object.fromEntries(fd.entries());
const acquistionChannel = fd.getAll("acquisition");
data.acquisition = acquistionChannel;
if (data["password"] !== data["confirm-password"]) {
setPasswordsAreNotEqual(true);
return;
}
setPasswordsAreNotEqual(false);
console.log(data);
e.currentTarget.reset();
};
위의 코드와 같이 단순하게 폼이 제출되는 시점에 한번만 유효성 검사를 진행할수도 있다.