폼 렌더링 성능 개선기 (feat. React Hook Form)

limhi·2024년 9월 3일
0
post-thumbnail

배경 및 동기

이전 프로젝트에서 React-hook-form을 활용하여 재사용 가능한 비제어 컴포넌트(Uncontrolled Component)를 구현한 경험을 공유하고자 합니다.
해당 작업을 시작하게 된 주요 동기는 다음과 같습니다.

  1. 성능 개선: 폼 입력값이 많은 경우, 각 입력마다 발생하는 리렌더링으로 인해 화면 깜빡임과 속도 저하가 발생했습니다. 이는 사용자 경험을 저하시키는 요인이 되었습니다.

  2. 코드 중복 최소화: react-hook-form 라이브러리를 사용하고 있었지만, 각 컴포넌트마다 반복적으로 코드를 작성하고 있었습니다. 기존 디자인 시스템을 활용한 재사용 가능한 컴포넌트를 만들어 개발 효율성과 개발자 경험을 향상시키고자 했습니다.

이 과정에서 가장 큰 의문점이었던 폼이 입력될 때마다 왜 화면이 깜빡이고 속도가 저하된 것인지왜 비제어 컴포넌트로 구현해야 되었는지에 대해 학습하였던 점을 정리해보았습니다.

문제 분석

화면이 깜빡였던 이유

리액트에서 렌더링이란, 리액트가 컴포넌트에게 현재의 props와 state를 기반으로 UI가 어떻게 생겼으면 좋겠는지 컴포넌트에게 요청하는 작업을 의미합니다.

따라서, 초기 렌더링 이후 상태가 변경되면 React는 컴포넌트를 리렌더링하여 변경된 부분만 업데이트합니다.
React 공식 문서: rerenders-when-state-updates

문제는 특정 컴포넌트가 리렌더링되면 그 하위 컴포넌트들도 함께 리렌더링된다는 점입니다. 이로 인해 다음과 같은 현상이 발생합니다.

  • 불필요한 리렌더링으로 화면 깜빡임과 지연이 발생
  • 데이터를 요청하는 컴포넌트의 경우, 불필요한 API 호출이 발생하여 성능 저하

따라서, 폼 입력값이 변경될 때마다 상태가 업데이트되어 리렌더링이 발생했고, 이것이 화면 깜빡임과 속도 저하의 원인이 되었습니다.

제어/비제어 컴포넌트(Uncontrolled Component)의 이해

React를 사용하면서 폼을 다룰 때, 제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled Component)의 개념은 매우 중요합니다. 각각 다른 방식으로 상태를 관리하며, 어떤 상황에서 어떤 방식을 선택해야 하는지를 이해하는 것이 필요합니다.

제어 컴포넌트 (Controlled Component)

제어 컴포넌트는 React가 입력값을 직접적으로 관리하는 방식입니다.
사용자가 입력한 값은 항상 React의 state에 저장되며, 이를 통해 실시간으로 UI가 업데이트됩니다.

예를 들어, <input> 요소가 있고 사용자가 텍스트를 입력하면, 이 값은 이벤트 핸들러를 통해 상태로 전송되고, 그 결과로 다시 렌더링 됩니다.

const ControlledComponent = () => {
  const [value, setValue] = useState('')
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value)
  }

  return (
    <div>
      <label>이름: </label>
      <input type="text" value={value} onChange={handleChange} />
      <p>입력한 이름: {value}</p>
    </div>
  );
}

비제어 컴포넌트 (Uncontrolled Component)

비제어 컴포넌트는 DOM 요소 자체가 값을 저장하는 방법입니다. 이 경우에는 ref를 이용하여 현재 input 값이나 폼 데이터를 직접 접근합니다. 즉, React상태와는 독립적으로 운영됩니다.

따라서 input값은 React의 리렌더링과 상관없이 직접 DOM에서 관리된다고 볼 수 있습니다. 폼 제출 시에만 값을 가져오며, 중간 입력과정에서는 React 상태를 업데이트하지 않습니다.

const UncontrolledComponent = () => {
  const inputRef = useRef()
  
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    console.log("name :", inputRef.current.value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>이름: </label>
      <input type="text" ref={inputRef} defaultValue="" />
      <button type="submit">🚀</button>
    </div>
  );
}

따라서, 사용자가 입력할 때마다 리렌더링을 하여 최신값을 유지하는 제어 컴포넌트를 사용하는 것보다 제출 시에만 값이 필요한 경우 비제어 컴포넌트를 사용하여 불필요한 리렌더링을 줄일 수 있는 것입니다.

useController 활용하여 비제어 컴포넌트 만들기

React-hook-form을 활용하여 비제어 컴포넌트를 구현하는 방법에는 여러가지가 있습니다. React-hook-form의 useController를 활용하여 비제어 컴포넌트를 구현했습니다. 이 방식을 선택한 이유는 다음과 같습니다.

  1. 컴포넌트의 재사용성과 유지보수성 향상
  2. Hook을 활용한 폼 관리로 코드 가독성 개선

아래는 useController를 활용하여 작성한 예시입니다.

interface FormInputProp<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>> extends TControl<TFieldValues, TName> {
	inputProps?: DryInputProps; // 기존 디자인시스템의 input 컴포넌트 타입
}

const FormInput = <TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>({
	inputProps,
	...props
}: FormInputProp<TFieldValues, TName>) => {
	const {
		field: { value, onChange, onBlur, ref },
		fieldState: { error },
	} = useController(props);

	return (
		<DryInput
			value={value}
			onChange={(e: ChangeEvent<HTMLInputElement>) => {
				onChange(e);
				inputProps?.onChange && inputProps?.onChange(e);
			}}
			onBlur={onBlur}
			ref={ref}
			{...inputProps}
			error={inputProps?.error || !!error}
			helperText={inputProps?.helperText || (!!error && error.message)}
		/>
	);
};

TFieldValues와 TName을 사용하여 필요한 form 옵션에 대해 커스텀한 TControl 제네릭 타입을 정의함으로써 다양한 형태의 폼 데이터를 처리하고자 하고 타입 추론이 용이하도록 하였습니다.

또한, 추가적인 onChange 호출을 위해 inputProps의 onChange를 호출할 수 있도록 작성하였습니다. 덕분에 부모 컴포넌트에서 추가적인 로직을 잘 전달할 수 있도록 하고 사용성을 더욱 높일 수 있었습니다.

결과 및 성과

  • 성능 개선: 크롬 익스텐션의 리렌더링 하이라이트 기능을 통해 불필요한 리렌더링이 줄어든 것을 확인
  • 사용자 경험 개선: 화면 깜빡임 감소와 속도 향상으로 전반적인 사용자 경험 개선
  • 재사용성 향상: TextInput, Select, Checkboxes 등 다양한 폼 컴포넌트를 비제어 방식으로 구현하여 재사용성 높임
  • 개발자 경험 개선: 선언적 프로그래밍 방식으로 코드 가독성과 유지보수성 향상

이 경험을 통해 React의 렌더링, 그리고 비제어 컴포넌트를 활용한 form 최적화에 대해 깨달을 수 있었습니다.
문득, 다른 사람들은 이러한 문제에 대해 어떻게 해결했는지 궁금해지네요.. 좀 더 조사를 해봐야겠습니다 👍

profile
null 사랑하지 않아 - 어반자카파

0개의 댓글