1. TextField
TextField는 사용자의 입력을 받고 입력값에 따라 에러가 뜨거나, 입력이 완료되었다는 표시를 하는 공통 컴포넌트이다. 컴포넌트 특성상 안에 있는 사용자의 입력값에 따라 스타일이 달라진다.
- 평상시 :
default - 입력값에 validation에 충족하지 않을 때 :
warning
- 그 외에도 여러가지 케이스가존재한다.
- 기본값:
default
- 기본값:
- focus되었을 때 :
focused - 조건에 충족했을 때 :
filled - 읽기 전용 :
readonly
상황에 따라 label, caption이 추가되기도 한다. 다양한 상황이 존재한다. 이 상황 처리에 대한 로직을 어디에 넣는게 맞을까? 현재 우리 프로젝트는 react-hook-form을 활용하고 있다.
첫 번째 방법은, useForm 혹은 useFormContext에서 받은 props를 통째로 TextField에 넘겨주고, TextField내부에서 status에 따라 error처리, success처리를 하는 것이다. 모든 책임을 TextField컴포넌트에 넘겨주는 것이다.TextField외부에서는 register와 hookForm을 넘겨주면 되는 것이다. 그 이후에는 TextField안에서 처리하는 것이다. 밖에서는 안에서 무슨 일이 일어나는 지 알지 않아도 된다. 완전 추상화라 할 수 있다.
위와 같은 생각을 가지고 코드를 작성해보았다.
`TextField.tsx`
'use client';
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
import cn from '@/utils/cn';
import clsx from 'clsx';
import Image from 'next/image';
import { useRef, useState } from 'react';
import { UseFormReturn } from 'react-hook-form';
interface TextFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
hookForm: UseFormReturn<any>;
register: any;
placeholder?: string;
label?: string;
leftCaptionText?: string;
inputLeftIcon?: React.ReactNode;
maxCount?: number;
timer?: number;
}
const formatTimer = (timer: number) => {
const minutes = Math.floor(timer / 60);
const seconds = timer % 60;
return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
};
export default function TextField({
hookForm,
register,
placeholder,
label,
leftCaptionText,
inputLeftIcon,
maxCount,
timer,
...props
}: TextFieldProps) {
const [isFocus, setIsFocus] = useState(false);
const textFieldRef = useRef<HTMLDivElement>(null);
const inputName = register.name;
useOnClickOutside(textFieldRef, () => {
setIsFocus(false);
});
const { formState, getFieldState, watch, setValue } = hookForm;
const isSuccess = formState.isValid;
const errorMessage = formState.errors[inputName]?.message;
const isDirty = getFieldState(inputName).isDirty;
const inputRightIcon = errorMessage ? 'warning' : isDirty ? 'backspace' : '';
let rightCaptionText;
if (maxCount) {
rightCaptionText = `${watch(inputName).length}/${maxCount}`;
} else if (timer) {
rightCaptionText = formatTimer(timer);
}
return (
<section ref={textFieldRef}>
<section
className={cn('w-full rounded-8 border-1 p-16', {
'border-border-pressed bg-white': isFocus,
'border-transparent bg-sub': !isFocus,
'border-success-cto bg-brand-color': isSuccess,
'border-warning bg-warning-color': !!errorMessage,
})}
>
{label && <label className="mb-2 text-caption text-sign-tertiary">{label}</label>}
<div className="relative flex h-24 w-full items-center justify-around">
{inputLeftIcon}
<input
className={cn(
'h-full w-full text-paragraph-1 outline-none placeholder:text-paragraph-1',
{
'bg-white': isFocus,
'bg-sub': !isFocus,
'bg-brand-color': isSuccess,
'bg-warning-color': !!errorMessage,
}
)}
onFocus={(e) => {
setIsFocus(true);
}}
placeholder={placeholder}
{...register}
{...props}
/>
{inputRightIcon && (
<Image
src={`/icons/24/${inputRightIcon}.svg`}
width={24}
height={24}
alt={inputRightIcon}
onClick={() => setValue(inputName, '')}
/>
)}
</div>
</section>
<section className={cn('flex justify-between px-8 pt-4 text-caption')}>
<LeftCaption text={leftCaptionText} isError={!!errorMessage} />
<RightCaption text={rightCaptionText} />
</section>
</section>
);
}
interface LeftCaptionProps {
text?: string;
isError?: boolean;
}
function LeftCaption({ text, isError }: LeftCaptionProps) {
return <span className={clsx(isError && 'text-warning')}>{text}</span>;
}
interface RightCaptionProps {
text?: string;
}
function RightCaption({ text }: RightCaptionProps) {
return <span>{text}</span>;
}
props로 넘겨주는 값이 많아서 내부에서 처리하는 로직이 밖에서는 보이지 않는다. 이제 정말 좋은 코드일까?
2. 코드 리뷰 바탕으로 코드 수정
1. 1차 피드백 : 내부 처리로 인한 복잡함
프론트엔드 팀원인 주혁님이 ButtonGroup과 Button을 구분하여 구현한 코드이다.
위 방법처럼 TextFieldGroup으로 TextField를 감싸면 어떨까? 그렇다면, TextFieldGroup 내부에 TextFieldLabel, TextFieldCaption 각각 컴포넌트를 직접 넣는 것이다. 이 방법이 좋을까? 혹은 props로 label, caption등을 넘겨줘서 내부에서 처리하는 것이 좋을까?
지금까지는 나는 후자와 같은 방식으로 구현을 하였다. 이러한 방식으로 구현을 하니 한 컴포넌트가 상당히 많은 역할을 해서 무거워지고, 코드가 상당히 난잡해졌다. 전자와 같은 방식으로 구현을 하면 이러한 코드의 복잡함이 어느정도 해소가 될 것이다.
이러한 방식으로 TextFieldGroup 구성을 하였다.
- captionLeft
- errorMessage
- 안내 메시지
- captionLeft에는 props로 넘겨준 안내 메시지가 보일 수도 있고, error가 있는 경우 errorMessage가 보일 수도 있다.
- captionRight
- 글자수
- 남은 시간
- label
- leftNode
- rightNode
과연, TextField를 사용하는 곳에서 TextFieldLabel, TextFieldCaption을 추가하는 것이 더 좋은 코드일까? 위와 같은 방식으로 구현할 경우 caption내의 메시지는 error인 경우와 아닌 경우에 따라 내용과 색이 다르다. 이러한 처리를 바깥에서 일부 해야한다. 이러한 핸들링은 TextField내부에서 처리하는 것이 가장 좋은 추상화 아닐까.
TextFiled내부에서 에러 핸들링하는 방식으로 코드를 작성하였다.
`TextField.tsx`
'use client';
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
import { StrictPropsWithChildren } from '@/types';
import cn from '@/utils/cn';
import clsx from 'clsx';
import Image from 'next/image';
import { useRef, useState } from 'react';
import type { UseFormReturn } from 'react-hook-form';
interface TextFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
hookForm: UseFormReturn<any>;
register: any;
label?: string;
leftCaptionText?: string;
inputLeftIcon?: React.ReactNode;
maxCount?: number;
timer?: number;
}
const formatTimer = (timer: number) => {
const minutes = Math.floor(timer / 60);
const seconds = timer % 60;
return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
};
export default function TextField({
hookForm,
register,
label,
leftCaptionText,
inputLeftIcon,
maxCount,
timer,
...props
}: TextFieldProps) {
const [isFocus, setIsFocus] = useState(false);
const textFieldRef = useRef<HTMLDivElement>(null);
const inputName = register.name;
useOnClickOutside(textFieldRef, () => {
setIsFocus(false);
});
const { formState, watch, setValue } = hookForm;
const isSuccess = formState.isValid;
const errorMessage = formState.errors[inputName]?.message;
const inputRightIconName = errorMessage
? 'warning'
: watch(inputName).length > 0
? 'backspace'
: '';
let rightCaptionText;
if (maxCount) {
rightCaptionText = `${watch(inputName).length}/${maxCount}`;
} else if (timer) {
rightCaptionText = formatTimer(timer);
}
return (
<section ref={textFieldRef}>
<section
className={cn('w-full rounded-8 border-1 p-16', {
'border-border-pressed bg-white': isFocus,
'border-transparent bg-sub': !isFocus,
'border-success-cto bg-brand-color': isSuccess,
'border-warning bg-warning-color': !!errorMessage,
})}
>
<Label>{label}</Label>
<div className="relative flex h-24 w-full items-center justify-around">
{inputLeftIcon}
<input
className={cn(
'h-full w-full text-paragraph-1 outline-none placeholder:text-paragraph-1',
{
'bg-white': isFocus,
'bg-sub': !isFocus,
'bg-brand-color': isSuccess,
'bg-warning-color': !!errorMessage,
}
)}
onFocus={() => {
setIsFocus(true);
}}
{...register}
{...props}
/>
<InputRightIcon
name={inputRightIconName}
onClick={() => inputRightIconName === 'backspace' && setValue(inputName, '')}
/>
</div>
</section>
<section className={cn('flex justify-between px-8 pt-4 text-caption')}>
<LeftCaption isError={!!errorMessage}>{leftCaptionText}</LeftCaption>
<RightCaption>{rightCaptionText}</RightCaption>
</section>
</section>
);
}
interface InputRightIconProps {
name: string;
onClick: () => void;
}
function InputRightIcon({ name, onClick }: InputRightIconProps) {
if (!name) return;
return (
<Image src={`/icons/24/${name}.svg`} width={24} height={24} alt={name} onClick={onClick} />
);
}
function Label({ children }: StrictPropsWithChildren) {
if (!children) return;
return <label className="mb-2 text-caption text-sign-tertiary">{children}</label>;
}
interface LeftCaptionProps {
isError?: boolean;
}
function LeftCaption({ children, isError }: StrictPropsWithChildren<LeftCaptionProps>) {
if (!children) return;
return <span className={clsx(isError && 'text-warning')}>{children}</span>;
}
function RightCaption({ children }: StrictPropsWithChildren) {
if (!children) return;
return <span>{children}</span>;
}
Label, LeftCaption, RightCaption 등 컴포넌트를 분리하여 최대한 선언적으로 작성하려 하였다.
2. 2차 피드백 : 구조 개선
주혁님으로부터 react-hook-form라이브러리에 공통 컴포넌트가 너무 의존하고 있다는 피드백을 받았다. 지금 내 TextField는 react-hook-form에 의존적이다. 분명히 TextField라는 공통 컴포넌트임에도 에러 상황에 대한 로직, caption에 대한 로직, TextField에 대한 로직 모든 로직이 섞여 있다. props들도 성격이 모두 재각각이다. 이를 분리하면 어떨까?
TextField에 대한 로직을 TextField에 그대로 두고, 에러 상황에 대한 로직과 caption에 대한 로직 등 react-hook-form으로 다루는 로직을 TextFieldController라는 컴포넌트로 빼는 것이다. 그렇다면 TextFieldController가 TextFiled를 감싸는 구조가 될 것이다.
`TextField.tsx`
'use client';
import { Spacing } from '../common/Spacing';
import cn from '@/utils/cn';
import { forwardRef } from 'react';
import type { StrictPropsWithChildren } from '@/types';
import type { UseFormRegisterReturn } from 'react-hook-form';
export interface TextFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
leftCaption?: string;
rightCaption?: string;
leftInputIcon?: React.ReactNode;
rightInputIcon?: React.ReactNode;
isFocus?: boolean;
isSuccess?: boolean;
isLeftError?: boolean;
isRightError?: boolean;
register?: UseFormRegisterReturn<string>;
}
export default forwardRef(function TextField(
{
label,
leftCaption,
rightCaption,
leftInputIcon,
rightInputIcon,
isFocus = false,
isLeftError = false,
isRightError = false,
register,
...props
}: TextFieldProps,
textFieldRef: React.ForwardedRef<HTMLDivElement>
) {
const isError = isLeftError || isRightError;
return (
<div>
<section
className={cn('w-full rounded-8 border-1 p-16', {
'border-border-pressed bg-white': isFocus,
'border-transparent bg-sub': !isFocus,
'border-warning bg-warning-color': isError,
})}
>
<Label>{label}</Label>
<Spacing size={2} />
<div className="relative flex h-24 w-full items-center justify-around">
{leftInputIcon}
<input
className={cn(
'h-full w-full text-paragraph-1 outline-none placeholder:text-paragraph-1',
{
'bg-white': isFocus,
'bg-sub': !isFocus,
'bg-warning-color': isError,
}
)}
{...register}
{...props}
/>
{rightInputIcon}
</div>
</section>
<section className="flex justify-between px-8 pt-4 text-caption text-sign-tertiary">
<LeftCaption isError={isLeftError}>{leftCaption}</LeftCaption>
<RightCaption isError={isRightError}>{rightCaption}</RightCaption>
</section>
</div>
);
});
function Label({ children }: StrictPropsWithChildren) {
if (!children) return;
return <label className="text-caption text-sign-tertiary">{children}</label>;
}
interface LeftCaptionProps {
isError?: boolean;
}
function LeftCaption({ isError, children }: StrictPropsWithChildren<LeftCaptionProps>) {
if (!children) return;
return <span className={isError ? 'text-warning' : ''}>{children}</span>;
}
interface RightCaptionProps {
isError?: boolean;
}
function RightCaption({ isError, children }: StrictPropsWithChildren<RightCaptionProps>) {
if (!children) return;
return <span className={isError ? 'text-warning' : ''}>{children}</span>;
}
`TextFieldController.tsx`
'use client';
import TextField, { type TextFieldProps } from './TextField.client';
import { useOnClickInside } from '@/hooks/useOnClickInside';
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
import Image from 'next/image';
import { useRef, useState } from 'react';
import type { UseFormRegisterReturn, UseFormReturn } from 'react-hook-form';
interface TextFieldControllerProps extends TextFieldProps {
register: UseFormRegisterReturn<string>;
hookForm: UseFormReturn<any>;
/**
* leftCaption에 문구를 표기하는 경우
*/
caption?: string;
/**
* rightCaption에 글자수를 표기하는 경우
*/
maxCount?: number;
/**
* rightCaption에 타이머를 표기하는 경우
*/
timer?: number;
}
export default function TextFieldController({
register,
hookForm,
caption,
maxCount,
timer,
...TextFieldProps
}: TextFieldControllerProps) {
const textFieldRef = useRef<HTMLDivElement>(null);
const [isFocus, setIsFocus] = useState(false);
const [isUserTouchOutsideOnce, setIsUserTouchOutsideOnce] = useState(false);
const { formState, watch, setValue } = hookForm;
const inputName = register.name;
const errorMessage = formState.errors[inputName]?.message;
const isRightError = maxCount ? watch(inputName).length > maxCount : false;
const isLeftError = isUserTouchOutsideOnce && (!!errorMessage || isRightError);
const isError = isRightError || isLeftError;
const rightInputIconName = isError ? 'warning' : watch(inputName).length > 0 ? 'backspace' : '';
return (
<TextField
leftCaption={caption ?? String(errorMessage) ?? ''}
rightCaption={
maxCount ? `${watch(inputName).length}/${maxCount}` : timer ? `${timer}초 후 재전송` : ''
}
rightInputIcon={
rightInputIconName && (
<Image
src={`/icons/24/${rightInputIconName}.svg`}
width={24}
height={24}
alt={rightInputIconName}
onClick={() => rightInputIconName === 'backspace' && setValue(inputName, '')}
/>
)
}
isFocus={isFocus}
isLeftError={isLeftError}
isRightError={isRightError}
register={register}
{...TextFieldProps}
/>
);
}
3. 고민
focus되었다가 focus가 해제되었을 때, input 조건에 충족하지 않을 경우 빨간색이 되어야 한다. 이를 어떻게 구현해야 할까? input태그에는 focus되었음과 focus가 해제되었음을 감지하는 onFocus, onBlur속성이 있다. input이 focus되었을 때와 focus가 해제되었을 때 input뿐 아니라 input을 둘러싼 영역 또한 색이 변해야 한다. 그러기 위해서는 결국 isFocus라는 상태값을 관리해야한다. 그렇다면 이 상태값에 대한 로직을 TextField에서 관리하는 것이 좋을까, 아니면 TextFieldController에서 하는 것이 좋을까?
어차피 상태값을 관리해야 한다면, TextFieldController에서 하는 것이 좋지 않을까? TextFieldController에서 focus, blur에 대한 처리를 모두 하려면 ref를 자식 컴포넌트인 TextField에 넘겨줘야 한다. input에 직접 넘겨주어 onBlur, onFocus처리를 할 수가 없다. 왜냐하면 우리는 register를 input에 넘겨주어 input을 컨트롤하고 있기 때문이다. react-hook-form은 ref를 기반으로 input을 비제어 컴포넌트로 관리하기 때문에 ref와 register 모두 넘겨주면 정상적으로 동작하지 않는다.
그래서 불가피하게 TextField 컴포넌트 안과 바깥을 클릭했을 때로 focus상태를 감지하였다.
// TextField.tsx
// ..
return (
<div ref={textFieldRef}>
// ..
</div>
);
});
// TextFieldController.tsx
export default function TextFieldController({
register,
hookForm,
caption,
maxCount,
timer,
...TextFieldProps
}: TextFieldControllerProps) {
const textFieldRef = useRef<HTMLDivElement>(null);
const [isFocus, setIsFocus] = useState(false);
const [isUserTouchOutsideOnce, setIsUserTouchOutsideOnce] = useState(false);
useOnClickInside(textFieldRef, () => {
setIsFocus(true);
});
useOnClickOutside(textFieldRef, () => {
setIsUserTouchOutsideOnce(true);
setIsFocus(false);
});
// ..
return (
<TextField
isFocus={isFocus}
register={register}
ref={textFieldRef}
{...TextFieldProps}
/>
);
}
useOnClickInside와 useOnClickOutSide 커스텀 훅을 이용하여 TextField 컴포넌트 안을 클릭했는지, 밖을 클릭했는지 감지하였다. 과연 이 로직이 맞을까? 기획 의도는 분명히 input이 포커스 되었을 때 색이 변하고, 포커스가 해제되었을 때 색이 또 변하는 것이다. 이 코드는 기획 의도를 벗어난 코드이다.
isFocus 상태값을 TextField안으로 넣는 것이 맞다. 왜나하면 ref를 input에 넘기는 것은 react-hook-form의 register로 input을 관리하려면 불가능하기 때문이다.
그래서 onFocus와 onBlur를 TextField안으로 넣었다.
<input
onFocus={() => setIsFocus(true)}
onBlur={() => {
setIsFocus(false);
}}
{...register}
{...props}
/>
이것은 react-hook-form의 mode:onBlur를 사용하면 해결될 문제이다. 아래는 최종 코드이다.
`TextField.tsx`
'use client';
import { Spacing } from '../common/Spacing';
import cn from '@/utils/cn';
import { forwardRef, useState } from 'react';
import type { StrictPropsWithChildren } from '@/types';
import type { UseFormRegisterReturn } from 'react-hook-form';
export interface TextFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
leftCaption?: string;
rightCaption?: string;
leftInputIcon?: React.ReactNode;
rightInputIcon?: React.ReactNode;
isSuccess?: boolean;
isLeftError?: boolean;
isRightError?: boolean;
register?: UseFormRegisterReturn<string>;
isSpacing?: boolean;
}
export default forwardRef(function TextField(
{
label,
leftCaption,
rightCaption,
leftInputIcon,
rightInputIcon,
isLeftError = false,
isRightError = false,
register,
isSpacing = true,
...props
}: TextFieldProps,
textFieldRef: React.ForwardedRef<HTMLLabelElement>
) {
const isError = isLeftError || isRightError;
const [isFocus, setIsFocus] = useState(false);
return (
<label ref={textFieldRef} htmlFor="textField" className="relative">
<section
className={cn('w-full rounded-8 border-1 p-16', {
'border-border-pressed bg-white': isFocus,
'border-transparent bg-sub': !isFocus,
'border-warning bg-warning-color': isError,
})}
>
<Label>{label}</Label>
<Spacing size={2} />
<div className="relative flex h-24 w-full items-center justify-around">
{leftInputIcon}
<input
className={cn(
'h-full w-full text-paragraph-1 outline-none placeholder:text-paragraph-1',
{
'bg-white': isFocus,
'bg-sub': !isFocus,
'bg-warning-color': isError,
}
)}
onFocusCapture={() => {
setIsFocus(true);
}}
onBlurCapture={() => {
setIsFocus(false);
}}
id="textField"
{...register}
{...props}
/>
{rightInputIcon}
</div>
</section>
<section
className={cn(
'flex h-18 w-full justify-between px-8 pt-4 text-caption text-sign-tertiary',
{ absolute: !isSpacing }
)}
>
<LeftCaption isError={isLeftError}>{leftCaption}</LeftCaption>
<RightCaption isError={isRightError}>{rightCaption}</RightCaption>
</section>
</label>
);
});
function Label({ children }: StrictPropsWithChildren) {
if (!children) return;
return (
<label htmlFor="textField" className="block text-caption text-sign-tertiary">
{children}
</label>
);
}
interface LeftCaptionProps {
isError?: boolean;
}
function LeftCaption({ isError, children }: StrictPropsWithChildren<LeftCaptionProps>) {
if (!children) return;
return <span className={isError ? 'text-warning' : ''}>{children}</span>;
}
interface RightCaptionProps {
isError?: boolean;
}
function RightCaption({ isError, children }: StrictPropsWithChildren<RightCaptionProps>) {
if (!children) return;
return <span className={isError ? 'text-warning' : ''}>{children}</span>;
}
`TextFieldController.tsx`
'use client';
import TextField, { type TextFieldProps } from './TextField.client';
import Image from 'next/image';
import { useRef } from 'react';
import type { UseFormRegisterReturn, UseFormReturn } from 'react-hook-form';
interface TextFieldControllerProps extends TextFieldProps {
register: UseFormRegisterReturn<string>;
hookForm: UseFormReturn<any>;
/**
* leftCaption에 문구를 표기하는 경우
*/
caption?: string;
/**
* rightCaption에 글자수를 표기하는 경우
*/
maxCount?: number;
/**
* rightCaption에 타이머를 표기하는 경우
*/
timer?: number;
}
export default function TextFieldController({
register,
hookForm,
caption,
maxCount,
timer,
...TextFieldProps
}: TextFieldControllerProps) {
const textFieldRef = useRef<HTMLLabelElement>(null);
const { formState, watch, setValue } = hookForm;
const inputName = register.name;
const errorMessage = formState.errors[inputName]?.message;
const isRightError = maxCount ? watch(inputName).length > maxCount : false;
const isLeftError = !!errorMessage || isRightError;
const isError = isRightError || isLeftError;
const rightInputIconName = isError ? 'warning' : watch(inputName).length > 0 ? 'backspace' : '';
return (
<TextField
leftCaption={caption ?? (errorMessage as string) ?? ''}
rightCaption={
maxCount ? `${watch(inputName).length}/${maxCount}` : timer ? `${timer}초 후 재전송` : ''
}
rightInputIcon={
rightInputIconName && (
<Image
src={`/icons/24/${rightInputIconName}.svg`}
width={24}
height={24}
alt={rightInputIconName}
onClick={() => rightInputIconName === 'backspace' && setValue(inputName, '')}
/>
)
}
isLeftError={isLeftError}
isRightError={isRightError}
register={register}
ref={textFieldRef}
{...TextFieldProps}
/>
);
}
react-hook-form의 mode:onBlur을 사용하고 있기 때문에 input의 onBlur가 먹히지 않았다. 그래서 onBlurCapture를 사용하였다.
최종 PR : GitHub Pull Request 링크
3. 맺으며
이번 프로젝트에서는 TextField 컴포넌트의 재구조화를 통해 여러 중요한 개발 원칙을 실현했다. 초기에는 **react-hook-form**을 활용해 내부에서 모든 상태를 관리하는 방식을 채택했지만, 이로 인해 컴포넌트가 과도하게 복잡해지고 유연성이 떨어지는 문제가 발생했다. 이에 대한 해결책으로, 구조를 개선하여 TextFieldGroup, TextFieldLabel, **TextFieldCaption**과 같은 하위 컴포넌트들을 도입했다. 이러한 분리는 각 컴포넌트의 역할을 명확히 하고, 전체 코드의 가독성과 관리 용이성을 크게 향상시켰다.
더 나아가, TextFieldController 컴포넌트를 통해 **react-hook-form**과의 의존성을 줄임으로써, 더욱 독립적이고 재사용 가능한 구조를 구축했다. 이를 통해 코드의 안정성과 확장성을 높이는 동시에, 미래의 유지보수 작업에 대한 부담을 줄였다. 포커스 관리와 같은 세부적인 기능적 문제도 성공적으로 해결했다. onFocus 및 onBlur 이벤트를 활용하여 사용자의 인터랙션에 따라 동적으로 스타일을 조정하는 방식을 채택했다.
이 프로젝트를 통해 우리 팀은 컴포넌트의 독립성, 재사용성, 유지보수 용이성이라는 중요한 소프트웨어 공학 원칙들을 실천할 수 있었다. TextField 컴포넌트의 성공적인 재구조화는 팀의 기술적 역량을 향상시키는 데 중요한 기여를 했으며, 향후 프로젝트에도 이러한 접근 방식을 적용할 수 있는 기반을 마련했다.