리액트로 폼을 제출하는 컴포넌트를 구성해야 하는 상황이었다. CDD(Component-Drived Development)를 적용하면서 스토리북에 작은 컴포넌트를 작성하고 점점 큰 컴포넌트를 작성하는 방식으로 개발을 진행했다.
위와 같은 제출 폼에서 각 input 들을 컴포넌트로 분리해서 재사용할 수 있는 상황이다. 그래서 Input
이라는 컴포넌트를 만들었다.
그런데 제출 폼의 각 입력값들을 보면 카드번호는 4자리 숫자가 들어와야 하고 보안코드는 3자리 숫자가 들어와야 하는 등의 유효성 검사 로직이 Input
컴포넌트와 결부돼야 한다는 사실을 알 수 있다.
Input
컴포넌트가 유연하게 validator 로직을 prop으로 받아와서 들어온 로직에 대해서만 실행하면 확장성있고 유연하게 유효성 검사를 할 수 있다는 생각으로 처음에는 유효성 검사 로직 함수를 갖고 있는 객체를 props로 넘겨주었다.
AddCardForm.jsx
function AddCardForm({ updateCard, addCard }) { ... return ( ... <Input length={MAX_LENGTH.CARD_NUMBER} value={cardForm.firstCardNumber} name="firstCardNumber" updateCardForm={updateCardForm} validators={{ isOverMaxLength, isNaN: Number.isNaN }} /> ... <Input placeholder="MM" length={MAX_LENGTH.DATE} minLength={MIN_LENGTH.MONTH} min={RANGE.MONTH_MIN} max={RANGE.MONTH_MAX} value={cardForm.expireMonth} name="expireMonth" updateCardForm={updateCardForm} validators={{ isOverMaxLength, isNaN: Number.isNaN, isOutOfRange }} validators={{ checkMaxLength, checkIsNaN, checkRange }} /> ... ); ... }
위와 같은 형태로
Input
컴포넌트를 사용할 때 해당Input
값의 유효성 검사 로직을validators
라는 이름의 prop으로 객체를 넘겨준다. 그렇다면Input
컴포넌트에서는 해당 로직을 어떻게 사용할 수 있을까?
Input.jsx
function Input({ ...(생략), validators }) { const checkValidation = (event, targetValue) => { if (validators.isNaN && Number.isNaN(+targetValue)) { event.target.value = targetValue.substring(0, value.length - 1); throw new Error(ERROR_MESSAGE.NOT_NUMBER); } if (validators.isOverMaxLength && validators.isOverMaxLength(targetValue, length)) { event.target.value = targetValue.substring(0, length); throw new Error(ERROR_MESSAGE.OVER_MAX_LENGTH); } if (validators.isOutOfRange && validators.isOutOfRange(min, max, +targetValue)) { event.target.value = targetValue.substring(0, targetValue.length - 1); throw new Error(ERROR_MESSAGE.INVALID_MONTH_RANGE); } }; ... }
Input
컴포넌트에서는validators
객체를 prop으로 받아오고checkValidation
함수에서는 해당 함수가 객체에 있는지 하드코딩으로 확인하고 있으면 검사하고 없으면 넘어가는 형태이다.
이런 식으로 구현을 했을 때 카드 제출 폼이 아닌 다른 성격의 입력값을 받을 때 또 다른 유효성 검사로직이 생기는데 그러면 그렇게 늘어나는 유효성 검사로직에 따라 Input
컴포넌트에서 늘어난 만큼 그 함수들이 있는지 없는지를 하드코딩으로 체크하면서 유효성을 검사하게 될 것이다.
이렇게 되면 Input
컴포넌트는 validator와 매우 강한 결속상태를 가진다고 할 수 있다. 이렇게 하드코딩이 연속되면 유지보수가 어렵고 버그가 발생하기 쉬운 문제점이 있다.
Input
컴포넌트와 Validator의 종속성을 완화하기 위해서는 Input
컴포넌트를 사용하는 측에서는 필요한 유효성 검사 로직들을 똑같이 넘겨주고 Input
컴포넌트에서는 해당 로직이 있는지 없는지를 하드코딩으로 일일이 검사하는 것이 아니라 그냥 넘어온 모든 validator 들을 전부 실행만 시켜주는 구조로 바꾸면 된다.
이를 실현하기 위해서는 각 유효성 검사 함수에 들어올 매개변수와 함수를 한 번에 받아야 한다.
그래서 아래와 같은 Validator 규격을 만들어서 사용했다.
export const validator = (validate, ...args) => {
return {
validate: () => validate(...args),
};
};
위 함수를 뜯어보면 첫 번째 인자로 들어오는 validate
는 checkMaxLength
와 같은 유효성 검사 함수들 중 하나이다. 그리고 ...args
에는 validate
함수에 들어갈 인자들을 받아온다.
이제 Input
컴포넌트를 사용하는 측에서 아래와 같이 사용할 수 있다.
<Input
size="w-25"
type="password"
length={MAX_LENGTH.SECURITY_CODE}
value={value}
name={name}
updateCard={updateCard}
validators={[
validator(checkMaxLength, value, MAX_LENGTH.SECURITY_CODE),
validator(checkIsNaN, value),
]}
/>
validators
라는 props에 유효성 검사 로직 함수들의 배열을 담아서 Input
컴포넌트에 넘겨준다. 그리고 해당 함수에서 필요한 인자들 또한 사용하는 측에서 넘겨준다. 그러면 해당 인자들이 첫 번째 인자로 들어간 함수의 인자로 들어가서 실행될 것이다.
이제 Input
컴포넌트에서는 props로 받은 validators
를 아래와 같이 실행만 시켜주면 된다.
function Input({ ...(생략), validators }) {
...
const checkValidation = () => {
validators.forEach((validator) => {
validator.validate();
});
};
...
}
아래와 같이 코드를 수정함으로써 validators
가 어떤 값이 들어오는지 Input
컴포넌트는 일일이 알아야 할 필요가 사라졌다. 어떤 값이 들어오든 해당 Input
컴포넌트가 수행해야할 유효성 검사 로직을 실행만 시킬 뿐이다.
Input
컴포넌트를 사용하는 측에서 어떤 유효성 검사를 할 지 정하고 필요한 인자들까지 같이 넘겨주기 때문에 Input
컴포넌트와 validator의 강한 결합상태가 완화되었다.
이와 같이 독립적인 컴포넌트는 외부의 어떤 상황에서도 독립적으로 유연하고 확장성있는 모습을 갖춰야 한다는 것을 느끼는 리팩토링 과정이었다.