Recoil 메모리 누수 확인을 하려고, 성능 확인 라이브러리 이것 저것을 확인해보다가 React Dev tools를 사용하게 되었다.
Dev tools를 사용하면서 메모리 누수 보단 오히려 렌더링 관점의 확인을 더 할 수 있었다.
예를 들어, 컴포넌트의 리렌더링 빈도와 원인을 시각적으로 파악할 수 있었다.
내가 발견한 상황은,
input의 값을 useState-onChange로 변경하면, 글자 하나 변경당 useState를 갖는 컴포넌트가 리렌더 되는 것을 확인했다.
하지만 react-hook-form에서는 register가 적용된 input의 값이 변경되어도, 컴포넌트가 리렌더 되지 않았다.
[ 여기서 'register가 적용된 input'은 react-hook-form에서 제공하는 함수로 관리되는 입력 필드이다. ]
왜 그런지 이유가 궁금했다.
결과적으로는, register가 적용된 input은 ref 사용을 통해 리렌더링이 일어나고 있지 않았던 것이다. [ useRef는 랜더링이 필요없는 참조 값임을 알려주는 React Hook이다. ref는 DOM 요소에 직접 접근할 수 있는 기능을 제공하며, 값이 변경되어도 리렌더링이 트리거되지 않는다. ]
동작 관련 코드들이 너무 길기 때문에, 중요하게 생각한 지점을 간추려 밑에 남기겠다.
[ 다만, useState-onChange로 글자를 입력할 때, 리렌더링이 되는 상황이 나쁜 것인가? 꼭 ref를 사용 해야 하는가?에 대해서 고민할 필요가 있다고 생각하여 이 부분에 대해서는 다음에 간단하게 정리 해보고자 한다. ]
const register: UseFormRegister<TFieldValues> = (name, options = {}) => {
...
name,
onChange,
ref: (ref: HTMLInputElement | null): void => {
...
},
};
};
const onChange: ChangeHandler = async (event) => {
...
// "name" 값에 해당하는 필드의 값을 fieldValue로 설정
// Ref 사용으로 re-rendering X
set(_formValues, name, fieldValue);
// 유효성 / 규칙 준수 여부 검증이 필요 없을 시
if (shouldSkipValidation) {
// re-rendering 여부 판단 후 실행
// const { watch } = useForm(); 를 사용하고, input 값이 변경되면 rendering
const shouldRender = !isEmptyObject(fieldState) || watched;
return (
shouldRender &&
_subjects.state.next({ name, ...(watched ? {} : fieldState) })
);
}
// 값이 변경되었고
if (isFieldValueUpdated) {
field._f.deps &&
trigger(
field._f.deps as
| FieldPath<TFieldValues>
| FieldPath<TFieldValues>[],
);
// 유효성 / 규칙 비준수 시
// const { formState } = useForm() 을 사용한다면 rendering
shouldRenderByError(name, isValid, error, fieldState);
}
}
set
type FieldValues = Record<string, any>;
type FieldPath<TFieldValues extends FieldValues> = Path<TFieldValues>;
export default (
object: FieldValues, // 값을 설정할 대상이 되는 객체
path: FieldPath<FieldValues>, // FieldValues 객체의 경로
value?: unknown, // 설정할 값
) => {
...
...
object[key] = newValue;
object = object[key];
return object; // 최종적으로 수정된 객체 반환
};
shouldRenderByError
const shouldRenderByError = (
name: InternalFieldName,
isValid?: boolean,
error?: FieldError,
fieldState?: {
dirty?: FieldNamesMarkedBoolean<TFieldValues>;
isDirty?: boolean;
touched?: FieldNamesMarkedBoolean<TFieldValues>;
},
) => {
const previousFieldError = get(_formState.errors, name);
const shouldUpdateValid =
(_proxyFormState.isValid || _proxySubscribeFormState.isValid) &&
isBoolean(isValid) &&
_formState.isValid !== isValid;
if (_options.delayError && error) {
delayErrorCallback = debounce(() => updateErrors(name, error));
delayErrorCallback(_options.delayError);
} else {
clearTimeout(timer);
delayErrorCallback = null;
error
? set(_formState.errors, name, error)
: unset(_formState.errors, name);
}
re-rendering 조건 궁금하다면
https://www.developerway.com/posts/react-re-renders-guide
번역본: https://velog.io/@yoonvelog/react-rerendering
위 링크에서는 React 컴포넌트가 리렌더링되는 조건과 이를 최적화하는 방법을 상세히 다룬다. 리렌더링 최적화가 필요할 때 유용한 가이드가 될 것이다.
form을 다루는 여러가지 방법이 궁금하다면
해당 글에서는 React에서 폼을 관리하는 다양한 접근법과 각각의 장단점을 소개하고 있다. React로 폼을 구현할 때 참고하기 좋은 자료이다.
링크
register & onChange & shouldRenderByError
set
Field & FieldRefs & InternalFieldName & FieldValues
FieldPath & Path & PathInternal
isKey
stringToPath
compact
get
RegisterOptions
isObject
isWatch
useFormProps
https://github.com/react-hook-form/react-hook-form/blob/master/src/types/form.ts
cloneObjects
'FE > React' 카테고리의 다른 글
useState가 의도대로 동작을 하지 않는다. (0) | 2025.02.22 |
---|---|
React useRef vs useState (0) | 2025.02.13 |
Recoil 메모리 누수 확인하기 (0) | 2025.01.11 |
Recoil (0) | 2024.12.27 |
npm vs yarn (0) | 2024.12.26 |