formik + react-query + typescript 에서 폼 다루기

우현민·2022년 3월 17일
1

React

목록 보기
3/10

리액트 생태계에서 typescript, formikreact-query 는 아주 대중적인 라이브러리들입니다. formik공식문서에 등장할 정도로 대중적이고 인정받은 폼 처리 라이브러리이고, react-queryserver state 라는 패러다임을 제시하면서 server state manager 의 대표주자로 굳건히 자리를 잡았습니다. typescript 야 뭐 더 말할 것도 없이 자바스크립트 생태계를 모두 먹어가는 중입니다.

formikreact-query
npm trends

오늘은 이 두 라이브러리를 통해 서버에서 받은 정보를 form 으로 처리하는 저만의 방법을 소개하려 합니다.

편의를 위해 "내 프로필 수정 페이지" 를 가정하겠습니다. 프로필 정보에는 이름, 성별, 나이, 웹사이트 이렇게 네 가지 항목이 있다고 하겠습니다. 그리고 그 중 "이름"은 바꾸지 못한다고 가정할게요.

편의상 import문은 생략하겠습니다 XD



아이디어

아래 내용들을 신경써서 만들었습니다.

  • 즉 중복되는 동일한 값에 대한 불필요한 state 를 가능한 최대로 줄일 것
  • 로직의 책임에 대한 분리가 확실할 것
  • 가능한 최신의 (fresh한) 정보를 수정할 수 있도록 할 것

그럼 이제 코드를 하나하나 살펴보겠습니다 :)


코드를 봅시다

react-query 는 서버 상태에 대한 로직을 아예 분리하여 선언적으로 관리하는 것을 권장합니다. 따라서 저는 이를 파일로 분리하여, 아래와 같은 컴포넌트 파일 구조를 선호합니다.

MyProfile/
|- MyProfile.tsx         # 컴포넌트 파일
|- MyProfile.types.ts    # 복잡한 타입의 경우 이 파일로 분리
|- MyProfile.queries.ts  # server 와 소통하는 파일
|- MyProfile.module.scss # 스타일 파일, CSS-in-JS 라면 MyProfile.styles.ts 등등

타입 정의 파일: MyProfile.types.ts

이 파일에는 필요한 타입들을 정의합니다. 리스폰스 타입과 폼 타입을 분리했는데, 이렇게 두 타입을 분리하면 서버 리스폰스 타입이 변경될 때 대응하기 편하다는 장점도 있고, 폼의 타입을 더 엄밀하게 지정할 수 있습니다.

// Profile.types.ts
interface MyProfile {
  name: string;    // 이름
  gender: Gender;  // 성별 (어딘가 전역적으로 정의되어 있고 import한 타입)
  age: number;     // 나이
  website: string; // 웹사이트
}

// 서버에서 넘어오는 data type
type MyProfileResponseDto = MyProfile;
// 수정할 때 서버에 넘겨주는 data type
type PatchMyProfileRequestDto = Partial<Omit<MyProfile, 'name'>>;

// Formik 에 이용할 폼 타입
type MyProfileForm = Partial<Omit<MyProfile, 'name'>>;

서버와 소통하는 파일: MyProfile.queries.ts

이 파일에는 서버와 소통하기 위한 함수들 + react-query 로직을 분리하기 위한 custom hook 이 정리되어 있습니다. 서버 데이터에 대한 data transformation 때문에 select 옵션을 써야 한다거나, id값에 따라 enabled 옵션을 결정하는 등등 많은 내용을 이 파일에서 처리할 수 있습니다. 실제로 이런 로직은 server state manager 의 책임인 것이 좋기 때문에 tkdodo님 권장사항 처럼 파일 자체를 분리해 버렸습니다.

아래 usePatchMyProfile 의 케이스처럼, 성공 시 해당 데이터를 다시 fetch 하는 로직은 server state manager 의 책임이라고 보면 깔끔해집니다.

// Profile.queries.ts

// 정보 받아오기
const getProfile = () => axios.get<MyProfileResponseDto>('/api/me');
export const useMyProfile = () => useQuery(['myProfile'], getProfile);

// 정보 수정하기
const patchProfile = (data: PatchMyProfileRequestDto) => axios.patch('/api/me', data);
export const usePatchMyProfile = () => {
  const queryClient = useQueryClient();

  return useMutation(patchProfile, {
  	onSuccess: () => queryClient.invalidateQueries(['myProfile']), // 성공 시 refetch
  })
};

스타일 파일: MyProfile.module.scss

넘어가겠습니다 :D


컴포넌트 파일: MyProfile.tsx

먼저 일반적인 formik 의 사용방식대로면 서버에서 받은 값을 formik 의 initialValue 로 넘겨줘야 합니다. 하지만 react-query 와 함께 사용하고 있기 때문에 이런 방식의 처리는 local copy 를 만들어냅니다. 즉 불필요한 중복되는 state가 생기는 거죠. 저는 state를 명확하게 정의하고자 했습니다.

export const MyProfile = () => {
  const { data } = useMyProfile();
  const { mutate } = usePatchMyProfile();
  
  const { values, handleSubmit, handleChange } = useFormik<ProfileForm>({
    initialValues: {},
    onSubmit: mutate,
  });
  
  return (
    <form onSubmit={handleSubmit}>
      <label>
        이름
        <input value={data.name} disabled />
      </label>
      <label>
        성별
        <select value={values.gender ?? data.gender ?? ''} name="gender" onChange={handleChange}>
          <option value={Gender.MALE}></option>
          <option value={Gender.FEMALE}></option>
        </select>
      </label>
      <label>
        나이
        <input type="number" value={values.age ?? data.age ?? ''} name="age" onChange={handleChange} />
      </label>
      <label>
        웹사이트
        <input value={values.website ?? data.website ?? ''} name="website" onChange={handleChange} />
      </label>
      
      <button type="submit">저장</button>
    </form>
  );
}

코드에서 볼 수 있다시피, formik 의 초기값을 과감하게 비워줬고, 각 인풋의 value 에 react-query 가 fetch 한 데이터를 보여주도록 구현했습니다. 즉 포믹에는 "수정된 값"만 저장하고, 수정되지 않은 값은 server state 의 선언적인 판단에 맡기겠다는 철학입니다. 이걸 위해서 ProfileForm 타입을 Partial 로 처리했습니다.



참고자료

profile
프론트엔드 개발자입니다

0개의 댓글