개발을 하다 보면 초반에 잘 설계를 해도 여러 개발자들이 협업을 하다 보면 언젠간 부작용(Side Effects)
이 발생하기 마련이다.
프론트엔드에서 부작용을 일으키기 쉬운 부분인 Client State Management 에서 Recoil을 사용할 때 부작용을 최소화 할 수 있는 방법에 대해 작성했다.
Selector
는 Recoil에서 제공하고 있는 순수함수의 개념이다. 동일한 Parameter를 제공하면 동일한 값을 Return 해준다.
Selector
는 atom
에서는 불가능한 비동기 처리와 복잡한 로직을 구현할 수 있다. 공식문서에는 아래와 같이 나와있다.
Selector는 파생된 상태(derived state)의 일부를 나타낸다.
파생된 상태를 어떤 방법으로든 주어진 상태를 수정하는 순수 함수에 전달된 상태의 결과물로 생각할 수 있다.
Selector
를 사용하지 않고 atom
을 직접 변경하게 되면, 해당 atom
을 바라보고 있는 모든 컴포넌트에서 상태가 변경이 되어 예상치 못한 사이드 이펙트가 발생하게 된다.
Selector
를 사용하면 atom
을 직접 변경하지 않기 때문에 불변성을 유지할 수 있고, 특히 여러 atom
을 가공해 사용해야 하는 겨우 더욱 유용하다.
Selector
는 필요한 atom
만 구독하여 의존성 관리에 효율적이다.
import { atom } from "recoil";
export default atom({
key: 'countState',
default: 0,
});
import { DefaultValue, selector } from "recoil";
import countState from "../atom/countState";
export default selector({
key: "countSelector",
get: ({ get }): number => {
const count = get(countState);
return count + 1;
},
set: ({ set, get }, newCount)=>{
return set(countState, newCount + 10)
}
})
get: ({ get }): number => {
// countState를 구독하고 있습니다.
// countState가 바뀔 때마다 1증가 시켜서 반환합니다.
const count = get(countState);
return count + 1;
},
set: ({ set, get }, newCount)=>{
// 설명을 위한 코드로,
// 현재 count는 사용하고 있지 않습니다.
const count = get(countState);
return set(countState, newCount + 10)
}
set은 두가지 매개변수를 받는데, 첫번째는 Recoil의 상태, 두번째는 어떤 값으로 바뀔 것인지 새로운 값을 넣는다.
atom
을 직접 사용할 때 비동기 통신을 하는게 아닌 아래 코드와 같이 selector
안에서 비동기 통신 후 값을 사용할 수 있다.const myQuery = selector({
key: 'MyQuery',
get: async ({ get }) => {
return await myAsyncQuery(get(queryParamState));
},
});
알림을 구현하면서 낙관적 업데이트(Optimistic Update
)가 필요했다. 서버에서 알림 데이터를 패칭해온 뒤 클라이언트가 알림 삭제, 전체 읽기, 전체 삭제 등 액션을 취하면 해당 액션의 리스폰스를 받기 전 UI를 업데이트 해야했다.
따라서, 서버에서 가져온 알림 데이터를 atom으로 관리하였고 유저가 액션을 취했을 때 atom의 값을 selector를 사용하여 변경하고 바로 UI를 업데이트하는 방식으로 구현하였다.
먼저, 알림 데이터를 담을 atom
을 생성한다.
import { atom, selector } from 'recoil';
export const notificationState = atom({
key: 'notificationState',
default: '',
});
각 알림을 삭제 했을 때 해당 알림을 제거하는 selector
// 알림 삭제
export const deleteNotiSelector = selector({
key: 'deleteNotiSelector',
get: ({ get }) => get(notificationState),
set: ({ set, get }, notifyId) => {
const prevNotifications = get(notificationState);
const pages = prevNotifications.pages;
const updatedNotifications = pages.map((page) => ({
...page,
page_data: page.page_data.filter((item) => item.appnotify_id !== notifyId),
}));
const updateData = {
pages: updatedNotifications,
pageParams: prevNotifications.pageParams,
};
set(notificationState, updateData);
},
});
전체 알림 읽기 버튼을 클릭했을 때 selector
export const allReadNotiSelector = selector({
key: 'allReadNotiSelector',
get: ({ get }) => get(notificationState),
set: ({ set, get }) => {
const prevNotifications = get(notificationState);
const updatedNotifications = prevNotifications.pages.map((page) => ({
...page,
page_data: page.page_data.map((item) => ({
...item,
read_status: 'Y',
})),
}));
const updateData = {
pages: updatedNotifications,
pageParams: prevNotifications.pageParams,
};
set(notificationState, updateData);
},
});
전체 알림 삭제 버튼을 눌렀을 때 전체 알림을 삭제하는 selector
// 알림 전체 삭제
export const allDeleteNotiSelector = selector({
key: 'allDeleteNotiSelector',
get: ({ get }) => get(notificationState),
set: ({ set, get }) => {
set(notificationState, {
pages: [
{
current_page: 1,
isLast: true,
page_data: [],
},
],
pageParams: [null],
});
},
});
뷰 컴포넌트에서는 아래와 같이 사용한다.
/** 알림 삭제 (전체) */
export const useAllDeleteNotification = () => {
return useMutation(async () => {
await client.post(`${V3_BASE_URL}/notify/all_delete`);
});
};
const AlarmComponent = () => {
const setAllDeleteNotificationState = useSetRecoilState(allDeleteNotiSelector);
const allDeleteNotification = useAllDeleteNotification();
const onAllDelete = () => {
// 클라이언트 딴에서 먼저 데이터 업데이트 (UI 업데이트)
setAllDeleteNotificationState();
// 알림 전체 삭제 API 호출 (비동기 뮤테이션)
allDeleteNotification.mutateAsync();
}
return (
<div>
<button onClick={onAllDelete}>전체 삭제</button>
</div>
)
}
Tanstack query
에서는 낙관적 업데이트를 지원한다. 쿼리 객체의 onMutate
옵션을 사용해서 API 호출 전 데이터를 업데이트하는 방식도 적용하면 좋을 것 같다. 그렇게 되면 굳이 서버 딴에서 가져온 알림 데이터를 클라이언트 딴으로 넘기지 않아도 될 것이다.
Recoil - Selectors
Recoil 정확하게 사용하기! (feat. Selector)
Recoil 정리 1편 - Recoil시작하기 (atom, selector)
recoil의 selector 왜 쓰고 어떻게 쓰는 걸까?