새로고침을 해도 입력 값이 유지되는 Form 구현하기! (with formik)

💛 nalsae·2024년 3월 11일
2
post-thumbnail

 Form 관련 개발을 하다 보면 새로고침을 하더라도 사용자가 입력했던 값이 사라지지 않도록 하는 기능을 구현해야 할 때가 있다. 이러한 경우 보통 로컬 스토리지나 세션 스토리지를 사용하여 스토리지 안에 사용자의 입력 값을 저장하는 방식으로 구현하곤 한다.

 만약 formik 라이브러리를 사용하고 있는 상황이라면 어떨까? 물론 useFormikContext 훅을 사용하여 참조한 values 값을 스토리지에 저장하는 방식으로 구현이 가능하겠지만, 값이 변경될 때마다 스토리지에 저장하는 로직을 따로 작성해야 하니 여간 번거로운 일이 아닐 수 없다. 게다가 새로고침 시 스토리지에 저장된 입력 값이 있다면 이를 Form의 초기 값으로 설정해주는 로직도 작성해주어야 한다.

 그렇다면 이렇게 번거로운 작업을 좀 쉽게 해볼 수는 없을까? 이번 글에서 소개할 formik-persist 라이브러리를 사용하면 해결이 가능하다! formik-persist 라이브러리는 formik 라이브러리의 제작자가 제작한 라이브러리다. 라이브러리 이름에서도 유추해볼 수 있듯 formik 라이브러리로 구현한 Form의 value를 지속 가능하도록 도와주는 라이브러리다. 설치 방법은 다음과 같다.

npm install formik-persist --save

🔍 formik-persist 뜯어보기!

📌 기본 사용법

 공식 문서에 따로 명시되어 있지 않은 것으로 보아 아쉽게도 Yarn은 지원하지 않는 것 같다. 설치가 완료되었으면 사용법을 한 번 살펴보자. 사용법은 정말 간단하다. formik-persist 라이브러리에서 <Persist>라는 커스텀 컴포넌트를 import 해서 기존 formik 라이브러리의 <Form> 컴포넌트 최하단에 작성해주기만 하면 된다. 하단 예제 코드를 참조하면 아마 더 쉽게 이해할 수 있을 것이다.

// <Persist> 사용 예제

import { Formik, Form } from 'formik';
import { Persist } from 'formik-persist';

export default function App() {
  
  // ... 중략 ...
  
  return (
    <Formik
      initialValues={initialValues}
      validationSchema={validationSchema}
      onSubmit={handleSubmit}>
      <Form>
        <label htmlFor="text">이름</label>
        <Field id="text" type="text" name="name" placeholder="이름을 입력하세요." />
        <ErrorMessage name="name" />
          // ⭐️ formik-persist 라이브러리의 Persist ⭐️
        <Persist name="name-form" />
      </Form>
    </Formik>
  );
}

 정말 간단하다! 다만 <Persist> 컴포넌트에 name prop을 필수적으로 전달해야 한다는 점만 유념하면 된다. name prop은 말 그대로 스토리지에 저장될 이름을 의미한다. name-form이라고 값을 지정하면 하단 이미지처럼 스토리지에 저장된다.

📌 라이브러리 구현부로 이해하는 동작 방식

 스토리지에 저장된 값을 개발자 도구로 확인해보았더니 뭔가 익숙한 느낌이 든다. 사실 저 객체는 <Formik> 컴포넌트가 하위 컴포넌트에 전달하는 context 객체와 구조가 동일하다. 즉, <Persist> 컴포넌트는 Form에 change 이벤트가 발생할 때마다 context 객체를 스토리지에 저장한다고 볼 수 있다. 이러한 동작 과정은 formik-persist 라이브러리 코드를 뜯어보면 확인할 수 있다.

// formik-persist 라이브러리의 <Persist> 구현부

class PersistImpl extends React.Component<
  PersistProps & { formik: FormikProps<any> },
  {}
> {
  static defaultProps = {
    debounce: 300,
  };

  saveForm = debounce((data: FormikProps<{}>) => {
    if (this.props.isSessionStorage) {
      window.sessionStorage.setItem(this.props.name, JSON.stringify(data));
    } else {
      window.localStorage.setItem(this.props.name, JSON.stringify(data));
    }
  }, this.props.debounce);

  // Form에 change 이벤트 발생 시 호출
  componentDidUpdate(prevProps: PersistProps & { formik: FormikProps<any> }) {
    if (!isEqual(prevProps.formik, this.props.formik)) {
      this.saveForm(this.props.formik);
    }
  }

  // Form 초기 렌더링 시 호출
  componentDidMount() {
    const maybeState = this.props.isSessionStorage
      ? window.sessionStorage.getItem(this.props.name)
      : window.localStorage.getItem(this.props.name);
    if (maybeState && maybeState !== null) {
      this.props.formik.setFormikState(JSON.parse(maybeState));
    }
  }

  render() {
    return null;
  }
}

export const Persist = connect<PersistProps, any>(PersistImpl);

 상단 코드는 formik-persist 라이브러리의 <Persist> 컴포넌트 구현부를 발췌해온 것이다. 아무래도 훅이 등장하기 이전에 제작되어서 그런지 클래스 컴포넌트 방식으로 구현되어 있기는 하지만, 동작 과정을 확인하는 데는 크게 문제가 없다. 간단히만 살펴보면 컴포넌트가 마운트되어 componentDidMount 메서드가 호출될 때는 스토리지에서 getItem 메서드로 값을 가져오고, 가져온 값이 존재하는 경우에만 setFormikState 메서드로 formik의 context 객체에 저장한다. 한편 컴포넌트가 업데이트되어 componentDidUpdate 메서드가 호출될 때는 formik의 context 객체가 이전과 동일한 지 비교하고, 동일하지 않은 경우 인스턴스 메서드로 정의해놓은 saveForm을 호출하여 스토리지에 context 객체를 저장한다.

📌 isSessionStorage prop

 라이브러리를 뜯어보니 동작 방식이 좀 더 명확하게 이해되는 느낌이다. 그런데 여기서 잠깐, 한 가지 의문점이 들 수도 있다. context 객체는 로컬 스토리지에만 저장할 수 있는 것일까? 그렇지 않다. 상단 코드를 살펴보았다면 눈치채신 분들도 있겠지만 formik-persist 라이브러리는 로컬 스토리지에 값을 저장할 수도, 세션 스토리지에 값을 저장할 수 있다. 저장할 스토리지를 변경하기 위해서는 <Persist> 컴포넌트에 isSessionStorage prop을 추가로 전달하면 된다. 다음과 같이 말이다.

// <Persist> 사용 예제

import { Formik, Form } from 'formik';
import { Persist } from 'formik-persist';

export default function App() {
  
  // ... 중략 ...
  
  return (
    <Formik
      initialValues={initialValues}
      validationSchema={validationSchema}
      onSubmit={handleSubmit}>
      <Form>
        <label htmlFor="text">이름</label>
        <Field id="text" type="text" name="name" placeholder="이름을 입력하세요." />
        <ErrorMessage name="name" />
          // ⭐️ isSessionStoarge 값 지정하기 ⭐️
        <Persist name="name-form" isSessionStorage />
      </Form>
    </Formik>
  );
}

 isSessionStorage 값은 위와 같이 boolean 타입으로 할당하면 된다. 기본 값은 false로 할당되어 있기 때문에 prop을 따로 전달하지 않으면 자동으로 로컬 스토리지에 저장된다. 당연히 true로 값을 할당하여 전달하면 세션 스토리지에 저장될 것이다.

📌 debounce prop

 isSessionStorage 외에도 <Persist> 컴포넌트에 옵셔널하게 전달할 수 있는 prop이 하나 더 있는데, debounce가 바로 그것이다. 이름에서도 유추가 가능하듯 debounce에 할당한 값만큼 스토리지에 저장하는 행위를 디바운싱한다. 예를 들어 debounce 값을 500으로 할당하면 사용자가 값을 입력하는 행위가 종료된 후 500ms가 지나야 스토리지에 context 객체를 저장하는 것이다. 이를 활용하여 <Field>가 많은 Form의 성능 최적화를 시도해볼 수 있겠다. 사용법은 하단 예제를 참고하면 된다.

// <Persist> 사용 예제

import { Formik, Form } from 'formik';
import { Persist } from 'formik-persist';

export default function App() {
 
 // ... 중략 ...
 
 return (
   <Formik
     initialValues={initialValues}
     validationSchema={validationSchema}
     onSubmit={handleSubmit}>
     <Form>
       <label htmlFor="text">이름</label>
       <Field id="text" type="text" name="name" placeholder="이름을 입력하세요." />
       <ErrorMessage name="name" />
         // ⭐️ debounce 값 지정하기 ⭐️
       <Persist name="name-form" debounce={500} />
     </Form>
   </Formik>
 );
}

👍 간편한 formik-persist

 지금까지 formik 라이브러리 사용 시 함께 사용하면 유용한 formik-persist를 살펴보았다. 스토리지에 저장하는 로직을 useEffect로 작성하여 커스텀 훅으로 분리해도 되기는 하지만, 이를 추상화한 <Persist> 컴포넌트를 활용하면 훨씬 간단하게 구현이 가능해서 프로젝트에 편리하게 적용할 수 있었다.

🙏 출처

https://github.com/jaredpalmer/formik-persist

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

0개의 댓글