본문으로 건너뛰기

React Native 글로벌 상태 관리 라이브러리

· 약 21분
Dongmin Yu

리액트 네이티브의 글로벌 상태 관리 라이브러리

React Native 모바일 애플리케이션에서 주로 사용되는 글로벌 상태 관리 라이브러리로는 다음과 같은 것들이 있습니다:

  1. Redux: 가장 널리 사용되는 상태 관리 라이브러리 중 하나로, 전역 상태를 관리하는 중앙 저장소를 제공합니다. Redux는 단방향 데이터 흐름을 가지며, 상태 변경은 순수 함수인 리듀서를 통해 수행됩니다. Redux의 장점으로는 예측 가능한 상태 변경, 개발 도구 지원, 커뮤니티 지원 등이 있습니다. 단점으로는 초기 설정이 복잡하고, 보일러플레이트 코드가 많다는 점이 있습니다.
  2. MobX: 관찰 가능한 상태 트리를 사용하여 상태 관리를 수행하는 라이브러리입니다. MobX는 자동으로 상태 변경을 추적하고 필요한 컴포넌트만 리렌더링합니다. MobX의 장점으로는 간단한 API, 높은 성능 등이 있습니다. 단점으로는 디버깅이 어렵다는 점이 있습니다.
  3. Context API: React에 내장된 상태 관리 기능으로, 전역 상태를 관리할 수 있습니다. Context API는 중첩된 컴포넌트 구조에서 상위 컴포넌트의 상태를 하위 컴포넌트에 전달하는 데 유용합니다. Context API의 장점으로는 추가적인 라이브러리 설치가 필요 없다는 점이 있습니다. 단점으로는 복잡한 상태 관리에 적합하지 않다는 점이 있습니다.
  4. Recoil: Facebook에서 개발한 상태 관리 라이브러리로, Atom과 Selector라는 개념을 사용하여 상태 관리를 수행합니다. Recoil은 React의 기능과 밀접하게 연결되어 있으며, 복잡한 상태 의존성을 쉽게 관리할 수 있습니다. Recoil의 장점으로는 간단한 API, 높은 성능 등이 있습니다.
  5. React Query: 서버 데이터를 캐싱하고 동기화하는 데 사용되는 라이브러리입니다. React Query는 서버 데이터와 관련된 로직을 쉽게 구현할 수 있도록 도와줍니다. React Query의 장점으로는 간단한 API, 자동 캐싱 및 리패칭 등이 있습니다.

각 라이브러리마다 특징과 장단점이 다르므로 프로젝트의 요구사항에 따라 적합한 라이브러리를 선택하는 것이 좋습니다.

References

  1. 리액트 상태 관리 가이드 | Stevy's wavyLog
  2. [React] 상태관리 라이브러리 어떤걸 써야할까?
  3. React Native UI 개발 시작하기 | WIT블로그
  4. 상태관리 라이브러리, 뭘 쓸까?

리덕스와 리코일

Redux와 Recoil 모두 Facebook에서 개발한 상태 관리 도구입니다. Redux는 Flux 아키텍처를 기반으로 한 상태 관리 라이브러리로, 전역 상태를 중앙 저장소에서 관리합니다. Redux는 단방향 데이터 흐름을 가지며, 상태 변경은 순수 함수인 리듀서를 통해 수행됩니다. Redux의 장점으로는 예측 가능한 상태 변경, 개발 도구 지원, 커뮤니티 지원 등이 있습니다. 단점으로는 초기 설정이 복잡하고, 보일러플레이트 코드가 많다는 점이 있습니다. Recoil은 Facebook에서 최근 개발한 상태 관리 라이브러리로, Atom과 Selector라는 개념을 사용하여 상태 관리를 수행합니다. Recoil은 React의 기능과 밀접하게 연결되어 있으며, 복잡한 상태 의존성을 쉽게 관리할 수 있습니다. Recoil의 장점으로는 간단한 API, 높은 성능 등이 있습니다. Redux와 Recoil의 가장 큰 차이점은 상태 관리 방식입니다. Redux는 중앙 저장소에서 전역 상태를 관리하는 반면, Recoil은 Atom과 Selector를 사용하여 상태 관리를 수행합니다. 또한 Recoil은 React의 기능과 밀접하게 연결되어 있어서 React와의 호환성이 높습니다.

References

  1. Redux 와 Recoil 비교
  2. [TIL] Redux와 Recoil 비교
  3. 2021년 React 상태 관리 라이브러리 전쟁 : Hooks, Redux, Recoil
  4. Redux vs Recoil - velog

리코일의 Atom과 Selector

Recoil은 Atom과 Selector라는 개념을 사용하여 상태 관리를 수행합니다. Atom은 상태의 단위로, 업데이트와 구독이 가능합니다. 컴포넌트는 Atom을 구독하고, Atom이 업데이트되면 구독한 컴포넌트만 리렌더링됩니다. Selector는 Atom이나 다른 Selector를 입력으로 받는 순수 함수로, 상태를 기반으로 데이터를 계산합니다. Recoil은 전역 상태 관리를 위한 라이브러리로, 각 컴포넌트나 페이지 레벨에서 상태 관리를 수행할 수 있습니다. Recoil을 사용하면 전역 상태를 쉽게 관리할 수 있으며, 복잡한 상태 의존성도 쉽게 관리할 수 있습니다. Recoil에서 Atom과 Selector는 상태 관리를 수행하는 데 사용되는 개념입니다. Atom은 상태의 단위로, 업데이트와 구독이 가능합니다. Atom은 고유한 키를 가지며, 이 키는 전역적으로 고유해야 합니다. 컴포넌트는 Atom을 구독하고, Atom이 업데이트되면 구독한 컴포넌트만 리렌더링됩니다. Atom은 useRecoilState라는 훅을 사용하여 읽고 쓸 수 있습니다. Selector는 Atom이나 다른 Selector를 입력으로 받는 순수 함수로, 상태를 기반으로 데이터를 계산합니다. Selector는 상위 Atom이나 Selector가 업데이트될 경우 하위 Selector도 재실행됩니다. 컴포넌트는 Atom 뿐만 아니라 Selector도 구독할 수 있으며, 구독하고 있는 Selector가 변경되면 구독한 컴포넌트도 리렌더링됩니다. Selector는 useRecoilValue라는 훅을 사용하여 조회할 수 있습니다. 간단히 말하면, Atom은 상태의 단위이며 업데이트와 구독이 가능하고, Selector는 상태를 기반으로 데이터를 계산하는 순수 함수입니다. Recoil을 사용하여 Pedometer 데이터를 관리하는 타입스크립트 리액트 네이티브 예제는 다음과 같습니다:

import React, { useEffect } from "react";
import { View, Text } from "react-native";
import { atom, useRecoilState } from "recoil";
import { Pedometer } from "expo-sensors";
// Atom 정의
const stepsState = atom({
  key: "stepsState",
  default: 0,
});
const PedometerComponent = () => {
  const [steps, setSteps] = useRecoilState(stepsState);
  useEffect(() => {
    // Pedometer 데이터 구독
    Pedometer.watchStepCount((result) => {
      setSteps(result.steps);
    });
    return () => {
      // 구독 취소
      Pedometer.stopWatchingStepCount();
    };
  }, []);
  return (
    <View>
            <Text>Steps: {steps}</Text>   {" "}
    </View>
  );
};
export default PedometerComponent;

위 예제에서는 stepsState라는 Atom을 정의하고 있습니다. 이 Atom은 걸음 수를 저장하는 데 사용됩니다. PedometerComponent 컴포넌트에서는 useRecoilState 훅을 사용하여 stepsState Atom의 값을 읽고 쓸 수 있습니다. useEffect 훅을 사용하여 Pedometer 데이터를 구독하고, 데이터가 변경될 때마다 setSteps 함수를 호출하여 stepsState Atom의 값을 업데이트합니다. 이 예제에서는 Recoil의 상태 관리 시스템이 어떻게 작동하는지 확인할 수 있습니다. 사용자가 걸어갈 때마다 네이티브 이벤트 에밋터로부터 Pedometer 데이터를 수신하고, 변경된 데이터를 Recoil로 업데이트합니다. Recoil의 useRecoilState 훅은 React의 useState 훅과 유사한 인터페이스를 가지고 있습니다. 그러나 Recoil의 상태 관리는 전역적으로 수행되며, 상태 변경 사항은 동일한 컴포넌트 뿐만 아니라 다른 컴포넌트에서도 적용됩니다. Recoil을 사용하면 전역 상태를 쉽게 관리할 수 있습니다. Atom을 정의하고, 컴포넌트에서 useRecoilState 훅을 사용하여 Atom의 값을 읽고 쓸 수 있습니다. Atom이 업데이트되면 구독한 모든 컴포넌트가 리렌더링됩니다. 따라서 Recoil의 상태 관리는 useState와 다르게 전역적으로 수행되며, 하위 컴포넌트에 데이터를 전달하는 등의 기능도 수행할 수 있습니다.

리코일에서 비동기 처리 수행하기

Recoil에서 비동기 처리를 수행하는 방법은 여러 가지가 있습니다. 가장 일반적인 방법은 Selector를 사용하는 것입니다. Selector는 Atom이나 다른 Selector를 입력으로 받는 순수 함수로, 상태를 기반으로 데이터를 계산합니다. Selector의 get 속성에는 계산될 함수를 정의할 수 있습니다. 이 함수에서 비동기 처리를 수행할 수 있습니다. 예를 들어, 다음과 같이 비동기 처리를 수행하는 Selector를 정의할 수 있습니다:

const currentUserIDState = atom({
  key: "CurrentUserID",
  default: null,
});
const currentUserNameQuery = selector({
  key: "CurrentUserName",
  get: async ({ get }) => {
    const response = await myDBQuery({
      userID: get(currentUserIDState),
    });
    return response.name;
  },
});

위 예제에서 currentUserNameQuery라는 Selector는 currentUserIDState Atom을 입력으로 받아서 비동기 처리를 수행합니다. get 함수를 사용하여 currentUserIDState Atom의 값을 조회하고, 이 값을 사용하여 데이터베이스 쿼리를 수행합니다. 컴포넌트에서는 useRecoilValueLoadable 훅을 사용하여 Selector의 상태와 값을 조회할 수 있습니다. 이 훅은 Loadable 객체를 반환하며, Loadable 객체의 상태에 따라 컴포넌트에서 적절한 처리를 수행할 수 있습니다. 예를 들어, 다음과 같이 컴포넌트에서 비동기 처리 상태를 표시할 수 있습니다:

function CurrentUserName() {
  const userNameLoadable = useRecoilValueLoadable(currentUserNameQuery);
  switch (userNameLoadable.state) {
    case "hasValue":
      return <div>{userNameLoadable.contents}</div>;
    case "loading":
      return <div>Loading...</div>;
    case "hasError":
      throw userNameLoadable.contents;
  }
}

위 예제에서 CurrentUserName 컴포넌트는 useRecoilValueLoadable 훅을 사용하여 currentUserNameQuery Selector의 상태와 값을 조회합니다. Loadable 객체의 상태에 따라 컴포넌트에서 적절한 처리를 수행합니다. 이 외에도 Recoil에서는 Suspense와 ErrorBoundary를 사용하여 비동기 처리 상태를 표시할 수도 있습니다. Suspense와 ErrorBoundary는 React에서 비동기 처리 상태를 표시하는 데 사용되는 컴포넌트입니다. Suspense는 컴포넌트가 로딩 중일 때 로딩 표시자를 표시하는 데 사용됩니다. Suspense 컴포넌트는 fallback prop을 사용하여 로딩 표시자를 지정할 수 있습니다. Suspense 컴포넌트의 자식 컴포넌트에서 비동기 처리가 수행되면, Suspense 컴포넌트는 자동으로 로딩 표시자를 표시합니다. 예를 들어, 다음과 같이 Suspense 컴포넌트를 사용하여 비동기 처리 상태를 표시할 수 있습니다:

import React, { Suspense } from "react";
function UserProfile({ userID }) {
  return (
    <Suspense fallback={<div>Loading...</div>}>
            <ProfileDetails userID={userID} />
            <ProfilePosts userID={userID} />   {" "}
    </Suspense>
  );
}

위 예제에서 UserProfile 컴포넌트는 Suspense 컴포넌트를 사용하여 비동기 처리 상태를 표시합니다. ProfileDetailsProfilePosts 컴포넌트에서 비동기 처리가 수행되면, Suspense 컴포넌트는 자동으로 로딩 표시자를 표시합니다. ErrorBoundary는 컴포넌트에서 발생한 오류를 처리하는 데 사용됩니다. ErrorBoundary 컴포넌트는 componentDidCatch 라이프사이클 메서드나 getDerivedStateFromError 정적 메서드를 구현하여 오류를 처리할 수 있습니다. 예를 들어, 다음과 같이 ErrorBoundary 컴포넌트를 사용하여 오류를 처리할 수 있습니다:

import React from "react";
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    // 오류 정보를 로깅
    logErrorToMyService(error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

위 예제에서 ErrorBoundary 컴포넌트는 getDerivedStateFromError 정적 메서드와 componentDidCatch 라이프사이클 메서드를 구현하여 오류를 처리합니다. 자식 컴포넌트에서 오류가 발생하면, ErrorBoundary 컴포넌트는 오류 정보를 로깅하고 오류 메시지를 표시합니다. Suspense와 ErrorBoundary는 Recoil에서도 사용할 수 있습니다. Recoil의 Selector에서 비동기 처리가 수행되면, Suspense와 ErrorBoundary 컴포넌트를 사용하여 비동기 처리 상태와 오류 상태를 쉽게 표시할 수 있습니다. Recoil에서 useRecoilStateuseRecoilValue는 Atom이나 Selector의 값을 조회하는 데 사용되는 훅입니다. useRecoilState 훅은 Atom이나 읽기/쓰기 가능한 Selector의 값을 조회하고 업데이트하는 데 사용됩니다. 이 훅은 [value, setValue] 형태의 배열을 반환하며, value는 Atom이나 Selector의 현재 값이고, setValue는 Atom이나 Selector의 값을 업데이트하는 함수입니다. 예를 들어, 다음과 같이 useRecoilState 훅을 사용하여 Atom의 값을 조회하고 업데이트할 수 있습니다:

import React from "react";
import { atom, useRecoilState } from "recoil";
const textState = atom({
  key: "textState",
  default: "",
});
function TextInput() {
  const [text, setText] = useRecoilState(textState);
  const onChange = (event) => {
    setText(event.target.value);
  };
  return (
    <div>
            <input type="text" value={text} onChange={onChange} />
            <br />      Echo: {text}   {" "}
    </div>
  );
}

위 예제에서 TextInput 컴포넌트는 useRecoilState 훅을 사용하여 textState Atom의 값을 조회하고 업데이트합니다. setText 함수를 호출하여 textState Atom의 값을 업데이트할 수 있습니다. useRecoilValue 훅은 Atom이나 Selector의 값을 조회하는 데 사용됩니다. 이 훅은 Atom이나 Selector의 현재 값을 반환합니다. 예를 들어, 다음과 같이 useRecoilValue 훅을 사용하여 Atom의 값을 조회할 수 있습니다:

import React from "react";
import { atom, useRecoilValue } from "recoil";
const textState = atom({
  key: "textState",
  default: "",
});
function TextDisplay() {
  const text = useRecoilValue(textState);
  return <div>Echo: {text}</div>;
}

위 예제에서 TextDisplay 컴포넌트는 useRecoilValue 훅을 사용하여 textState Atom의 값을 조회합니다. 간단히 말하면, useRecoilState 훅은 Atom이나 읽기/쓰기 가능한 Selector의 값을 조회하고 업데이트하는 데 사용되며 [value, setValue] 형태의 배열을 반환합니다. 반면에 useRecoilValue 훅은 Atom이나 Selector의 값을 조회하는 데 사용되며 Atom이나 Selector의 현재 값을 반환합니다. Recoil의 Selector는 기본적으로 읽기 전용입니다. Selector는 Atom이나 다른 Selector를 입력으로 받는 순수 함수로, 상태를 기반으로 데이터를 계산합니다. Selector의 get 속성에는 계산될 함수를 정의할 수 있습니다. 읽기 전용 Selector는 다음과 같이 정의할 수 있습니다:

const charCountState = selector({
  key: "charCountState",
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
  },
});

위 예제에서 charCountState라는 Selector는 textState Atom을 입력으로 받아서 문자열의 길이를 계산합니다. 이 Selector는 읽기 전용이므로 값을 조회하는 것만 가능합니다.

Writable and Read-Write Selectors

읽기/쓰기 가능한 Selector는 set 속성을 추가하여 정의할 수 있습니다. set 속성에는 값을 업데이트하는 함수를 정의할 수 있습니다. 읽기/쓰기 가능한 Selector는 다음과 같이 정의할 수 있습니다:

const uppercaseTextState = selector({
  key: "uppercaseTextState",
  get: ({ get }) => {
    const text = get(textState);
    return text.toUpperCase();
  },
  set: ({ set }, newValue) => {
    set(textState, newValue);
  },
});

위 예제에서 uppercaseTextState라는 Selector는 textState Atom을 입력으로 받아서 대문자로 변환된 문자열을 반환합니다. 이 Selector는 set 속성을 정의하고 있으므로 읽기/쓰기 가능합니다. 간단히 말하면, 읽기 전용 Selector와 읽기/쓰기 가능한 Selector의 차이점은 set 속성의 유무입니다. 읽기 전용 Selector는 get 속성만 정의하고 있으며 값을 조회하는 것만 가능합니다. 반면에 읽기/쓰기 가능한 Selector는 set 속성도 정의하고 있어서 값을 업데이트하는 것도 가능합니다.