모든 웹사이트를 만들 때 들어가지 않는 곳이 없는
회원가입 Form을 React로 만들 때 다들 어떤 식으로 만드시나요?
이번에 저도 똑같이 회원가입 Form을 만들었는데 정말 세련되지 못한 것 같아서
이번에 React hooks를 사용한 useForm 만들기
를 하면서 했던 내용들을 공유하려고 합니다.
모든 코드는 Typescript를 기준으로 작성되었고,
UI 라이브러리는 Chakra UI를 사용했습니다.
우선 Form을 만들 때 고려해야 하는 것들이 있습니다.
위 사진처럼 이름
만 받고, 버튼을 클릭하면 회원가입이 된다고 합시다.
그러면 작성해야하는 코드는 아래와 같습니다.
const [name, setName] = useState<string>("");
const [nameErrorText, setNameErrorText] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
우선 Input
의 값을 담아줄 state가 필요합니다. 그리고 Error Text
를 띄울 state와 제출 버튼을 클릭했을 때 로딩중인지 나타낼 state 이렇게 생성했습니다.
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
Input
값이 변경될 때마다 값을 갱신시켜 줄 핸들러를 생성합니다.
const validateName = () => {
if (!name) {
setNameErrorText('이름을 입력해주세요.');
} else if (name.length < 2) {
setNameErrorText('이름은 최소 1자 이상입니다.');
} else {
setNameErrorText('');
}
};
제출 버튼을 클릭했을 때 Input
값을 판별할 validation function 또한 선언해주었습니다.
const handleSubmit = async (event: React.SyntheticEvent) => {
event.preventDefault(); // 새로고침 방지
validateName(); // Input값 검증
setIsLoading(true);
};
// isLoading 값이 바뀌면 실행
useEffect(() => {
(async function() {
if (isLoading) {
if (!nameErrorText) {
await new Promise((r) => setTimeout(r, 2000));
toast({
title: `회원가입 되었습니다!`,
description: `${name}님 환영합니다!`,
status: 'success',
duration: 9000,
isClosable: true,
});
}
setIsLoading(false);
}
})();
}, [isLoading]);
그리고 제출 함수를 작성해줍니다.
useEffect
안에는 async - await
구문을 사용하기 위해서 IIFE(즉시실행함수) 구문을 사용했습니다.
isLoading
값이 변경되면 제출된 값에 대해서 Alert(toast)를 띄우는 간단한 함수입니다.
우선 Form에 필요한 데이터는 name
만 있는게 아닙니다.
보통 회원가입 페이지를 만들면 id
, password
, email
등등 많은 값들이 필요하게 됩니다.
그럼 위에서 작성한 것처럼 만들면
onChange
핸들러를 그 수에 맞게 만들고,
validation
함수도 그에 맞게 만들어야 합니다.
그럼 자연스럽게 해당 값들을 관리할 state
들이 늘어나게 되죠.
코드도 길어질 뿐더러 정말 섹시하지 못합니다.
이렇게 만드는 사람이 있냐구요?
그럼 섹시하게 Form을 만드는 방법은 무엇일까요?
섹시한 Form
을 작성하기 위한 아이디어로는 중복된 코드를 줄이는 것입니다.
우선 섹시하지 못한 Form
의 중복되는 코드들은
state
들 (input value, error text)onChange Handler
함수들validation
함수들정도로 나눌 수 있습니다.
그래서 우리는 React Hooks
를 이용해서 useForm
을 제작할 겁니다!
그럼 Hooks
는 무엇일까요? 잠깐 짚고 넘어가면
React Hooks
은 리액트의 새로운 기능으로 React 16.8버전에 새로 추가된 기능으로state
,component
에 대한 것들을 바꿔놓았습니다.
React Hooks
예를 들면 function component
에서 state
을 가질 수 있게 된 것이죠
만일 앱을 react hook
을 사용하여 만든다면 class component
, render
등을 안해도 된다는 뜻입니다.
모든 것은 하나의 function
이 되는 것 함수형 프로그래밍이 가능해지는 것입니다.
보통 Hooks 들의 이름 앞에는 use
를 붙입니다.
그래서 요약을 하자면 우리는 form
을 만들건데,
render
함수 또는 생명주기 함수들을 사용하지 않는 hook
을 사용하니, 이름은 useForm
짓자.
이렇게 됩니다.
우선 useForm의 전체 코드를 살펴보고 하나하나씩 뜯어보겠습니다.
// useForm.ts
import { useEffect, useState } from "react";
function useForm({ initialValues, onSubmit, validate }: useFormProps) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isLoading, setIsLoading] = useState(false);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setValues({ ...values, [name]: value });
};
const handleSubmit = async (event: React.SyntheticEvent) => {
setIsLoading(true);
event.preventDefault();
await new Promise((r) => setTimeout(r, 1000));
setErrors(validate(values));
};
useEffect(() => {
if (isLoading) {
if (Object.keys(errors).length === 0) {
onSubmit(values);
}
setIsLoading(false);
}
}, [errors]);
return {
values,
errors,
isLoading,
handleChange,
handleSubmit,
};
}
export default useForm;
function useForm({ initialValues, onSubmit, validate }: useFormProps) {
우선 useForm
을 사용하는 곳에서 받아올 매개변수들입니다.
initialValues
는 말 그대로 초기값 입니다.
이름, 아이디, 패스워드, 이메일등등의 값들을 하나씩 정의하는 것이 아닌
객체로 생성해서 하나의 state
로 관리를 합니다.
useForm({
initialValues: {
id: '',
password: '',
},
});
이런식으로 useForm
을 사용하는 곳에서 state
들을 정의할 수 있습니다.
onSubmit
와 validate
도 같은 맥락으로 제출 함수와 검증 함수를 useForm
을 사용하는 곳에서 선언에서 사용할 수 있도록 하는 것입니다.
// useForm.ts
const [values, setValues] = useState(initialValues);
위에서 받은 initialValues
는 useForm
안에서 values
라는 state
로 저장이 됩니다.
그러면 각각의 state
들은 어떻게 할까요? useForm
안에서의 onChange
는 어떻게 작성될까요?
// SignUpForm.tsx
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setValues({ ...values, [name]: value });
};
섹시하지 못한 Form
에서 각각의 state
들마다 handler를 하나씩 생성해주었는데, useForm
에서는 하나로 통일했습니다.
들어오는 event.target
객체의 name
과 value
를 받습니다.
그래서 Input
컴포넌트의 name
속성으로 state
와 같은 이름을 지정해줘야 합니다.
<Input name="id" />
이런식으로 말이죠.
이렇게 코딩을하면 Input
컴포넌트가 많아진다해도, name
만 변경해주고, 그에 맞는 initialValues
만 선언해주면 handleChange
함수를 또 만들지 않아도 됩니다.
그럼 입력 검증은 어떤식으로 진행될까요?
우선 저는 validation을 코드를 적을 파일을 새로 작성해주었습니다.
// SignUpValidation.ts
type SignUpValidationProps = {
name?: string,
email?: string,
id?: string,
password?: string,
}
export default function SignUpValidation({ name, email, id, password }: SignUpValidationProps) {
const errors: SignUpValidationProps = {};
if (!name) {
errors.name = '이름이 입력되지 않았습니다.'
}
/*
else if (Some regex validation) {
errors.name = 'Some error text';
}
*/
if (!email) {
errors.email = '이메일이 입력되지 않았습니다.';
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email)) {
errors.email = '입력된 이메일이 유효하지 않습니다.';
}
if (!id) {
errors.id = '아이디가 입력되지 않았습니다.';
}
/*
else if (Some regex validation) {
errors.id = 'Some error text';
}
*/
if (!password) {
errors.password = '비밀번호가 입력되지 않았습니다.';
} else if (password.length < 8) {
errors.password = '8자 이상의 패스워드를 사용해야 합니다.';
}
return errors;
}
저는 4개의 값을 받고, 해당 값에 대한 검증을 진행했습니다.
그리고 각 validation에 걸린다면, errors 객체에 error text를 달아서 반환해주는 식입니다.
그리고 useForm
을 사용하고자 하는 컴포넌트에서
// SignUpForm.tsx
import SignUpValidation from './SignUpValidation';
const {
values, errors, isLoading, handleChange, handleSubmit
} = useForm({
initialValues: ...,
onSubmit: ...,
validate: SignUpValidation,
});
아까 useForm.ts
파일에서 validation
을 받도록 되어있기 때문에,
validate: SignUpValidation
이런식으로 validation 함수를 넘겨줍니다.
그러면 useForm
에서는
// useForm.ts
const handleSubmit = (event: React.SyntheticEvent) => {
setIsLoading(true);
event.preventDefault();
setErrors(validate(values));
};
useEffect(() => {
(async () => {
if (isLoading) {
if (Object.keys(errors).length === 0) {
await onSubmit(values);
}
setIsLoading(false);
}
})();
}, [errors, isLoading]);
제출 함수인 handleSubmit
이 실행될 때, errors state의 setter인 setErrors
를 통해서 저희가 넘겨준 validate 함수 SignUpValidation
함수를 실행해서 errors
객체를 state
로 변경시켜줍니다.
그럼 errors
state가 변경되었으니, 아래 useEffect
가 실행이 되겠죠
그럼 errors
의 key
의 개수를 파악하는 식으로 에러가 났는지 안났는지 확인할 수 있습니다.
if (Object.keys(errors).length === 0) {
// Submit API 호출!
}
다음과 같이 말이죠.
그럼 validation
진행은 알았고, error text
는 어떻게 나타낼까요?
useForm
을 사용하는 컴포넌트에서 errors
state를 사용하면 됩니다.
const FormComponent = () => {
const {
values,
errors,
isLoading,
handleChange,
handleSubmit,
} = useForm({
initialValues:...,
onSubmit:...,
validate:...,
});
return (
<form>
<input type="email" name="email" />
<p>{errors.email}</p>
</form>
);
}
이런식으로요
useForm
을 사용해서 form
을 작성하는게 정답인 것은 아닙니다.
이것보다 더 좋은 코드가, 효율적인 코드가 있을 수도 있습니다!
이렇게 작성하는 방법도 있구나, 라고 생각해주시면 감사하겠습니다.
맨날 그냥 코드를 작성하다가, 어떻게 하면 더 효율적으로 더 깔끔하게 짤 수 있을까를 생각하면 점점 더 실력이 느는 것 같습니다.
저기서 성능 최적화까지 진행된다면 더욱더 섹시한 코드가 되겠죠.
저도 이번에 form
을 작성하면서 이렇게도 짤 수 있구나를 알게되어서 되게 좋았습니다.
저도 이제 섹시해지고 싶어서 이 글을 보게 되었습니다,,
이제 가능할 것 같네요 +_+