Recoil 을 이용한 상태관리 (feat. useContext)

2024-05-05 18:00:30

기존 스노우볼 프로젝트는 useContext API 를 통해 상태를 공유하며 관리하였지만, Provider 가 데이터를 제공하는 하위 컴포넌트 모두 useState 의 상태 변경시 모든 컴포넌트가 리렌더링 되기 때문에 구조 설계에 불편함이 있었습니다.

또한 다른 라우팅에서 로그인 되어 있는 상태인지 확인하기 위한 API 요청으로 효율이 좋지 못하였고, 서버에서 무리한 요청을 막기위해 1초에 3번의 API 요청을 막아둔 설정에 자주 문제가 나타나게 되었습니다.

이로 인해 네이버 부스트캠프를 수료하고 계속해서 리팩토링을 진행하면서, 상태관리 라이브러리를 사용하기로 하였습니다.

Recoil

상태관리 라이브러리로 Recoil 을 선택하였는데, 새로 시작하는 프로젝트가 아닌, 기존 프로젝트를 리팩토링 하는과정에서 적합하다고 판단하였기 때문입니다.

그 이유는

  • 원자 (atom) 단위로 상태를 관리하여 전역적으로 상태를 공유해 다른 라우팅 depth 에서도 공유가 가능

  • React 와 hook API 가 유사하여 러닝커브가 낮고 직관적이라 유지보수가 용이

  • 리렌더링 최적화

  • Selector 를 통해 비동기 작업 직관적

이 밖에도 Redux, Zustand 등 여러 상태관리 라이브러리가 있었지만, 현재 프로젝트에는 가장 적합하다고 생각되어 Recoil 을 선택하게 되었습니다.

시작하기

1npm install recoil

라이브러리를 설치 후, RecoilRoot 상태를 제공하는 컴포넌트를 최상위 App 컴포넌트에 감싸줍니다.

1import { RecoilRoot } from 'recoil'; 
2const App = () => { 
3  return ( <RecoilRoot> {/* ...codes */} </RecoilRoot> ); 
4};

후에 하위 컴포넌트에서 공유하는 최소값 atom 을 설정하면 해당 값을 모든 컴포넌트에서 읽고 쓰기가 가능해집니다.

atom에 변화가 있다면 atom을 import한 모든 컴포넌트들이 리렌더링이 됩니다.

1//atom example
2
3import { atom } from 'recoil';
4
5export interface ContentTypes {
6  name: string;
7  status: boolean;
8  message: string;
9}
10
11export const contentState = atom<ContentTypes>({
12  key: 'content',
13  default: {
14    name: 'test',
15    status: false,
16    message: ''
17  }
18});

추가로 atom 에서 파생되어 의존성을 가질수 있는 selector 또한 다음과 같이 작성 할 수 있습니다.

1import { selector } from 'recoil';
2import { contentState, ContentTypes } from './atomFile';
3
4// contentState의 status 값을 기반으로 메시지를 반환하는 selector 정의
5export const statusSelector = selector<string>({
6  key: 'statusSelector',
7  get: ({ get }) => {
8    const content = get(contentState);
9
10    // content의 status 값에 따라 다른 메시지 반환
11    if (content.status) {
12      return 'Status is true!';
13    } else {
14      return 'Status is false!';
15    }
16  },
17});

위와 같이 atom 은 유지하면서 파생되는 데이터는 selector 로 데이터를 만들어 원자단위로 나누어 설계할 수 있습니다. 이로인해 프로젝트 볼륨이 커지고 많은 기능이 생기더라도, 적절하게 atom 을 설계하였다면 selector 를 사용하여 상태를 효율적으로 관리할 수 있습니다.

이때, selector 의 get 함수는 쓰지 않아도 구조 상 반드시 작성해주셔야 합니다.

Recoil 관련 함수

  • useRecoilState ➡️ atom 상태 설정 및 변경 가능, React 의 useState처럼 사용합니다.
  • useRecoilValue ➡️ atom 을 조회합니다.
  • useSetRecoilState ➡️ atom 을 변경합니다.
  • useResetRecoilState ➡️ atom값을 default 값으로 변경합니다.

따라서 스노우볼 프로젝트에서는 다음과 같이 리팩토링 하였습니다.

기존 useContext

1import React, { useState, createContext } from 'react';
2
3interface MyContextType {
4 message: string;
5 setMessage: React.Dispatch<React.SetStateAction<string>>;
6 color: string;
7 setColor: React.Dispatch<React.SetStateAction<string>>;
8 sender: string;
9 setSender: React.Dispatch<React.SetStateAction<string>>;
10 messageID: number;
11 setMessageID: React.Dispatch<React.SetStateAction<number>>;
12}
13
14const MessageContext = createContext<MyContextType>({
15 message: '',
16 setMessage: () => {},
17 color: '',
18 setColor: () => {},
19 sender: '',
20 setSender: () => {},
21 messageID: 0,
22 setMessageID: () => {}
23});
24
25const MessageProvider: React.FC<{ children: React.ReactNode }> = ({
26 children
27}) => {
28 const [message, setMessage] = useState<string>('');
29 const [color, setColor] = useState<string>('');
30 const [sender, setSender] = useState<string>('');
31 const [messageID, setMessageID] = useState<number>(0);
32 return (
33   <MessageContext.Provider
34     value={{
35       message,
36       setMessage,
37       color,
38       setColor,
39       sender,
40       setSender,
41       messageID,
42       setMessageID
43     }}
44   >
45     {children}
46   </MessageContext.Provider>
47 );
48};
49
50export { MessageProvider, MessageContext };

위 코드와 메세지 컴포넌트의 데이터를 하위 컴포넌트에 제공하였는데,

1import { atom } from 'recoil';
2
3interface MessageType {
4  message: string;
5  color: string;
6  sender: string;
7  messageID: number;
8}
9
10const MessageRecoil = atom<MessageType>({
11  key: 'Message',
12  default: {
13    message: '',
14    color: '',
15    sender: '',
16    messageID: 0
17  }
18});
19
20export { MessageRecoil };

Recoil 라이브러리의 함수를 통해 직관적으로 변경되고 리렌더링 최적화를 할 수 있었습니다.

또한, 다른 라우터에서도 useRecoilValue 함수를 사용해 전역 상태를 확인하여 로그인 되어 있는 유저인지 간단하게 확인해 API 요청수를 1/3 로 줄이게 되었습니다.

그래서 항상 Recoil 을 사용할까요 ?

recoil이 여러모로 편리하였지만, 한 객체 내에서 특정 속성값을 바꿀 때는 useContext가 편하였고, 특정 상태만을 공유하는 컴포넌트라면 useContext 가 디버깅시에 이점이 있을것 같습니다.

recoil의 경우 모든 컴포넌트에서 사용이 가능하여 후에 atom 값을 수정해야 할 일이 생길 경우 프로젝트 볼륨에 따라 디버깅하는데 어려움이 생길수 있다고 생각합니다.

Loading...