최근에 우연히 React Query와 관련된 우아한테크세미나를 보게 되었는데 세미나에서 state를 Client state와 server state로 구분하여 설명해주신게 굉장히 흥미로웠다.
react query의 장점도 워낙 많아서 쓰지 않을 이유가 없었고 무엇보다 프로젝트를 진행 하면서 코치님들께서 react query에 대한 언급도 자주해주셔서 관심 있게 봤던 기술이였기때문에 기존의 사용중인 프로젝트에 적용해보고자 한다.
사용자가 키워드를 입력하면 그 키워드에 맞는 비동기 API 통신과 클라이언트 전역상태에 대해 관리를 하고 있는 코드이다. Redux-saga를 사용하고 있다.
function* getMusicList({ payload: keyword }) {
const beforeKeyword: string = yield select((state) => state.music.keyword);
try {
if (keyword === beforeKeyword) throw new Error('동일한 키워드 검색');
yield put(setKeyword(keyword));
const data: IMusic[] = yield call(fetchPlayList, `${keyword} 플레이리스트`);
yield put(getMusicListSuccess(data));
history.replace(`/search?query=${keyword}`);
} catch (err) {
yield put(getMusicListFail(err));
}
}
컴포넌트가 mount, window focus가 되었을 때, query를 호출하는 방식이 아닌 사용자가 키워드를 입력 후 검색을 하고자 할 때만, query를 호출하려고 한다.
이렇게 구현하기 위해서는 useQuery 옵션 중 enabled를 사용하면 되며 enabled을 false시에 Query가 자동적으로 실행되는 것을 방지할 수 있다.
const getPlaylist = async (query: string, token: string | undefined) => {
try {
const { data } = await axiosInstance.get('/search', {
params: {
q: `${query}`,
pageToken: token || '',
},
});
return data;
} catch (err) {
throw new Error('fetch playlist error');
}
};
export const useGetPlaylist = ({
query,
token,
...option
}: CustomQueryOption) => {
return useQuery<IMusic[]>(
[QUERY_KEYS.PLAYLIST, query],
() => getPlaylist(query, token),
{ enabled: false, retry: false, ...option },
);
};
그다음 사용자가 키워드를 입력 후 검색버튼을 클릭했을 때만 query를 트리거 하려면 refetch()메서드를 적절한 시점에 호출해주면 된다.
const { refetch } = useGetPlaylist({
query,
onSuccess: (data) => {
// TODO: 바꿔야 할 부분(recoil 적용시)
console.log(data);
dispatch(setKeyword(query));
dispatch(getMusicListSuccess(data));
history.replace(`/search?query=${keyword}`);
},
onError: () => {
// TODO: 에러핸들링
console.log('실패시');
},
});
const handleSubmit = (
e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLDivElement>,
) => {
e.preventDefault();
if (myKeyword.length === 0) return;
if (keyword === myKeyword) return;
refetch();
};
그런데 조건부로 query를 실행하고자 하는 곳이 검색창뿐만 아니라 다음과 같이 카드형식의 아이템을 클릭했을 때도 발생해야 하기 때문에 매번 refectch()를 호출하는 것이 마음에 들지 않았다.
그래서 다른 방식을 사용하기로 했다...
1) 방식으로 구현을 하면서 느꼈던 단점은 최초 Fetch 후 신경 쓰지 않는다면 데이터가 "out of date"가 될 수 있다는 점으로 enabled를 false로 설정하면 컴포넌트가 마운트 되거나 window focus가 되어도 자동으로 query를 실행하지 않기 때문이다.
out of date는 처음에 불러왔던 데이터가 서버의 DB에서는 업데이트 되었지만 클라이언트에서는 업데이트 되기전의 상태를 가지고 있는 것을 의미한다.
그래서 enabled 옵션을 false로 두기 보다는 특정 state 값을 두어 검색어(query라는 변수명)가 입력되었을 때만 자동적으로 query가 호출되도록 해놓으면 mount 되거나 window focus이 되었을 때도 새로운 데이터를 불러올 수 있게된다.
// searchContainer.tsx
const keyword = useAppSelector((state) => state.music.keyword);
const [query, setQuery] = useState(keyword);
const { data } = useGetPlaylist({ query: keyword });
// services/queries/player.ts
export const useGetPlaylist = ({
query,
token,
...option
}: CustomQueryOption) => {
return useQuery<any>(
[QUERY_KEYS.PLAYLIST, query],
() => getPlaylist(query, token),
{ enabled: !!query, retry: false, ...option }, // enabled를 query로
);
};
이제는 검색창 컴포넌트와 카드 컴포넌트에서 해당 함수를 사용하여 키워드만 업데이트 해주면 자동적으로 Query가 호출되고 out-of-date의 위험에서 벗어날 수 있게 된다.
const handleSearchKeyword = (value: string) => {
setQuery(value);
dispatch(setKeyword(value));
history.replace(`/search?query=${value}`);
};
사실 캐싱의 이점으로 인해 Reacy Query를 도입한 이유가 가장 컸었는데 사용을 해보니 그동안 데이터를 fetching하는 동안 UI를 다르게 보여주기 위해 필요했던 추가적인 상태들을 관리 할 필요성이 없어졌고 무엇보다 캐싱 옵션만 적절하게 설정해주면 api를 통해 가져온 음악목록을 캐싱하고 있고 Unique key만 잘 활용하면 어느 컴포넌트에서도 불러올 수 있어 전역으로 저장해줄 필요성도 없어졌다는 점에서 왜 React Query를 사용하면 전역상태에 대한 고민이 어느정도 해결이 가능하다는 의미를 알 수 있는 계기가 되었던 것 같다.