[react-query] 단일 진실 공급원을 고려하며 수정 form 만들기
Intro
원티드 프리온보딩 1월 - 리액트 쿼리 강의를 듣던 중,
server state와 client state에 대한 얘기가 나왔다.
server state 값을 client state에 할당해서
재사용할 때의 문제점에 대해 논했는데,
많이 공감되는 주제라 글을 쓰게 되었다.
server state
만약 useQuery가 userDetail이라는 data를 리턴한다면
userDetail을 그대로 사용하면 된다.
const { data : userDeail } = useQuery(...);
복제
근데 유저의 정보를 수정해야 한다면, userDetail을 변경할 수 없으니
새로운 client state를 만들어서 userDetail 값을 할당해주곤 했었다.
즉, 원본이 여러 개(?) 생기는 셈이 된다.
const { data : userDetail, isSuccess: getSuccess } = useQuery(...);
const [updatedUserDetail, setUpdatedUserDetail] = useState({});
useEffect(() => {
if (!getSuccess) return;
setUpdatedUserDetail(userDetail);
}, [getSuccess]);
single truth of source (단일 진실 공급원)
모든 데이터 요소를 한 곳에서만 제어 또는 편집하도록 조직하는 관례를 이른다.
이해한 내용
나는 이 말을 "원본을 여러 개 복제하면 복잡해지니까 1개만 유지하자"라고 이해했다.
실제 사례
실무를 하면서 이 개념이 중요한 상황이 있었다.
server state 복제 -> client state를 또 복제 -> ... 해서 state가 여러 개 생기다 보니
무엇이 최신의 진짜 값인지 알 수 없는 상황에 처한 적이 있었다.
상황을 구체적으로 설명하자면
테이블 정렬 기능을 만들 때도, 초반에는 프론트가 정렬을 구현해서
server state (userListData) 외에도 client state (userList)를 따로 만들었다
하지만 server state 값을 복사해서 client state를 만들었다 보니
팀원 분들이 두 개가 무슨 차이냐고 종종 헷갈려 하셔서,
백엔드에 정렬 API를 만들어 달라고 하고 + 최대한 server state만 사용하려고 했었다..
const { data : userListData, isSuccess: getSuccess } = useQuery(...);
const [userList, setUserList] = useState([]);
const [order, setOrder] = useState('');
const sort = () => {
// userList를 정렬하는 코드
};
useEffect(() => {
if (!getSuccess) return;
setUserList(userListData); // 초기값으로 userListData를 할당
}, [getSuccess]);
useEffect(() => {
if (!order) return;
const sorted = sort(userList);
}, [order]);
...
return (
<ul>
{userList.map(user =>
<li>{user.name}</li>
)}
</ul>
);
Solution
참고한 블로그에 따르면, server state의 값을 수정해야 할 경우
server state, client state를 엄격하게 분리하는 것이 방안이 될 수 있다고 한다.
- React Query에서 서버 상태를 유지하고, 클라이언트 상태에서 사용자가 변경한 사항만 추적한다.
- 사용자에게 표시하는 진실의 출처는 이 두 가지에서 파생된 상태이다.
- 사용자가 필드를 변경한 경우 클라이언트 상태를 표시합니다. 그렇지 않은 경우 서버 상태로 돌아간다.
// ✅ derive state from field value (client state)
// and data (server state)
<input {...field} value={field.value ?? data.firstName} />
내가 이해한 바로는 만약 username, phone, email 인풋이 있을 때
username 인풋만 사용자가 값을 변경했다면,
client state의 username만 변경된 값이 할당되고
나머지 phone, email은 빈 문자열이 그대로 유지된다고 이해했다.
즉, server state를 복제하는 것이 아니라
변경된 값만 추적하는 것!
const [field, setField] = useState({
username: '',
phone: '',
email: '',
});
정보 수정 form
수정 기능을 만들 때도 위와 같은 문제가 발생한 적이 있어서
solution을 적용해서 리팩토링 해봤다.
[이미지]
[요구 사항]
최초
- get api 리턴 값을 input value에 할당한다 (server state 값 사용)
수정
- 사용자는 input의 value를 수정할 수 있다.
- 수정 버튼을 클릭하면 지금까지 입력된 값을 console.log로 출력한다.
(* patch api는 mock data로 구현할 수 없어서 api 호출 대신 console.log로 대체했다.)
기존
- userDetail을 field에 할당해서 client state만 사용
const { data: userDetail, isLoading: getLoading } = useGetUserDetail();
const { mutate: updateUserDetail, isSuccess: updateSuccess } = useUpdateUserDetail();
const [field, setField] = useState(userDetail); // 복제
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
updateUserDetail(field); // client state를 patch api 인자로 넘긴다
};
...
<input name="username" value={field.username} onChange={handleInputChange} />
수정
- client state 값이 없으면 server state 값 우선 사용
- 사용자가 input에 값을 입력하면 client state 업데이트
- submit 이벤트 발생 시 지금까지 input에 입력된 value를 가져와서 patch api 인자로 넘긴다.
=> userDetail을 복제하지 않고, 변경된 값을 전달할 수 있게 된다.
Tip. 지금까지 입력된 input value 가져오는 법
e.currentTarget[input의 name].value을 이용하면
form 태그 내의 input들의 value를 가져올 수 있다.
const { data: userDetail, isLoading: getLoading } = useGetUserDetail();
const { mutate: updateUserDetail, isSuccess: updateSuccess } = useUpdateUserDetail();
const [field, setField] = useState({
username: '',
phone: '',
email: '',
}); // 빈 값
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setField((prev) => ({
...prev,
[name]: value,
}));
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const target = e.currentTarget;
// 지금까지 input에 입력된 value
const data = {
username: target.username.value,
phone: target.phone.value,
email: target.email.value,
};
updateUserDetail(data);
};
...
// client state 값이 없으면 server state 값 우선 사용
<input name="username" value={field.username || userDetail.username} onChange={handleInputChange} />
느낀 점
정보 수정 기능을 만들면서 항상
" 왜 useQuery에서 리턴해주는 데이터를 따로 복사해서 써야 하지?
전역 상태 값을 왜 또 다른 상태에 할당하지..?"라는 의문이 들었는데
대안을 찾게 되어서 기쁘다!
server state를 복제하지 않고
single truth of source 원칙을 잘 지키면서
작업을 하도록 노력해야겠다.
참고글
- 단일 진실 공급원
- react query and form
- 리액트를 다루는 기술 4장 - 이벤트 핸들링