아주 오랜만에 작성하는 벨로그 글입니다.
미디엄으로 옮겨서 글을 작성하기도 하고, 주로 사내에서 발생한 트러블 슈팅은 노션에 작성하고 있어서 그간 글 작성이 뜸했는데요. 다시 심기 일전해서 제가 겪은 트러블 슈팅을 공유해서 다른 분께 도움을 드리려고 합니다.
이번 글은 사내 개발 환경 모던화의 일환으로 진행하고 있는 antd 라이브러리 버전업에거 겪었던 트러블 슈팅에 대한 글입니다.
아몬즈를 운영하는 비주얼의 프론트엔드 개발팀은 백 오피스 어드민 제품에 UI 디자인 라이브러러리로 antd를 사용하고 있습니다. 어드민 기능 개선은 꾸준히 이루어지고 있었지만, 적절한 시점을 놓쳐 상용 안정화 버전보다 무려 2단계나 낮은 V3버전을 사용하고 있는데요.
"빠르고 쉽고 안정적으로" 기능을 제공받기 위해 사용하는 antd가 현재에 와서는 오히려 최신 버전의 React 패키지로 나아가는데 큰 장애물이 되고 있었습니다.
올해부터 팀에서는 최신 버전의 리액트로 전환하고 더이상 유지보수되지 않거나, 결함이 있는 패키지를 차차 제거하고 있는데요. 기타 작업이 모두 진행된 지금 마지막 장애물로 antd 마이그레이션만 남겨두고 있습니다.
antd v3에서 v4로 전환하는건 몇 가지 컴포넌트에서 큰 변경점을 갖습니다.
아주 간단하게 요약하자면,
고로, 마이그레이션을 진행하게 된다면 위 3가지 컴포넌트를 변경점이 아주 큰 난관이 됩니다.
우선 HOC 패턴을 사용해 하위 컴포넌트까지 전달하는 form props를 전달하는 방식이 바뀌었는데요,
import { Form } form 'antd-v4';
const { form } = Form.useForm();
위 처럼 훅을 불러내 하위 컴포넌트로 전달할 form 객체를 생성하고 <Form />
컴포넌트로 래핑된 하위 컴포넌트들은 <Form.Item/>
컴포넌트로 래핑을 해주어야 합니다.
이전 버전에서는 단지 HOC Form.create로 하위 컴포넌트를 묶기만해도 form 객체에 접근하고, 내부 상태를 이용가능했는데요. 변경된 버전에서는 반드시 최상위 컴포넌트를 Form으로 묶어주어야 form.getFieldsValue 등 인스턴스의 메소드에서 올바른 값을 얻을 수 있습니다. 만일 Form이 제대로 묶여있지 않으면 인스턴스에서 아무것도 조회되지 않기 때문에 동작이 제대로 되지 않습니다.
특정 폼 페이지의 유효성 검증과 버튼 등에 사용할 변수를 관리하는 커스텀 훅이 작성되어있다고 가정해봅시다.
const useCusotmForm = (form) => {
const name = form.getFieldsValue({'id', form});
const password = form.getFieldValue({'password',, form});
const isValidName = !!name.length && !form.getFieldError('name');
const isValidPassword = !!password.length && !form.getFieldError('password');
return {
isValidName,
isValidPassword,
}
}
V3를 사용하는 form이 위 같은 커스텀 훅을 사용해서 특정 필드에 대한 값이 있는지를 체크하고, 필드의 에러가 있는지 검증하는 로직을 갖추고 있었는데요,
V4로 전환한 이후에는 해당 값들을 getFieldsValue
나 getFieldValue
로 조회하면 리랜더링을 일으키지 않는 한 조회가 되지 않는 문제가 있었습니다. 하위 컴포넌트는 <Form.Item/>
으로 묶여있고, rules
와 name
필드가 모두 제대로 전달되고 있음을 확인해도, 값이 조회되지 않는 문제였는데요.
만일 onChange나 onBlur 이벤트 발생할때마다 필드의 유효성 검증을 해야한다면 form.getFieldValue 대신 Form.useWatch를 사용해야 합니다.
더불어 Form.Item으로 래핑한 컴포넌트에 rules를 전달할때도,
<Form.Item
label={label}
name={name}
rules={rules}
validateTrigger={['onBlur', 'onChange']} // <- 트리거 조건을 전달해야합니다.
>
validateTrigger
옵션을 사용해 트리거 조건을 설정해야 최상단에서 Form
의 값의 변화를 관측할 수 있습니다.
form.getFieldError
의 경우, 비동기적으로 발생하는 값의 변화를 아쉽게도 알아낼 수 있는 방법이 없는데요, 훅에서 해당 값에 의존하는 로직이 있다면, 이를 제거하고 form.submit
시 발생하는 함수 내부에서 form.valiadateFileds
메서드를 통해 폼의 유효성 검증이 모두 완료되었음을 검증해야 합니다.
더 이상 커스텀 훅 내부에서 특정 필드를 관측하거나 해당 필드의 에러를 알아내려고 시도하지 마세요! 에러는 Input이나 Select를 래핑하고 있는 Form.Items 컴포넌트에 전달한 rules로 유효성 검증을 하고, 최종적인 폼의 검증은 onFinish 함수에서 이뤄지게 해야 합니다.
v3 버전에서는 onFinish
함수 내부에서 try ... catch
블록을 구성하거나 then
체이닝을 통해 에러를 핸들링 했는데요
// antd v3
const Demo = ({ form: { getFieldDecorator, validateFields } }) => {
const onSubmit = e => {
e.preventDefault();
validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
return (
<Form onSubmit={onSubmit}>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
);
};
const WrappedDemo = Form.create()(Demo);
V4 버전에서는 onFinish
함수와 onFinishFailed
함수가 분리되었습니다.
const Demo = () => {
const onFinish = values => {
console.log('Received values of form: ', values);
};
const onFinishFailed = ({ errorFields }) => {
form.scrollToField(errorFields[0].name);
};
return (
<Form onFinish={onFinish} onFinishFailed={onFinishFailed}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
};
// antd v3
validateFields((err, value) => {
if (!err) {
// Do something with value
}
});
To
// antd v4
validateFields().then(values => {
// Do something with value
});
v3에서는 onSumit
함수 내부에서 각 폼의 유효성을 검증하고, 콜백함수를 통해 에러핸들링을 했다면, 지금은 성공처리와 실패처리 역할을 하는 각 함수를 통해 보다 유연하게 코드를 작성할 수 있습니다. (가독성도 챙기구요)
더불어, 만약 <Form.Item />
에 전달하는 rules
에 validator
함수를 포함해 전달하고 있다면, 이 validator
는 반드시 Promise
를 반환하는 함수로 작성이 되어 있어야 합니다.
rules={
[formRequiredRules(true, INVALID_REQUIRED_PASSWORD_MESSAGE, false),
{ validator: validateToConfirmNewPassword },
{ pattern: new RegExp(REX_PASSWORD), message: INVALID_MINIMUM_EIGHT_WORD_ENG_NUM_MESSAGE }]
}
이 부분이 아주 절 골탕먹인 부분이었는데요,
v3를 기준으로 작성된 코드는 프로미스를 리턴하도록 작성하지 않았어도 큰 문제가 없었겠지만,
v4에서는 전달된 validator
가 프로미스를 리턴하지 않는다면 폼의 유효성 검증이 어디선가 블락이 되버리고,
onFinish
함수가 콜 되지 않습니다. 대신 onFinishFailed
에서는 아무런 에러가 발생하지 않아 errorFields가 빈 배열로 관측됨에도 계속 이 함수가 콜되죠.
개인적으로 아주 불편한 점이라고 생각합니다. 디버그도 어렵고요. 다시 강조해서 만일 validator로 전달하는 함수가 프로미스를 리턴하지 않도록 작성되어 있다면 아래의 예시처럼 바꾸어주세요
const validateToConfirmNewPassword = (_, value) => {
if (value) {
return Promise.resolve();
} else {
return Promise.reject(new Error('비밀번호가 일치하지 않습니다.'));
}
};
아직 현재 진행 중이지만, antd Form
을 마이그레이션 한다면, 위 4가지 사항 정도를 필히 숙지하시는게 좋습니다. antd
를 버전업하는 작업을 진행하고는 있지만, 전 개인적으로 react-hooks-form
과 formik
을 조합해 폼 컴포넌트를 구성하는게 훨씬 DX가 좋다고 느껴지네요.
antd
패키지의 4 버전도 현재는 레거시로 취급받고 있고, 향후 개발환경을 고려한다면 V5로 빌드업을 또 진행해야겠지만, MVP 레벨이 아닌 프로덕션을 운영 중인 회사의 제품에서 굳이 antd
패키지 사용을 고집할 필요가 있을까? 라는 생각이 드는 것도 사실입니다.
동료를 통해 알게 된 사실이지만, v5에서는 공식문서에서 컴포넌트를 래핑해 커스텀한 디자인을 입히지 말라고 명시되어 있다고도 하구요.
이 패키지에 대한 인기도 전과 같지 않고 shadn ui같은 훨씬 좋은 ui 라이브러리들이 존재하기도 하구요.
누군가에게 이 글이 필히 도움이 되었으면 좋겠습니다.
추가적으로 질문 생기시면 댓글로 남겨주시거나 이메일을 주셔도 좋습니다.
감사합니다.