Frontend/트러블 슈팅

[트러블 슈팅] MobX(observer) + React-Hook-Form(FormProvider) 충돌

동혁이 2025. 3. 16. 14:46

MobX(observer) + React-Hook-Form(FormProvider) 충돌

 

 

 

사이드 프로젝트에 React-Hook-Form 라이브러리를 도입하면서 발생한 트러블 슈팅 해결과정입니다.

 

🔥 문제 상황

CreatePage ->  handleSubmit함수 필요 (제출 버튼 위치)

SectionTitleEditor, QuestionEditor -> require, formState : { errors } 필요 (폼 위치)

CreatePage 부모 컴포넌트에서 SectionTitleEditor, QuestionEditor 컴포넌트까지의 경로는

 

CreatePage -> SectionListEditor -> SectionEditor -> SectionTitleEditor, QuestionEditor

 

위와 같이 컴포넌트의 깊이가 깊었고 이럴 때 매번 props 로 컴포넌트간 데이터를 다 전달하기엔 너무 복잡하고 힘이 든다. (prop drilling 이 발생)

 

이러한 상황에서 쉽게 폼 데이터를 사용하는 방법이 FormProvider이다

 

적용하기

1. CreatePage 부모 컴포넌트에 제출 버튼이 있기에 여기서 FormProvider와 useForm을 import 해온다.

 

여기서 methods는 useForm을 통해 반환된 객체이고 methods를 통해 handleSubmit 메서드를 가져올 수 있다.

 

<FormProvider {...methods}>
  <Button
    type="button"
    onClick={methods.handleSubmit(onSubmit)}
  >
    제출
  </Button>
  /.../
</FormProvider>

 

FormProvider 세팅은 여기서 끝이다.

 

2. FormProvider 로 하위에 쉽게 접근하게 되었고 하위에서는 useFormContext 를 사용하여 해당 폼 객체에 대해 접근을 한다.

- 하위 컴포넌트가 input, textarea, select 태그면 register 를 사용해 등록해준다.

 

// SectionTitleEditor
const SectionTitleEditor = observer(function SectionTitleEditor({ ... }: Props) {
	const {
    register,
    formState: { errors },
  } = useFormContext();
  
  
  return (
  	/.../
      <Input
          value={section.title}
          {...register("title", {
            required: {
              value: true,
              message: "제목을 입력해주세요.",
            },
            onChange: handleChangeTitle,
          })}
        />
        {errors.title && (
          <p>
            {errors.title.message as string}
          </p>
        )}
    /.../
  )
}

 

+) QuestionEditor 컴포넌트 내용이 비슷하기 때문에 생략 (데이터는 다름)

 

3. observer

- React는 기본적으로 props나 state가 변경될 때만 리렌더링한다.

- MobX의 observable 상태 변화는 React가 감지할 수 없다.

- 그렇기 때문에 observer는 MobX 상태 변화를 감지하여 컴포넌트를 리렌더링하게 만든다.

 

생략 - mobX의 observable로 관리하는 상태를 SectionTitleEditor, QuestionEditor 컴포넌트 내부에서 사용중이기 때문에 컴포넌트를 observer로 감쌌다.

 

 

😡 문제 발생

제출 버튼을 눌러 질문지를 생성하고 react-router의 useNavigate로 edit 페이지로 이동했을때 빈 화면과 콘솔창에 에러가 발생했다.

 

 

❗️ 첫 번째 시도

원인을 유추했을때 useFormContext에서 사용하는 register가 null값으로 FormProvider Context에 전달되어서 나오는건가? 라고 해석을 하면서 시작했다.

CreatePage에서 methods 변수를 선언한 useForm 내부에 defaultValues를 넣어 null일 경우를 대비하는 방법을 시도해보았다.

 

const methods = useForm({
    defaultValues: {
      title: '',
      description: '',
      sections: / ... /,
      /.../
    }
  });

 

실패..

 

❗️ 두 번째 시도

에러 현상 유추

register 속성이 null이라고 나오는 에러 메시지를 보고 useFormContext를 구조 분해할당하는것을 form 변수로 만들어 콘솔을 찍어보았다.

 

// 기존 코드
const { register, formState : { errors } } = useFormContext();

// 변경

const form = useFormContext();
console.log(form);

 

 

동작

1. 제출 버튼 정상 동작 (위에 데이터 잘 들어옴)

2. FormContext 정상 존재

3. 페이지 이동 시작 (navigate() 호출)

4. form이 null로 변함

 

의심

1. navigate()가 호출되면서 React Router가 페이지 전환 시작

2. 현재 페이지 컴포넌트들이 cleanup 되기 시작

3. FormProvider가 먼저 언마운트됨 -> form context가 null이 됨

 

 

4. 하지만 MobX의 observer로 감싸진 컴포넌트들은 아직 살아있음

5. 이 상태에서 observer 컴포넌트들이 마지막으로 한번 더 렌더링 시도 (props로 받고있는 observable한 MobX 상태가 변경되었기 때문)

6. 이때 useFormContext()를 호출하지만 이미 FormProvider는 없어진 상태

 

시도

 

const form = useFormContext();

if (!form) return null;

const {
  register,
  formState: { errors },
} = form;

 

위와같이 페이지 이동할때는 컴포넌트를 보여줄 필요가 없다고 판단해 조건문으로 막자 정상작동!