끄적이는 개발노트

React Native - 전역 상태 관리 (Recoil) 본문

React Native

React Native - 전역 상태 관리 (Recoil)

크런키스틱 2025. 2. 6. 22:55
728x90

state를 사용할 때, 간단한 modal 혹은 하위 컴포넌트를 구성한다면 props가 오히려 깔끔하고 간단한 코드가 된다. 하지만 다양한 기능 혹은 컴포넌트들을 구현하다보면 넘겨주고자하는 state가 하위 컴포넌트에 단지 값을 전달하기 위해 중간 컴포넌트에 props로 넘겨주는 경우를 마주치기도 했었다. 이게 너무 지저분해보였고, 에러발생 혹은 유지보수를 하기 위해 며칠이 지난 뒤 코드를 살펴보면 내가 짠 코드임에도 props를 추적하기가 어지러운 경우도 발생했었다.

이런 경험을 몇번 한 뒤로, 본인은 개발을 할 때 주로 state(상태)를 나누어 관리하는 것을 선호하고, prop drilling을 피하려고 노력하는 편이다.

아직 많이 부족한 개발자다보니 아래 방식이 최선의 방식인지는 늘 고민하고 있으며, 여러 글을 참고하며 배우려 노력 중이다!

  1. 전역상태 : Recoil
  2. 지역상태 : useState, react-hook-form 등
  3. 서버상태 : react-query

이 중, 오늘은 전역 상태 관리인 Recoil 사용법을 간단히 정리한다.

 

Recoil?
- 페이스북에서 제작한 상태 관리 라이브러리
- React를 위한 상태 관리 라이브러리
- atoms(공유 상태)에서 selectors(순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow 형태

Atoms?
- 상태의 단위
- 업데이트와 구독이 가능
- 업데이트되면 해당 상태를 구독하는 컴포넌트는 새로운 값을 반영하여 다시 렌더링
- 고유 키 필요

Selectors?
- atoms나 다른 selectors를 입력으로 받아들이는 순수 함수
- atoms처럼 구독이 가능
- 상위 atoms나 selectors가 업데이트되면 하위의 selector도 재실행
- 상태를 기반으로 하는 파생 데이터를 계산하는데 사용

 

 

1. 설치

npm install recoil
#or
yarn add recoil

 

 

2. RecoilRoot

Root 컴포넌트에 RecoilRoot로 감싸주어야 한다.

React Native에서 Root 컴포넌트는 App.tsx로 아래와 같이 코드를 작성한다.

// App.tsx

import './global.css';
import {RecoilRoot} from 'recoil';

const App = () => {
  return (
    <RecoilRoot>
      ...
    </RecoilRoot>
  );
};

export default App;

 

 

3. atoms

src/recoil 폴더를 만들고 그 안에 파일을 만들어 아래와 같이 전역상태를 선언한다.

// count.tsx

import {atom} from 'recoil';

export const countState = atom<number>({
  key: 'countState',
  default: 0,
});

 

 

4. 사용

가장 기본적인 사용법은 useRecoilState로 useState를 사용하듯이 하면서 선언한 atoms 값을 넣어주면 된다.

그 외에 값만을 띄우는게 목적이라면 useRecoilValue를 사용하면 되고,

값을 변경하는 setState만 목적이라면 useSetRecoilState를 사용하면 된다.

// home.tsx

import CustomText from '@/components/text';
import {countState} from '@/recoil/count';
import {HomeProps} from '@/types/stack';
import {Button, Text, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {useRecoilState} from 'recoil';

const HomeScreen = ({navigation}: HomeProps) => {
  const [count, setCount] = useRecoilState(countState);

  return (
    <SafeAreaView className="flex-1">
      <View className="w-full h-full flex justify-center items-center bg-white">
        <CustomText className="text-success text-xl" children="홈 화면" />
        <View className="w-full flex flex-row justify-center items-center">
          <Button
            title="-"
            onPress={() => {
              setCount(count - 1);
            }}
          />
          <Text className="text-success text-xl mx-4">{count}</Text>
          <Button
            title="+"
            onPress={() => {
              setCount(count + 1);
            }}
          />
        </View>
      </View>
    </SafeAreaView>
  );
};

export default HomeScreen;
// detail.tsx

import CustomText from '@/components/text';
import {countState} from '@/recoil/count';
import {DetailProps} from '@/types/stack';
import {Button, Text, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {useRecoilValue} from 'recoil';

const DetailScreen = ({navigation}: DetailProps) => {
  const count = useRecoilValue(countState);

  return (
    <SafeAreaView className="flex-1">
      <View className="w-full h-full flex justify-center items-center bg-white">
        <CustomText className="text-success text-xl" children="상세정보 화면" />
        <Button title="테스트" onPress={() => navigation.navigate('Test')} />
        <Text className="text-success text-xl">{count}</Text>
      </View>
    </SafeAreaView>
  );
};

export default DetailScreen;

 

 

5. selector

atoms를 직접 변경할 경우, 해당 atoms를 바라보고 있는 모든 컴포넌트에서 재렌더링이 일어난다.

이는 예상치 못한 부작용을 발생할 수 있다. 이를 방지하기 위해 사용되는 것이 selector로 atom을 직접 변경하지 않기 때문에 불변성을 유지할 수 있고, 여러 atom을 가공해서 사용해야하는 경우에 유용하다. 또한, 필요한 atom만을 구독하기 때문에 의존성 관리에 효율적이다.

 

  • get : 파생된 상태, atom을 구독하여 그에 따라 selector의 값이 변함
  • set : 쓰기 가능한 상태, 다른 값으로 변경 가능

예시로, count에 10배를 하는 값이 필요하다면 아래와 같이 설정할 수 있다.

count에 따라 변화하는 값만이 필요한 것이라면 get을 통해 atoms에 따라 값이 변화하게끔만 설정해주면 Read-only로 사용할 수 있다.

해당 값도 변경을 하고 그 값에 따라 atom도 변경하기를 원한다면 set을 통해 값을 변경하면 된다.

// count.tsx

import {atom, selector} from 'recoil';

export const countState = atom<number>({
  key: 'countState',
  default: 0,
});

export const plusTenState = selector({
  key: 'plusTenState',
  get: ({get}) => get(countState) * 10,
  set: ({set}, plusTen) => set(countState, Number(plusTen) / 10),
});
// detail.tsx

import CustomText from '@/components/text';
import {countState, tenTimesState} from '@/recoil/count';
import {DetailProps} from '@/types/stack';
import {Button, Text, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {useRecoilState, useRecoilValue} from 'recoil';

const DetailScreen = ({navigation}: DetailProps) => {
  const count = useRecoilValue(countState);
  const [tenTimes, setTenTimes] = useRecoilState(tenTimesState);

  return (
    <SafeAreaView className="flex-1">
      <View className="w-full h-full flex justify-center items-center bg-white">
        <CustomText className="text-success text-xl" children="상세정보 화면" />
        <Button title="테스트" onPress={() => navigation.navigate('Test')} />
        <Text className="text-success text-xl">{count}</Text>
        <View className="w-full flex flex-row justify-center items-center">
          <Button
            title="-"
            onPress={() => {
              setTenTimes(tenTimes - 10);
            }}
          />
          <Text className="text-success text-xl">{tenTimes}</Text>
          <Button
            title="+"
            onPress={() => {
              setTenTimes(tenTimes + 10);
            }}
          />
        </View>
      </View>
    </SafeAreaView>
  );
};

export default DetailScreen;

 

 

6. 실행

실행해보면 아래와 같이 state가 잘변경되고 변경된 state가 다른 컴포넌트에서도 표시되는 것을 확인할 수 있다.

또한, atoms와 selector의 변화도 확인할 수 있다.

728x90