๐Ÿ”ซ react-hook-form ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… ๊ธฐ๋ก

Suyeon Bakยท2024๋…„ 7์›” 17์ผ
2
post-thumbnail

how.... why... ์™œ ๋‹ฌ๋ผ ์™œ..

์ด์Šˆ ํ˜„์ƒ

๐Ÿšจ ๊ฐ™์€ register name์œผ๋กœ useWatch๋ฅผ ๋‘ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‹คํ–‰ ์‹œ ๋‘ watch ๊ฐ’์ด ๋‹ค๋ฅธ ํ˜„์ƒ ๋ฐœ์ƒ

๋ณด์ด๋Š” ๊ฑด ์˜ฌ๋ฐ”๋ฅธ ๊ฐ’์ธ๋ฐ submit ํ•˜๋‹ˆ ๋‹ค๋ฅธ ๊ฐ’์ด ์ฐํžŒ๋‹ค๊ณ ?!?!?!


์›์ธ ํŒŒ์•…

์ฒซ ๋ฒˆ์งธ ๊ฐ€์„ค
: useWatch๋ฅผ ๊ฐ™์€ register name์œผ๋กœ ๋‘ ๊ณณ์—์„œ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ ๋ฌธ์ œ๊ฐ€ ๋  ๊ฒƒ์ด๋‹ค.

react-hook-form github์—์„œ useWatch ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ณผ์—ฐ ์ค‘๋ณต register name์ด ๋ฌธ์ œ๊ฐ€ ๋ ๊นŒ?

// src/useWatch.ts

export function useWatch<TFieldValues extends FieldValues>(
  props?: UseWatchProps<TFieldValues>,
) {
  const methods = useFormContext();
  const {
    control = methods.control,
    name, // register name
    defaultValue,
    disabled,
    exact,
  } = props || {};
  const _name = React.useRef(name);

  _name.current = name;

  useSubscribe({
    disabled,
    subject: control._subjects.values,
    next: (formState: { name?: InternalFieldName; values?: FieldValues }) => {
      if (
        shouldSubscribeByName(
          _name.current as InternalFieldName,
          formState.name,
          exact,
        )
      ) {
        updateValue(
          cloneObject(
            generateWatchOutput(
              _name.current as InternalFieldName | InternalFieldName[],
              control._names,
              formState.values || control._formValues,
              false,
              defaultValue,
            ),
          ),
        );
      }
    },
  });

  const [value, updateValue] = React.useState(
    control._getWatch(
      name as InternalFieldName,
      defaultValue as DeepPartialSkipArrayKey<TFieldValues>,
    ),
  );

  React.useEffect(() => control._removeUnmounted());

  return value;
}

์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. useWatch์— ๋„˜๊ธด name prop์œผ๋กœ ref ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ด _name์— ํ• ๋‹นํ•œ๋‹ค.
  2. current ํ”„๋กœํผํ‹ฐ๋ฅผ name prop์œผ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.
  3. shouldSubscribeByName ํ•จ์ˆ˜์— _name.current์™€ formState์˜ name์„ ๋„˜๊ฒจ true๋ฉด value๋ฅผ updateํ•œ๋‹ค.

shouldSubscribeByName ํ•จ์ˆ˜ ์ฝ”๋“œ๋„ ์‚ดํŽด๋ณด์ž.

// src/logic/shouldSubscribeByName.ts

import convertToArrayPayload from '../utils/convertToArrayPayload';

export default <T extends string | string[] | undefined>(
  name?: T,
  signalName?: string,
  exact?: boolean,
) =>
  !name ||
  !signalName ||
  name === signalName ||
  convertToArrayPayload(name).some(
    (currentName) =>
      currentName &&
      (exact
        ? currentName === signalName
        : currentName.startsWith(signalName) ||
          signalName.startsWith(currentName)),
  );

์ฝ”๋“œ๋ฅผ ๊ฐ„๋žตํžˆ ๋ถ„์„ํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ์—์„œ true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

  1. name(useWatch์˜ prop์œผ๋กœ ๋„˜๊ธด name)์ด ์—†๊ฑฐ๋‚˜
  2. signalName(form์— registerํ•œ name)์ด ์—†๊ฑฐ๋‚˜
  3. name === signalName, ์ฆ‰ registerํ•œ ์ด๋ฆ„์„ useWatch์— ๋„˜๊ฒผ๋‹ค๋ฉด (name์ด string[] ํƒ€์ž…์ผ ๋•Œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€)

๊ทธ๋Ÿฌ๋‹ˆ๊นŒ registerํ•˜์ง€ ์•Š์€ ๊ฐ’์œผ๋กœ useWatch๋ฅผ ์‚ฌ์šฉ(์ดํ•˜ watch)ํ•˜๋Š” ๊ฑด ๋ฌธ์ œ๊ฐ€ ๋˜๊ฒ ์ง€๋งŒ, ๊ฐ™์€ register name๊ฐ€ ๋ฌธ์ œ๋˜๋Š” ์ฝ”๋“œ๋Š” ์—†๋‹ค. ๊ฐ™์€ register name์œผ๋กœ ๋‘ ๊ณณ์—์„œ watch ํ•ด๋„ ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ด ๊ฐ€์„ค์€ ํ‹€๋ ธ๋‹ค.



๋‘ ๋ฒˆ์งธ ๊ฐ€์„ค
: setValue๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค.

์ž์‹ ์ปดํฌ๋„ŒํŠธ(Component B)์—์„œ๋งŒ setValue ๊ฒฐ๊ณผ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋ฐ˜์˜๋˜์–ด ๋‘ watch ๊ฐ’์ด ๋‹ฌ๋ž๋‹ค.

์™œ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ setValue๋กœ ๊ฐ’์„ ๋ณ€๊ฒฝํ•ด๋„ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ watch ๊ฐ’์€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜์„๊นŒ?

rhf discussions์—์„œ ๋‚˜์™€ ๋น„์Šทํ•œ ์‚ฌ๋ก€๋ฅผ ๋ฐœ๊ฒฌํ–ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹ต๋ณ€์ด ๋‹ฌ๋ ค์žˆ์—ˆ๋‹ค.

useWatch์™€ setValue์˜ ์ˆœ์„œ์— ๋”ฐ๋ผ watch๊ฐ€ ์˜๋„๋Œ€๋กœ ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๊ณ , ์ด๊ฒŒ ์ •์ƒ์ด๋ผ๋Š” ๋‹ต๋ณ€์ด์—ˆ๋‹ค.

๋ถ€๋ชจ์™€ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ฐ’์ด ๋‹ค๋ฅด๊ฒŒ ์ฐํžŒ ์ด์œ ๊ฐ€ ์ด๊ฑฐ์˜€๋‹ค!!

  1. ์ž์‹ ์ปดํฌ๋„ŒํŠธ useWatch
  2. ์ž์‹ ์ปดํฌ๋„ŒํŠธ useEffect ๋‚ด๋ถ€ setValue
  3. ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ useWatch

์œ„ ์ˆœ์„œ๋กœ ์ง„ํ–‰๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์™€ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์˜ watch ๊ฐ’์ด ๋‹ฌ๋ž๋˜ ๊ฒƒโ€ฆ!

์‹ค์ œ๋กœ setTimeout ์ฝœ๋ฐฑ์—์„œ setValue๋ฅผ ์‹คํ–‰ํ•˜๋‹ˆ ๋‘ ์ปดํฌ๋„ŒํŠธ์—์„œ ์˜๋„ํ•œ ๋Œ€๋กœ ๊ฐ’์ด ์ฐํ˜”๋‹ค.

// ์ž์‹ ์ปดํฌ๋„ŒํŠธ
export const ComponentB = () => {
  ...	
  useEffect(() => {
    setTimeout(() => {
      setValue("useWatchTest", getTestValue(query, room));
    }, 0);
  }, [room]);

  ...

  return (...);
};
// ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ
export const ComponentA = () => {
  ...
  const useWatchTest = useWatch({ control, name: "useWatchTest" });

  const vaildateBookingsForm = () => {
    console.log("??? useWatchTest", useWatchTest);
    // ??? useWatchTest {a: 2, b: 1, c: 1} ๐Ÿ˜‡ ์˜ฌ๋ฐ”๋ฅธ ๊ฐ’
  };

	...

  return (...);
};

๊ฒฐ๋ก 
: useWatch์™€ setValue๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ ค๋ฉด, useWatch๊ฐ€ ๋จผ์ € ์‹คํ–‰๋˜๋„๋ก ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜์ž.

๋‚˜๋Š” ๋ถ€๋ชจ์ปดํฌ๋„ŒํŠธ๋Š” form์ƒ์„ฑ๊ณผ submit๋งŒ ๊ด€์—ฌํ•˜๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ๋กœ default ๊ฐ’์„ ๋ฐ”๊ฟ”์ค˜์•ผ ํ•  ๋•Œ setValue๋ฅผ ํ™œ์šฉํ–ˆ๊ธฐ์— ์ด๋ฒˆ ์ด์Šˆ๋ฅผ ๊ฒช์—ˆ๋‹ค. ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ watch๊ฐ€ ๋จผ์ € ์‹คํ–‰๋˜์–ด setValue๋กœ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•  ๊ฐ’์ด ๋ฐ˜์˜๋˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ!

setTimeout ์ฝœ๋ฐฑ์—์„œ setValue๋ฅผ ์‹คํ–‰ํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๊ฒ ์ง€๋งŒ, ๋‚˜์˜ ๊ฒฝ์šฐ์—๋Š” default ๊ฐ’์„ ์„ธํŒ…ํ•˜๊ธฐ ์œ„ํ•œ setValue์˜€๊ธฐ์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•ด๊ฒฐํ–ˆ๋‹ค.

// Component A
export const ComponentA = () => {

  ...

  const bookingForm = useForm<{
    useWatchTest: { a: number; b: number; c: number };
  }>({
    value: { 
	    ...DEFAULT_FORM_VALUES,
	    useWatchTest: getTestValue(room) ๐Ÿ‘๐Ÿ‘๐Ÿ‘
	  }
  });
  
	...
	
  return (...);
};

๋Œ์ด์ผœ๋ณด๋‹ˆ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ default ๊ฐ’์„ ์„ธํŒ…ํ•˜๋ ค ํ•œ ์ž์ฒด๊ฐ€ ์ข‹์€ ์„ ํƒ์ด ์•„๋‹ˆ์—ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” form ์ƒ์„ฑ๊ณผ submit๋งŒ ํ•˜๊ณ , ๊ฐ๊ฐ์˜ input ์ปดํฌ๋„ŒํŠธ์—์„œ validation๊ณผ value ํ•ธ๋“ค๋ง์„ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•˜๊ณ ์ž ํ–ˆ๋‹ค๊ฐ€ (๋„ˆ๋ฌด ๋ชฐ์ž…ํ•œ ๋‚˜๋จธ์ง€) default value๋„ ์„ธํŒ…ํ•ด๋ฒ„๋ฆฐ ๊ฒƒ....

๊ทธ๋ž˜๋„ ๋•๋ถ„์— ํœ˜๋šœ๋ฃจ ๋งˆ๋šœ๋ฃจ ์ž˜ ์“ฐ๋˜ react-hook-form์˜ ๋‚ด๋ถ€ ๊ตฌํ˜„๋„ ๋“ค์—ฌ๋‹ค๋ดค๋‹ค.

๋ถ€๋ชจ-์ž์‹ ์ปดํฌ๋„ŒํŠธ์˜ ๋ Œ๋”๋ง ์ˆœ์„œ๋„ ์ด๋ฒˆ ๊ธฐํšŒ์— ๋ณต๊ธฐํ–ˆ๋‹ค.

๋ฐฐ์šธ ์ ์ด ๋งŽ์€ ์ด์Šˆ์˜€๋‹ค.....!!!


์˜ˆ์‹œ ์ฝ”๋“œ๊ฐ€ ๊ถ๊ธˆํ•˜์‹  ๋ถ„๋“ค๊ป˜...

Component A (๋ถ€๋ชจ)
const DEFAULT_FORM_VALUES = {
	useWatchTest: {
    a: 1,
    b: 0,
    c: 0,
	}
}

// ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ
export const ComponentA = () => {
  const { data: room, error: roomError } = useRoom(...);
  const bookingForm = useForm<{
    useWatchTest: { a: number; b: number; c: number };
  }>({
    defaultValues: DEFAULT_FORM_VALUES
  });
  const { handleSubmit, control } = bookingForm;
  const useWatchTest = useWatch({ control, name: "useWatchTest" });

  const vaildateBookingsForm = () => {
    console.log("??? useWatchTest", useWatchTest);
    // ??? useWatchTest {a: 1, b: 0, c: 0} ๐Ÿ‘ฟ ์ž˜๋ชป๋œ ๊ฐ’ (setValue๊ฐ€ ๋ฐ˜์˜๋˜์ง€ ์•Š์€ default value)
  };

  return (
    <form onSubmit={handleSubmit(vaildateBookingsForm)}>
      <FormProvider {...bookingForm}>
        ...
        <ComponentB />
      </FormProvider>
    </form>
  );
};
Component B (์ž์‹)
const getTestValue = (room: IRoom) => ({
	a: room['default_a'], // 2
	b: room['default_b'], // 1
	c: room['default_c'], // 1
})

// ์ž์‹ ์ปดํฌ๋„ŒํŠธ
export const ComponentB = () => {
  const { data: room } = useRoom(...);
  const { setValue } = useFormContext();
  const useWatchTest = useWatch({ name: "useWatchTest" });

  useEffect(() => {
    setValue("useWatchTest", getTestValue(room));
  }, [room]);

  return <>...</>;
};
profile
๊ฐ•์•„์ง€๊ฐ€ ์ œ์ผ ์ข‹์€ ๊ฐœ๋ฐœ์ž ๐Ÿถ

0๊ฐœ์˜ ๋Œ“๊ธ€