2025.01.19 - [React] - 디자인 패턴을 활용한 Form 컴포넌트 리팩터링: 유연성과 재사용성 모두 확보하기(1)
이전 글에서 form을 리팩터링하게 된 계기와 리팩터링하면서 했던 고민들을 작성했으니,
이번 글에서는 해결 과정과 방법을 작성하고자 한다.
(보안 및 저작권 보호를 위해, 특정 회사의 내부 코드나 기밀 정보를 제외하였으며,
일반적인 기술 개념과 패턴을 기반으로 서술하였습니다.)
이전 글 요약
기획 고도화로 form 구조가 복잡해졌고, 기존의 `<Form />` 컴포넌트는 초기 기획 기반이여서 과도하게 추상화되어 있어 유지보수가 어려웠다.
이에, 리팩터링 하기로 하였고, 여러 가지 방법을 고려 후, 아래의 방법을 선택했다.
이제, 리팩터링을 어떻게 진행했는지와 디자인 패턴을 어디에서 활용했는지를 설명하겠다.
1. `<Form />` 공통 컴포넌트 구현
- `React-Hook-Form` 과 `Radix-Ui` 사용을 기본으로 하여 `<Form />`컴포넌트를 구현
- Compound components 패턴을 사용하여 구현
1) `<Form.Root />`
- FormProvider로 감싸서 form context를 공유할 수 있도록 함
- 복잡한 구조를 고려하였을 때 context를 공유하도록 하는 것이 유리하다고 판단함
// <Form.Root />
// Radix-ui의 FormRoot을 React-Hook-Form의 FormProvider로 Wrapping함
import { Root as FormRoot } from '@radix-ui/react-form';
import { type FormEventHandler, type ReactNode } from 'react';
import { type FieldValues, FormProvider, type UseFormReturn } from 'react-hook-form';
type FormProps<T extends FieldValues> = UseFormReturn<T> & {
children: ReactNode;
onSubmit?: FormEventHandler;
};
export const Root = <T extends FieldValues>({ children, onSubmit, ...methods }: FormProps<T>) => {
return (
<FormProvider {...methods}>
<FormRoot onSubmit={onSubmit ?? (data => console.log('Form Submit data: ', data))} noValidate>
{children}
</FormRoot>
</FormProvider>
);
};
2) 기타 반복적인 부분을 compound component 패턴으로 구현
- 반복적으로 사용되긴 하지만, 기획에 따라 사용 여부와 위치 등이 유동적이었기 때문에 compound component 패턴을 사용함
2. `<Form />` 컴포넌트 개선 및 수정
1) Compound Component 패턴의 단점 보완
- Radix-UI 사용하며 코드 라인이 지나치게 길어지는 단점을 완화하기 위해 반복되는 부분을 공통화하여 래핑함
- 예: Radix-UI의 Field를 수정하여 사용
// Radix-Ui의 Field 컴포넌트를 Wrapping하여 공통 기능을 추가하는 예시
import * as Form from '@radix-ui/react-form';
import type { PropsWithChildren } from 'react';
type FormFieldProps = {
//...
};
export const Field = ({
children, // 이때 children으로 input이 전달된다.
// ...관련 props
}: PropsWithChildren<FormFieldProps>) => {
return (
<div>
{/* 기타 반복되는 요소들 /*}
<Form.Field {...props} >
<div>
{/* 기타 반복되는 요소들 /*}
<Form.Label {...props} >
{label}
</Form.Label>
{/* 기타 반복되는 요소들 /*}
</div>
<Form.Control asChild>{children}</Form.Control>
</Form.Field>
{/* 기타 반복되는 요소들 /*}
</div>
);
};
적용 결과
위의 내용들을 적용하면, 아래의 코드와 같이 나타낼 수 있다.
Form.Field의 children으로 필요한 입력 필드를 전달하면 원하는대로 사용할 수 있어서, 더 직관적으로 입력 필드들을 구성할 수 있다.
import { Form, InputComponentExample } from '@/components';
import { useForm } from 'react-hook-form';
import { type FormType, schema, defaultValues } from '.';
const ExamplePage = () => {
const methods = useForm({ resolver: zodResolver(schema), defaultValues });
const onSubmit = () => {...} // submit 성공시 호출할 함수
const { handleSubmit, register } = methods
return (
<div>
{/* ... */}
<Form.Root<FormType> onSubmit={handleSubmit(onSubmit)} {...mehtods}>
<Form.Field name="email">
<input type='text' {...register("email")}/>
</Form.Field>
<Form.Field name="name">
<InputComponentExample {...register("name")}/>
</Form.Field>
{/* ... */}
</Form.Root>
{/* ... */}
</div>
)
}
복잡한 구조의 Input 컴포넌트에 디자인 패턴 적용하기
1) Input을 컴포넌트화한 이유
위의 결과 코드에서 입력 필드를 `<Form.Field></Form.Field>`의 `chidren`으로 주입하여 사용하고 있다.
이때 단순한 HTML `<input>` 태그를 사용할수도 있고, 컴포넌트로 만들어서 사용할수도 있는데, 컴포넌트로 만드는 방법을 선택했다.
그 이유는 다음과 같다.
- 스타일 적용의 일관성 유지 - 프로젝트의 디자인 시스템을 쉽게 적용하고 유지보수할 수 있다.
- 기획 요구사항 반영 용이 - 특정 필드에 대한 입력 제한(숫자 입력, 포맷팅, 자동완성 등), 유효성 검사 규칙, 기타 기획 요구 사항 등이 변경된다면, 이를 공통 컴포넌트에서 관리할 경우 유지보수가 쉬워진다.
- 중복 코드 감소 - 반복되는 속성이나 이벤트 핸들러, React-Hook-Form 관련 로직 등을 캡슐화하여 더 간결한 코드를 작성할 수 있다.
2) 복잡한 Input 구조에는 디자인 패턴 적용하기
진짜 문제는 복잡한 구조의 입력필드를 구현하는것이었다.
아코디언 내부에 입력 필드가 다수 존재한다거나, 특정 조건에 따라 input 타입이 변경되는 등의 다양하고 복잡한 구조가 존재했다. 이런 경우 디자인 패턴을 적용해서 해결했다. 그중 일부를 소개하겠다.
Compound component 패턴과 Render props 패턴을 결합하여 사용
- Compound component 패턴
- 리액트의 Context/Provider를 사용하여 여러 종류의 컴포넌트가 하나의 로직을 공유할 수 있게 하는 방법
- 선택의 이유: 중첩되거나 여러 컴포넌트가 포함된 복잡한 구조의 input을 만들때 내부 컴포넌트들의 재사용성을 높이고, 로직을 캡슐화하여 사용하는 곳에서의 가독성을 확보하고, 유연하게 사용하기 위함
- Render props 패턴
- 컴포넌트를 재사용 가능하게 할 수 있는 또 다른 방법
- 컴포넌트의 prop으로 함수이며 JSX 엘리먼트를 리턴한다. 컴포넌트 자체는 아무런 것도 렌더링하지 않지만 render prop함수를 호출함
- 컴포넌트간 데이터 공유가 간단함
- 선택의 이유: 간혹 input이 추가/삭제가 되는 경우 index등이 컴포넌트간에 공유되어야 했는데, 커스텀 훅은 사용할 수 없는 상황이어서 사용함
사용 예
(보안상의 이유로 사용하는 곳에서의 예제만 작성했다.)
// Form 컴포넌트 내부에 ComplexInput 을 사용할 때
<Form.Root onSubmit={handleSubmit(onSubmit)} {...methods}>
<ComplexField
// 관련 props...
renderItem={(param, param) => {
return (
<ComplexField.InnerWrapper>
{/* 다른 입력 필드들 */}
<InnerComplexField>
<InnerComplexField.Content
style={{ container: 'flex flex-col gap-4' }}
renderItem={param => {
// 로직 코드
return <InputCompenents props={param} />;
}}
/>
<InnerComplexField.SomeActionButton someFunc={someFunc}>
some action
</InnerComplexField.SomeActionButton>
</InnerComplexField>
{/* 다른 입력 필드들 */}
</ComplexField.InnerWrapper>
);
}}
/>
</Form.Root>
결과 및 효과
1. 코드의 가독성과 유지보수성 향상/기획 대응 용이
- 디자인 패턴을 적용하여 역할을 분리함으로써 구조가 더 명확해지고, 새로운 요구사항을 유연하게 반영할 수 있게 되었다.
2. 중복 코드 감소 및 재사용성 증가
- 반복되는 속성이나 로직을 공통 컴포넌트로 관리하여, 새로운 입력 필드가 추가될 때도 최소한의 코드 수정만으로 적용할 수 있었다.
3. UI라이브러리 및 React-Hook-Form과의 결합 최적화
- Radix-UI의 Form.Field를 래핑하여 공통 기능을 추가함으로써, UI 프레임워크의 장점은 유지하면서도 불필요한 중복을 최소화할 수 있었다.
- react-hook-form의 FormProvider를 활용하여 폼 상태를 Context로 공유하면서, 다양한 컴포넌트에서 손쉽게 접근 및 활용할 수 있도록 개선했다.
결론: 유연성과 재사용성을 모두 확보한 Form 컴포넌트
리팩터링을 통해 폼 컴포넌트의 유연성과 재사용성을 대폭 향상시킬 수 있었다.
기존보다 코드가 더 간결해지고, 유지보수가 쉬워졌으며, 새로운 요구사항을 쉽게 반영할 수 있는 구조로 발전했다.
리팩터링 이후 개발 생산성이 매우 향상되어 훨씬 효율적인 개발이 가능해졌고, 기획에서 변경을 요청해도 두렵지가 않았다. 이번 기회를 통해 설계의 중요성을 깨달을 수 있었다.
앞으로도 문제를 잘 발견하고, 지속적으로 개선해서, 제품도 최적화하고 개발자의 생산성도 최적화할 수 있는 개발자가 되도록 노력해야겠다.
Ref
1. https://www.react-hook-form.com/
2. https://www.patterns.dev/react/compound-pattern
4. https://fe-developers.kakaoent.com/2022/221110-ioc-pattern/
5. https://react.dev/reference/react/cloneElement#passing-data-with-a-render-prop
6. https://react.dev/reference/react/cloneElement#passing-data-through-context
7. https://fe-developers.kakaoent.com/2022/220731-composition-component/
'React' 카테고리의 다른 글
디자인 패턴을 활용한 Form 컴포넌트 리팩터링: 유연성과 재사용성 모두 확보하기(1) (0) | 2025.01.19 |
---|---|
i18next-react와 Google Sheets를 활용한 국제화(i18n) 자동화 도입기(2) - i18next적용하기(namespace 구분하기) (3) | 2024.10.27 |
i18next-react와 Google Sheets를 활용한 국제화(i18n) 자동화 도입기(1) - intro/도구 선정 (4) | 2024.09.18 |
[원티드 챌린지] 리액트 테스트와 최적화 (0) | 2023.07.26 |
댓글