Recoil Selector를 사용해서 Side Effect 줄이기

Singsoong·2024년 4월 22일
2

react

목록 보기
7/7
post-thumbnail

개발을 하다 보면 초반에 잘 설계를 해도 여러 개발자들이 협업을 하다 보면 언젠간 부작용(Side Effects)이 발생하기 마련이다.
프론트엔드에서 부작용을 일으키기 쉬운 부분인 Client State Management 에서 Recoil을 사용할 때 부작용을 최소화 할 수 있는 방법에 대해 작성했다.

📌 Selector 개념

Selector는 Recoil에서 제공하고 있는 순수함수의 개념이다. 동일한 Parameter를 제공하면 동일한 값을 Return 해준다.

Selectoratom에서는 불가능한 비동기 처리와 복잡한 로직을 구현할 수 있다. 공식문서에는 아래와 같이 나와있다.

Selector는 파생된 상태(derived state)의 일부를 나타낸다.
파생된 상태를 어떤 방법으로든 주어진 상태를 수정하는 순수 함수에 전달된 상태의 결과물로 생각할 수 있다.

📌 Selector 사용하는 이유

Selector를 사용하지 않고 atom을 직접 변경하게 되면, 해당 atom을 바라보고 있는 모든 컴포넌트에서 상태가 변경이 되어 예상치 못한 사이드 이펙트가 발생하게 된다.

Selector를 사용하면 atom을 직접 변경하지 않기 때문에 불변성을 유지할 수 있고, 특히 여러 atom을 가공해 사용해야 하는 겨우 더욱 유용하다.

Selector는 필요한 atom만 구독하여 의존성 관리에 효율적이다.

📌 Selector 사용

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)
    }
})
  • key: atom의 key와 동일, 프로젝트 전체에서 고유한 문자열을 가져야 함
  • get: 파생된 상태를 반환하는 곳
get: ({ get }): number => {
        // countState를 구독하고 있습니다.
        // countState가 바뀔 때마다 1증가 시켜서 반환합니다.
        const count = get(countState);
        return count + 1;
    },
  • set: set 없이 get만 제공되면 selector는 read-only한 상태이지만, set을 제공하면 쓰기 가능한 상태를 반환한다.
    set은 selector의 값을 수정하는 것이 아닌 수정 가능한 atom의 값을 바꿔준다.
set: ({ set, get }, newCount)=>{
    	// 설명을 위한 코드로,
        // 현재 count는 사용하고 있지 않습니다.
    	const count = get(countState);
        
        return set(countState, newCount + 10)
    }

set은 두가지 매개변수를 받는데, 첫번째는 Recoil의 상태, 두번째는 어떤 값으로 바뀔 것인지 새로운 값을 넣는다.

  • 비동기 Selector: atom을 직접 사용할 때 비동기 통신을 하는게 아닌 아래 코드와 같이 selector 안에서 비동기 통신 후 값을 사용할 수 있다.
const myQuery = selector({
  key: 'MyQuery',
  get: async ({ get }) => {
    return await myAsyncQuery(get(queryParamState));
  },
});

📌 Selector 활용

📕 배경

알림을 구현하면서 낙관적 업데이트(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 왜 쓰고 어떻게 쓰는 걸까?

profile
Frontend Developer

0개의 댓글