본문 바로가기

React

react-hook-form,react-query 사용한 몬스터도감 만들기 (1)

제목은 몬스터 도감이지만 사실상 todoList입니다..

 

사실 ToDoList 정도야 기본적인 CRUD 기능만 있으면 끝이므로 react-query,react-hook-form 까지 사용하지 않아도 상관없지만... 최근 두 라이브러리를 학습하고 간단한 웹앱을 만들고 싶은 마음에 진행해 보았습니다

 

  • react-query : 서버상태관리 위해서 사용하였고, api호출 관련 코드는 전부 react-query로 구현하였음
  • zustand : 몬스터 수정관련(patch method)로직 구현위해 사용하였음 무엇보다 프롭 이리저리 왔다가는게 싫었음
  • react-hook-form : 인풋컴포넌트 상태값 관리위해 사용하였음, 인풋태그와 useState 사용 시 나오던 되게 뻔한 코드
    (onChange 함수 안에 setState 어쩌고저쩌고..하는거) 패턴이 나오지 않아 코드가 간결해짐

 

초기세팅

우선 주로 사용한 라이브러리는 

"dependencies": {
    "@tanstack/react-query": "^5.28.4",
    "axios": "^1.6.8",
    "react-hook-form": "^7.51.0",
    "json-server": "^1.0.0-alpha.23"
    .
    .
    .
  },

 

이렇게 4개 입니다

json-server는 로컬폴더에 있는 목업데이터를 사용하기 위함 입니다. 

json-server를 설치하고...

 

// db.json

{
  "monster": [
    {
      "id": "1",
      "monsterName": "주황버섯",
      "level": "8"
    },
    {
      "id": "2",
      "monsterName": "초록버섯",
      "level": "13"
    },
    {
      "id": "3",
      "monsterName": "리본돼지",
      "level": "12"
    }
  ]
}

 

루트 폴더 밑에 파일명은 db.json으로 이렇게 json 객체를 지정해 놓고

 

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "serve-json": "json-server --watch db.json --port 4000"
  },

package.json의 스크립트에 명령어를 지정해 놓으면 

npm run serve-json

 

이 명령어를 실행 할 수 있게됩니다. 후일 사용할 api 엔드포인트는 http://localhost:4000/monster 입니다

 

App.js

import { FormProvider, useForm } from 'react-hook-form';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import MonsterList from './MonsterList';
import MonsterForm from './MonsterForm';

const queryClient = new QueryClient();

function App() {
  const method = useForm();

  return (
    <QueryClientProvider client={queryClient}>
      <FormProvider {...method}>
        <MonsterForm /> // 몬스터 등록하는 컴포넌트
        <MonsterList /> // 등록한 몬스터데이터 랜더링하는 컴포넌트
      </FormProvider>
    </QueryClientProvider>
  );
}

export default App;

 

주요한 컴포넌트는 MonsterForm,MonsterList 두개이고 react-query 사용을 위한 QueryClientProvider 컴포넌트를 감싸 주었습니다.

MonsterForm,MonsterList 이 두개의 컴포넌트 안의 react-hook-form과 관련된 상태값 공유 위해 FormProvider 컴포넌트로 또 감싸주었습니다. (이거 중요해요!!)

 

MonsterForm.jsx ( 몬스터 등록위한 컴포넌트)

우선 몬스터 등록 위한 post 요청을 위한 MonsterForm 컴포넌트부터 코드를 보면..

import { useFormContext } from 'react-hook-form';
import FormMain from './components/input/FormMain';
import { useMonsterDataQuery, useMonsterMutationPost } from './utils/query/monsterQuery';
import { useEffect } from 'react';

export default function MonsterForm() {
  const { register, handleSubmit, reset } = useFormContext();
  const { mutate } = useMonsterMutationPost();
  const { refetch } = useMonsterDataQuery();

  const onClickAddMonster = (data) => {
    const { monsterName, monsterLevel } = data;
    if (monsterName === '' || monsterLevel === '') return;
    mutate(data); // react-query사용한 post 요청!!
  };

  return (
    <FormMain onSubmit={handleSubmit(onClickAddMonster)}>
      <FormMain.Label htmlFor="monsterName">몬스터 이름</FormMain.Label>
      <FormMain.Input id="monsterName" name="monsterName" registerFn={register('monsterName')} />
      <FormMain.Label htmlFor="monsterLevel">몬스터 레벨</FormMain.Label>
      <FormMain.Input id="monsterLevel" registerFn={register('monsterLevel')} />
      <FormMain.Button text="몬스터 등록" type="submit" />
      <FormMain.Button text="초기화" onClick={() => reset()} />
    </FormMain>
  );
}

 

 

이전 포스팅에서 만든 컴포넌트들을 사용하였다

input 컴포넌트에 react-hook-form이 제공하는 기능들을 사용하기 위해 register 함수를 프롭으로 내려줬다

input 컴포넌트의 각 상태값은 register함수의 인자를 프로퍼티로 가지는 하나의 객체 안에 저장된다

 

input 컴포넌트의 상태값 사용을 위해 react-hook-form 의 handleSubmit 함수를 사용하였다

onClickAddMonster 함수는 POST요청을 위한 함수이고

값이 존재한다면 input 컴포넌트의 값을 db.json에 등록한다 (mutate함수)

 

mutate(data);

 

근데 이 부분... 이녀석이 그냥 마음에 안든다....

몬스터 등록 성공 여부에 따른 메시지도 출력해주고 등록한 몬스터를 바로 확인하기 위해 데이터 패칭도 했음 싶다

 

const { register, handleSubmit, reset } = useFormContext();
const { mutate } = useMonsterMutation();
const { refetch } = useMonsterDataQuery();
// reset,refetch 함수 추가

const onClickAddMonster = (data) => {
    const { name, level } = data;
    if (name === '' || level === '') return;
    // 콜백함수 사용하여 post 요청 이후 data fetching
    mutate(data, {
      // 요청 성공 시
      onSuccess: () => {
        reset(); // 상태 값 초기화
        refetch(); // 데이터 다시 불러오기
        alert('등록 성공!!!!');
      },
      // 요청 실패 시
      onError: (error) => {
        alert(`${error.response.status} ${error.response.data}`);
      },
    });
  };

 

그래서 공식문서의 도움으로 이렇게 mutate 함수 안에 요청 성공 여부에 따른 콜백함수를 넣어주면 내가 원하는 기능들을 구현 할 수 있다!!....

처음엔 이렇게 만들었지만 그런데도 내 마음에 들지 않는다....

저 onSuccess, onError 관련 함수는 query함수에 넣는게 더 좋지 않을까..하는 생각이 들었다

react-hook-form에서 제공하는 form 컴포넌트에도 onError,onSuccess 속성을 제공해줘서 form 컴포넌트에서도 요청 성공 여부에 따른 랜더링 처리를 할 수 있지만 이러한 처리는 react-query를 사용 중 이기에 form 컴포넌트에 따로 속성값을 주진 않았다

 

monsterQuery.js (react-query)

import axios from 'axios';
import { useMutation } from '@tanstack/react-query';


const addMonster = (monster) => {
  return axios.post('http://localhost:4000/monster', monster);
};

export const useMonsterMutationPost = (reset, refetch) => {
  return useMutation({
    mutationFn: addMonster,
    onSuccess: () => {
      reset();
      refetch();
      alert('등록 성공!!!!');
    },
    onError: (error) => {
      alert(`${error.response.status} ${error.response.data}`);
    },
  });
};

 

그래서 분리를 위해 관련 함수들을 react-query 코드로 옮겨주고,

useMonsterMutationPost를 사용하는 시점에 인자로 reset,refetch 함수를 넣어주었다

 

나는 저렇게 분리하는게 깔끔해 보여서 좋은데
사실 저렇게까지 코드를 분리하는게 올바른 것인지는 잘 모르겠다....

 

이제 데이터 페칭 위한 몬스터 랜더링 위한 컴포넌트를 보자

 

MonsterList.jsx

import CustomButton from './components/buttons/CustomButton';
import { useMonsterDataQuery } from './utils/query/monsterQuery';

export default function MonsterList() {
  const { data, isLoading, refetch } = useMonsterDataQuery();
  if (isLoading) {
    return <div>불러오는중</div>;
  }
  return (
    <>
      <ul>
        {data?.data.map((hero) => {
          return <li key={hero.id}>{hero.name}</li>;
        })}
      </ul>
      <CustomButton text="새로고침" onClick={() => refetch()} />
    </>
  );
}

 

정말 간단한 데이터 패칭하는 컴포넌트이다... 딱히 설명할 게 없을정도로...

react-query에서 제공하는 isLoading 값을 가지고 로딩에 대한 랜더링을 처리하였다

 

구현 과정 중 있던 자잘한 트러블슈팅

 

옵셔널 체이닝 에러

      {data?.data.map((hero) => {
          return <li key={hero.id}>{hero.name}</li>;
        })}

 

여기 있는 옵셔널 체이닝 구문이 자꾸 parse Error가 나서 뭐가 문제인지...하고 찾아보았더니 eslint 설정 오류였었다..

eslint 속성 parserOptions.ecmaVersion이 2018 이었는데 이 버전에서는 옵셔널 체이닝이 안된다길래 2020으로 바꾸어 해결하였다