안녕하세요. 저는 데브시스터즈 플랫폼셀에서 백엔드 엔지니어로 일하고 있는 이창희입니다. 저는 그동안 쌓은 대부분의 경력이 백엔드와 프론트엔드 개발을 같이하는, 흔히 말하는 풀 스택 엔지니어였는데요 오늘은 그 중 프론트엔드 개발 경험을 살려 흥미로운 이야기를 해볼까 합니다.
프론트엔드 개발을 하다 보면 폼을 통해 사용자에게 값을 입력받고, 검증하고 서버로 전송해야 하는 경우가 있습니다. 얼마 전에 새로 단장한 데브시스터즈 채용 홈페이지의 채용 지원 폼도 그러한데요. 이 글에서는 채용 홈페이지를 새로 만들며 입력이 많고 복잡한 폼을 함수형 라이브러리인 fp-ts를 사용하여 폼 데이터 상태 관리를 우아하게 처리한 경험에 대해 소개합니다.
#기본적인 폼 상태 관리
데브시스터즈 채용 홈페이지는 React를 사용하여 개발했습니다. React에서 기본적인 상태 관리는 React.useState 를 사용하는데요. 예를 들어 React.useState 를 사용해 채용 지원 폼에 있는 휴대폰 번호가 올바른 휴대폰 번호인지(적어도 아주 이상한 번호는 아닌지) 검증을 한다면 아래와 같이 명령형 코드로 작성할 수 있을 것입니다.
const [mobileNumber, setMobileNumber] = React.useState<string>('');
const [mobileNumberError, setMobileNumberError] = React.useState<string>('');
// 올바른 형식의 휴대폰 번호인지 검증하는 함수
const validateMobileNumber = (value: string): boolean => {
if (value == '') {
setMobileNumberError('휴대폰 번호를 입력해주세요.');
return false;
}
if (!mobileNumberRegex.test(value)
|| !value.startsWith('01')
|| value.length < 10
|| value.length > 11
) {
setMobileNumberError('휴대폰 번호가 올바르지 않습니다.');
return false;
}
return true;
}
// 휴대폰 번호 input의 onChange 이벤트 핸들러
const handleMobileNumberChange = (e) => {
const { value } = e.target;
validateMobileNumber(value);
setMobileNumber(value);
}
// form onsubmit 이벤트 핸들러
const onSubmit = () => {
const validations = [validateMobileNumber(mobileNumber), ...];
if (validations.some((valid) => !valid)) {
return;
}
// Submit Form ...
//
}
return (
...
<input onChange={handleMobileNumberChange} value={mobileNumber} />
<span className="error">{mobileNumberError}</span>
...
)위의 코드만 보면 딱히 문제는 없습니다. 하지만, 검증해야 할 값들이 더 늘어난다면 어떻게 될까요? 원래는 한 입력에 대해 값과 오류 상태를 관리하기 위해 2개의 React.useState가 필요했으니 만약 3개의 입력을 관리해야 한다면 아래와 같이 관리해야 할 상태와 작성해야 할 함수들이 비례해서 늘어나게 됩니다.
하지만 3개가 아닌 4개, 5개 혹은 그보다 더 늘어난다면 어떨까요? 하나의 폼을 검증하기 위해 작성해야 하는 코드가 너무 많아질 것 같습니다. 게다 가 ‘값이 필수적으로 입력되어 있어야 한다’ 같은 규칙들은 여러 필드가 공통으로 사용할 텐데, 이를 각 필드에서 매번 확인한다면 코드 중복도 높아질 것입니다. 그렇다면 이 문제는 어떻게 해결해야 할까요?
const [mobileNumber, setMobileNumber] = React.useState<string>('');
const [mobileNumberError, setMobileNumberError] = React.useState<string>('');
const [name, setName] = React.useState<string>('');
const [nameError, setNameError] = React.useState<string>('');
const [email, setEmail] = React.useState<string>('');
const [emailError, setEmailError] = React.useState<string>('');
// 검증 함수들
const validateMobileNumber = (value: string): boolean => { /* ... */ }
const validateName = (value: string): boolean => { /* ... */ }
const validateEmail = (value: string): boolean => { /* ... */ }
// input들의 onChange 이벤트 핸들러
const handleMobileNumberChange = (e) => { /* ... */ }
const handleNameChange = (e) => { /* ... */ }
const handleEmailChange = (e) => { /* ... */ }
// form onsubmit 이벤트 핸들러
const onSubmit = () => {
const validations = [
validateMobileNumber(mobileNumber),
validateName(name),
validateEmail(email),
];
if (validations.some((valid) => !valid)) {
return;
}
// Submit Form ...
}
return (
...
<input onChange={handleMobileNumberChange} value={mobileNumber} />
<span className="error">{mobileNumberError}</span>
<input onChange={handleNameChange} value={name} />
<span className="error">{nameError}</span>
<input onChange={handleEmailChange} value={email} />
<span className="error">{emailError}</span>
...
)#조금 더 간결하게
이런 문제를 만난다면 보통은 react-hook-form 같은 라이브러리를 사용하여 해결합니다. 하지만 저희는 조금 다르게 이 문제를 해결해보고 싶었고, 프로젝트에서 이미 사용 중이던 fp-ts를 사용하여 함수형 프로그래밍으로 이 문제를 해결해보기로 하였습니다.
문제를 간결하게 정의하기 위해서는 공통 부분 문제와 그렇지 않은 문제를 파악해야합니다. 앞에서 예로 들었던 이름, 휴대폰 번호, 이메일에 대해 입력 상태 흐름을 일반화하면 아래와 같이 표현할 수 있습니다.
