Formik 라이브러리에서 useRef 활용하기 대작전!

💛 nalsae·2024년 5월 20일
2
post-thumbnail

 React에서 Form 관련 요소를 다루는 방식은 크게 제어 컴포넌트와 비제어 컴포넌트로 구분할 수 있다. 간단하게만 설명하면 제어 컴포넌트 방식은 useState 훅을 사용하여 사용자가 입력하는 값을 상태로 저장하는, 즉 React 자체적으로 사용자의 입력 값을 제어하는 방식을 의미한다. 반면 비제어 컴포넌트는 useRef 훅을 사용하여 직접 DOM 요소를 참조함으로써 사용자가 입력하는 값을 획득하는 방식이다. 후자의 경우 당연히 React 자체적으로 입력 값 제어가 불가능하기 때문에 사용자가 입력 값을 변경한다고 해서 리렌더링이 발생하지 않는다. 비제어 컴포넌트 관련 내용은 하단 링크에서 더 자세히 살펴볼 수 있다.

📝 React 공식 문서의 비제어 컴포넌트

: https://ko.legacy.reactjs.org/docs/uncontrolled-components.html


🤔 비제어 컴포넌트 방식을 사용하는 상황?

 state의 변경을 기준으로 리렌더링을 수행해야 하는 보편적인 상황에서는 당연히 제어 컴포넌트 방식으로 Form 관련 요소를 다루는 것이 타당하다. React 공식 문서에서도 제어 컴포넌트 방식을 권장하고 있다.

 그러나 피치 못하게 비제어 컴포넌트 방식으로 Form 관련 요소를 제어해야 하는 경우가 있기 마련이다. 그 대표적인 예시로 <input> 요소에 인위적으로 focus 상태를 부여해야 하는 상황을 들 수 있다. focus 상태를 부여하는 과정에서 리렌더링이 발생하여 사용자가 기존에 입력했던 값들이 초기화되는 대참사가 일어나서는 안 될 일이다. 이러한 경우 React 18 이상의 환경에서는 앞서 언급했던 useRef 훅을 이용하여 DOM 요소를 직접 참조하고 조작함으로써 리렌더링을 방지할 수 있겠다.


🔍 Formik 라이브러리와 useRef

 그렇다면 Formik 라이브러리의 API로 명세되어 있는 <Formik>, <Field> 등의 커스텀 컴포넌트에는 useRef를 활용할 수 있을까? 활용할 수 없을 것 같지만 당연히 활용할 수 있다. 하지만 보편적인 React 문법과 동일하면 이 글을 굳이 작성할 필요도 없었을 것이다. useRef 훅을 호출하여 ref 객체를 생성하는 것까지는 동일하지만, 컴포넌트에 prop으로 전달하는 방식이 조금 다르다. 하단의 예제 코드로 이를 쉽게 이해해보자.

// 보편적인 React 컴포넌트의 useRef 훅 활용 예제

const inputRef = useRef<HTMLInputElement>(null);

return <input ref={inputRef} />
// Formik 라이브러리 <Field> 컴포넌트의 useRef 훅 활용 예제

const fieldRef = useRef<HTMLInputElement>(null);

return <Field innerRef={fieldRef} />

 예제 코드에서 확인할 수 있듯이 일단 기본적으로 가장 큰 차이점은 컴포넌트에 ref 객체를 props로 전달할 때 ref가 아니라 innerRef의 값으로 할당해야 한다는 것이다. 이외에는 useRef 훅의 제네릭 타입 정의도 동일하다. 여기서 Formik 라이브러리의 innerRef 명세를 잠깐 살펴보고 가면 다음과 같다.

Formik 라이브러리 공식 문서의 innerRef 설명부

innerRef?: (el: React.HTMLElement<any> => void)

When you are not using a custom component and you need to access the underlying DOM node created by Field (e.g. to call focus), pass the callback to the innerRef prop instead.

 간단히 해석해보면 React 컴포넌트에 전달하는 ref prop과 동일하게 DOM 요소에 직접 접근해야 하는 경우 innerRef prop을 커스텀 컴포넌트에 전달하라는 의미다.


💡 <Formik>useRef 활용하기

 이렇듯 Formik 라이브러리의 <Field> 커스텀 컴포넌트는 ref 객체를 값으로 전달하는 prop의 이름만 상이할 뿐, 보편적인 React 문법과 큰 차이점이 없다. 이쯤에서 이 글을 쓰게 된 진짜 목적, Formik 라이브러리의 <Formik> 커스텀 컴포넌트에 useRef 훅을 활용하는 예제를 소개해보고자 한다.

// Formik 라이브러리 <Formik> 컴포넌트의 useRef 훅 활용 예제

import { FormikProps } from 'formik';

const formikRef = useRef<FormikProps<FormValueType>>(null);

return <Formik innerRef={formikRef} />

 앞서 살펴봤던 <Field> 컴포넌트의 예제와 대부분 동일하지만, 큰 차이점이 있다면 바로 useRef 훅의 제네릭 타입 정의 부분일 것이다. <Formik> 컴포넌트는 그 자체로 실체가 있는 DOM 요소가 아니라 자신의 자식 컴포넌트들이 참조할 수 있는 context 객체를 전달하는 provider 역할을 한다. 이 context 객체의 이름은 공식 문서에 formikBag이라고 명세되어 있다. 따라서 전달할 context 객체의 타입을 useRef 훅에 정의해야 하고, 결과적으로는 상단 예제 코드처럼 작성할 수 있겠다. 여기서 FormValueType에는 <Form> 컴포넌트에 prop으로 전달하는 initialValue 객체의 타입을 전달하면 된다.

 그렇다면 <Formik> 컴포넌트에 useRef 훅까지 활용하면서 직접적으로 참조해야 할 일이 당최 언제 있을까? <Formik> 컴포넌트는 실체가 있는 DOM 요소가 아니므로 focus 상태를 부여해야 하는 상황도 없을 텐데 말이다. 필자는 <Formik> 컴포넌트가 전달하는 context 객체를 참조할 수 없는 depth에서 <Formik> 컴포넌트에 직접 접근하여 context 객체를 참조할 때 유용하게 활용했다. 본래 <Formik> 컴포넌트가 전달하는 context 객체는 Formik 라이브러리의 useFormikContext 훅을 호출하여 참조할 수 있다. 그런데 이 훅은 반드시 <Formik> 컴포넌트의 하위 depth, 즉 <Formik> 컴포넌트 하위의 자식 컴포넌트 위치에서 호출해야 한다. 그렇지 않은 경우 하단 이미지와 같이 경고 메시지가 콘솔에 출력된다. 참조하려는 context 객체 역시 console.log로 출력해보면 undefined를 반환한다.

 하위 컴포넌트에서 context 객체를 참조하는 상황만 있다면 참 좋겠지만 필자는 이번에 진행했던 프로젝트에서 그렇지 못한 상황을 마주쳤다. 바로 <Formik> 컴포넌트와 같은 depth의 다른 컴포넌트에서 submit 이벤트를 인위적으로 트리거해야 하는 경우였다. 이해를 돕기 위해 예제 코드를 간략하게나마 첨부하면 다음과 같다.

const handleGoNext = async () => {
  if (!formikRef.current) return;
  
  const { validateForm, setTouched } = formikRef.current;
  const errors = await validateForm();

  if (errors && Object.keys(errors).length) {
    setTouched(... 중략 ...);

    return;
  }

  changeType('submit');
  open();
};

<Formik innerRef={formikRef} >
  ... 중략 ...
</Formik>
<div>
	<button onClick={handleGoBack} />
    <button onClick={handleGoNext} />
</div>
{isOpen && type === 'bannerSubmit' && (
  <SubmitModal submitForm={formikRef.current!.submitForm} />
)}

 예제 코드를 살펴보면 <Formik> 컴포넌트가 자체적으로 트리거 해야 하는 submit 이벤트를 같은 depth의 <SubmitModal /> 컴포넌트가 prop으로 context 객체의 submitForm 메서드를 전달 받아 대신 수행하고 있다. 또한 버튼의 click 이벤트 핸들러 handleGoNext의 내부 로직을 살펴보면 마찬가지로 context 객체의 validateForm, setTouched 메서드를 참조하여 Form의 유효성 검사와 각 입력 필드를 touched 상태로 조작하는 일을 수행하고 있다. 수동으로 번거롭게 이러한 작업을 해주어야 했던 이유는 submit 이벤트를 인위적으로 <SubmitModal /> 컴포넌트로 이관하는 순간 유효성 검사나 입력 필드를 touched 상태로 조작하는 일은 별개로 처리해야 하는 작업이 되어버렸기 때문이다.


😇 이런 상황이 없었으면 좋겠지만..

 지금까지 살펴본 바와 같이 <Formik> 컴포넌트에 useRef 훅을 사용하여 context 객체를 참조하고, 직접 수동으로 관련 메서드를 호출하는 작업은 꽤나 복잡하고 번거롭다. 물론 Form에서 자체적으로 submit 이벤트를 주관하고 트리거 하는 것이 가장 최선의 상황이라는 데는 동감한다. 하지만 프로젝트를 진행하다 보면 필자가 경험한 것처럼 submit 이벤트와 관련된 메서드를 다른 컴포넌트에서 수동으로 호출해야 하는 별의별 일이 생길 수도 있다. 이런 상황에서 필자의 경험이 도움이 되었으면 하는 바람과 함께 글을 마친다.

🙏 출처

https://formik.org/docs/api/field#innerref

profile
𝙸'𝚖 𝚊 𝚍𝚎𝚟𝚎𝚕𝚘𝚙𝚎𝚛 𝚝𝚛𝚢𝚒𝚗𝚐 𝚝𝚘 𝚜𝚝𝚞𝚍𝚢 𝚊𝚕𝚠𝚊𝚢𝚜. 🤔

0개의 댓글