제목은 몬스터 도감이지만 사실상 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으로 바꾸어 해결하였다
'React' 카테고리의 다른 글
깃허브 페이지와 커스텀 도메인 (0) | 2024.04.03 |
---|---|
react-hook-form,react-query 사용한 몬스터도감 만들기 (2) (0) | 2024.03.26 |
react-hook-form을 이용한 인풋 컴포넌트 구현 (0) | 2024.03.20 |
버튼 컴포넌트 css 파일 관리 (0) | 2024.03.12 |
인풋 컴포넌트 상태관리 (1) | 2024.02.29 |