React Native에서 Recoil을 도입하여 리팩터링 하기

김민태·2023년 2월 23일
post-thumbnail

실무에서 있던 리팩터링에 대한 기록입니다.
Recoile 상태관리 라이브러리를 도입해 React Native로 개발된 소스를 개선시킨 과정에 대해 다룹니다.


1. 배경

당시 인계받은 프로세스는 사용자가 하나의 신청을 완료하기 위해 여러 입력 화면을 거치고, 다양한 인증 절차를 진행해야하는 기능이었습니다. 그러나 기존 코드베이스는 화면 간에 전달되는 상태 데이터가 많아 복잡성이 높아지고, 유지보수 또한 어려운 상태였습니다. 특히, 인증 절차나 입력 검증과 같은 필수 기능이 여러 화면에 분산되어 있어, 수정이나 추가 작업이 복잡하고 시간이 많이 소요되었습니다.

2. 문제점 분석

사진의 diff 처럼 이전에는 다양한 자료형의 상태를 각 화면 간에 props로 넘기면서 다음과 같은 문제점들을 만들어 냈었습니다:

  • 상태 전달의 어려움: 여러 입력 화면과 인증 절차를 거치는 동안 상태를 일관성 있게 유지하기 위해 많은 데이터를 전달해야 했습니다.
  • 코드 복잡성 증가: 상태와 인증 정보가 여러 컴포넌트에서 관리되다 보니 중복된 코드와 불필요한 복잡도가 증가했습니다. 특히, 인증 정보의 상태가 제대로 전달되지 않으면 오류가 발생하는 경우도 많았습니다.
  • 유지보수 어려움: 코드의 구조가 복잡해지면서 새로운 요구사항이 생길 때마다 많은 코드를 수정해야 했으며, 이는 개발 속도를 저하시킬 뿐만 아니라 다른 사람이 급하게 작업을 하거나 참고할때 읽고 이해하기에 어려움이 있었습니다.

3. 해결 방법 - Recoil 도입

제가 인계받은 프로세스에서는 상태 관리가 별도로 적용되지 않았으며, 화면 간 데이터를 직접 전달하는 방식으로 이루어졌습니다. 다른 프로세스에서는 Context API를 사용하여 전역 상태를 관리하고 있었지만
이는 상태가 변경되면 해당 상태를 구독하는 모든 컴포넌트가 리렌더링되는 문제가 있었습니다. 특히, 여러 화면에서 상태를 공유하고, 성능과 유지보수성을 고려해야 하는 상황에서 Recoil이 더 나은 선택이었습니다.
그렇기 때문에 Context API 대신 Recoil을 도입하고 추후 다른 전역 상태 기능들도 Recoil로 교체하는것이 효율면에서 적합하다고 판단했습니다.

Recoil을 도입해 기대한 효과는 다음과 같습니다:

  • 간단한 API: Recoil은 훅과의 연계를 통해서 리듀서, 액션, 스토어를 설정하고 dispatch로 상태를 변경하는 구조인 Redux보다도 더 간결하고 직관적인 API를 제공해, 상태 관리를 보다 쉽게 할 수 있었습니다.
  • 효율적인 상태 관리: Context API와 달리, Recoil은 상태 변화가 필요한 구독 중인 컴포넌트만 리렌더링하므로 성능 향상에 기여했습니다.
  • 동적인 상태 관리: Recoil을 통해 상태를 중앙에서 유연하게 관리할 수 있으며, 각 컴포넌트는 필요한 상태만 구독할 수 있습니다. 이로 인해 상태 전달의 복잡성도 크게 줄었습니다.

4. Recoil 도입 과정

이 항목에서는 Recoil을 도입하는 과정을 단계별로 설명하겠습니다. Recoil 설정부터 아톰 정의, 커스텀 훅 작성까지의 순서를 통해, Recoil을 어떻게 적용했는지 보여드립니다.

Recoil 도입 절차

Recoil을 도입하여 상태 관리를 개선한 과정은 크게 Recoil 설정, 아톰(atom) 정의, 커스텀 훅 정의의 세 가지 주요 단계로 나뉩니다.

1. Recoil 설정

먼저, Recoil의 상태 관리 기능을 사용하기 위해 애플리케이션의 최상단에 RecoilRoot를 추가했습니다. 이 작업은 Recoil에서 제공하는 전역 상태 관리를 설정하는 필수 단계입니다.

import { RecoilRoot } from 'recoil';

function App() {
  return (
    <RecoilRoot>
      {/* 다른 컴포넌트들 */}
    </RecoilRoot>
  );
}

2. 아톰(atom) 정의

상태를 관리하기 위해 아톰(atom)을 정의했습니다. depositState라는 아톰을 정의하여 다양한 계좌 개설 관련 상태 정보를 관리하게 했습니다. 이 상태는 전역적으로 사용되며, productInfo, serial, productBranch 등 계좌 개설에 필요한 정보를 포함하고 있습니다.

import { atom } from 'recoil';

export interface DepositState {
  productInfo: {
    type: string;
    code: string;
    name: string;
  };
  serial: string;
  productBranch: string;
  ordinaryAccountNumber: string;
  isOrdinary: boolean;
  recommenderId: string;
}

export const depositState = atom<DepositState>({
  key: 'depositState',
  default: {
    productInfo: {
      type: '',
      code: '',
      name: '',
    },
    serial: '',
    productBranch: '',
    isOrdinary: false,
    ordinaryAccountNumber: '',
    recommenderId: '',
  },
});

3. 커스텀 훅 정의

상태 관리 로직을 더 깔끔하게 정리하고, 재사용 가능하게 만들기 위해 커스텀 훅 useDepositData를 정의했습니다. 이 훅은 Recoil 상태를 읽고 업데이트하는 역할을 하며, 추가적으로 계좌 개설에 필요한 로직(예: 상품 분류, 계좌 정보 설정, 초기화 등)을 처리합니다. 또한, 상품 분류 타입 값을 결정하는 함수는 주석으로 설명을 대체했습니다.

import { useRecoilState, useResetRecoilState } from 'recoil';
import { DepositState, depositState } from '~/states/depositState';

export default function useDepositData() {
  const [deposit, setDeposit] = useRecoilState<DepositState>(depositState);
  const reset = useResetRecoilState(depositState);

  // 상품 분류 타입값을 결정하는 함수
  const _depositBranch = (type: string) => {
    // 상품 타입에 따라 분류값을 결정하는 로직
    return '';  // 실제 로직은 주석으로 대체
  };

  const setInitialDepositState = (props: DepositState) => {
    setDeposit({
      ...deposit,
      productInfo: {
        type: props?.productInfo?.type,
        code: props?.productInfo?.code,
        name: props?.productInfo?.name,
      },
      productBranch: _depositBranch(props?.productBranch),
      isOrdinary: props?.isOrdinary,
      ordinaryAccountNumber: props?.ordinaryAccountNumber,
      recommenderId: props?.recommenderId,
      serial: '',
    });
  };

  const setSerial = (serial: string) => {
    setDeposit({
      ...deposit,
      serial: serial,
    });
  };

  const resetDeposit = () => {
    reset();
  };

  return { deposit, setInitialDepositState, setSerial, resetDeposit };
}

5. Recoil 도입 전후: 비포 & 애프터

이 항목에서는 Recoil 도입 전후의 상태 관리 방식을 세 가지 예시로 비교하여 설명하겠습니다. Recoil을 통해 코드 구조와 상태 관리가 어떻게 개선되었는지, 구체적인 예시를 통해 보여드립니다.

1. 비포 & 애프터: 상태 전달 방식의 개선

비포:

Recoil을 도입하기 전에는 여러 화면 간에 데이터를 주고받기 위해, 각 화면에서 필요한 데이터를 계속 전달하는 방식이었습니다.

// 비포: 데이터를 props로 전달
const DepositApplyPolicyScreen = ({navigation, route}) => {
  const {
    productInfo,
    productBranch,
    isFsbRegist = false,
    isOrdinary,
    ordinaryAccountNo,
    recommenderId,
  } = route.params || {};

  const _onPress = async () => {
    navigation.replace(DepositApplyScreens.Info, {
      productInfo: productInfo,
      productBranch: productBranch,
      serial: 'serial',
      identityData: 'result',
      isFsbRegist: false,
      isOrdinary: false,
      ordinaryAccountNo: ordinaryAccountNo,
    });
  };
};
  • 문제점: 데이터를 여러 화면에서 일관성 있게 관리하기 위해 직접

    전달하다 보니 코드가 복잡해지고 유지보수성이 떨어졌습니다.

애프터:

Recoil의 useDepositData 훅을 도입하여 전역 상태 관리를 통해 상태 전달을 간소화했습니다.

// 애프터: useDepositData 훅을 통한 전역 상태 관리
import useDepositData from '~/hooks/useDeposit';

const DepositApplyPolicyScreen = ({navigation, route}) => {
  const {deposit, setSerial} = useDepositData() || {};

  const _onPress = async () => {
    setSerial('serial');
    navigation.replace(DepositApplyScreens.Info, {
      identityData: 'result',
      isFsbRegist: false,
    });
  };
};
  • 개선점: useDepositData 훅을 통해 전역 상태로 관리하면서, 화면 간 상태 전달 과정이 간결해졌습니다.

2. 비포 & 애프터: 상태 변경 로직의 개선

비포:

상태 변경 시 직접 데이터를 설정하고 전달하는 방식이 중복되었으며, 이로 인해 수정 시 여러 곳에서 변경이 필요했습니다.

// 비포: 직접 상태를 설정하고 props로 전달
const _fetchSerial = async () => {
  const response = await DepositService.fetchSerialNumber(
    productInfo?.type,
    productInfo?.code,
    recommenderId,
  );
  if (response.status === 'OK') {
    setSerial(response.data.serial);
    navigation.replace(DepositApplyScreens.Info, {
      productInfo: productInfo,
      productBranch: productBranch,
      serial: response.data.serial,
    });
  }
};

애프터:

Recoil을 통해 상태 변경 로직을 일원화하고, 각 상태를 중앙에서 관리하도록 변경했습니다.

// 애프터: Recoil 훅을 통한 상태 관리
const _fetchSerial = async () => {
  const response = await DepositService.fetchSerialNumber(
    deposit?.productInfo?.type,
    deposit?.productInfo?.code,
    deposit?.recommenderId,
  );
  if (response.status === 'OK') {
    setSerial(response.data.serial);
    navigation.replace(DepositApplyScreens.Info, {
      identityData: 'result',
    });
  }
};
  • 개선점: Recoil을 통해 상태 변경 로직을 중앙화하고, 코드 중복을 제거하여 유지보수성을 높였습니다.

3. 비포 & 애프터: 컴포넌트 간 상태 전달 개선

비포:

Recoil을 도입하기 전에는, 상태 데이터를 자식 컴포넌트로 전달하기 위해 props를 통해 모든 데이터를 내려줘야 했습니다.

// 비포: 데이터를 props로 자식 컴포넌트에 전달
const DepositApplyPolicyScreen = ({navigation, route}) => {
  const {productInfo} = route.params || {};

  return (
    <View>
      <DepositTitle productType={productInfo?.type} />
    </View>
  );
};

// 자식 컴포넌트에서 props 사용
const DepositTitle = ({productType}) => {
  return <Text>{productType} 상품 개설</Text>;
};

애프터:

Recoil을 통해 전역 상태에서 직접 데이터를 사용하도록 변경하여, 더 이상 props로 데이터를 전달할 필요가 없었습니다.

// 애프터: Recoil 훅을 통해 전역 상태에서 직접 값 사용
import useDepositData from '~/hooks/useDeposit';

const DepositApplyPolicyScreen = ({navigation}) => {
  return (
    <View>
      <DepositTitle />
    </View>
  );
};

// 자식 컴포넌트에서 전역 상태 사용
const DepositTitle = () => {
  const {deposit} = useDepositData();
  return <Text>{deposit?.productInfo?.type} 상품 개설</Text>;
};
  • 개선점: props를 사용하지 않고 Recoil에서 직접 상태를 가져다 씀으로써 컴포넌트 간 결합도가 줄어들고, 유지보수성이 개선되었습니다.

6. 결과 및 성과

결론적으로, Context API 대신 Recoil을 도입한 것은 상태를 더 효율적으로 관리하고, 코드의 복잡성을 줄이며 성능을 최적화하는 데 큰 도움이 되었으며 이런 효과를 얻을 수 있었습니다:

  • 화면 간 상태 전달 방식이 간소화되면서 코드가 훨씬 깔끔해졌습니다.
  • 복잡했던 상태 관리가 단순화되어, 여러 입력 화면과 인증 절차 간의 상태 전달 문제를 해결했습니다.
  • 상태가 중앙에서 일관성 있게 관리되므로, 새로운 기능을 추가하거나 기존 인증 절차를 수정하는 데 필요한 코드 수정량이 크게 줄어들었습니다.
  • 코드 구조가 명확해져, 다른 개발자들도 쉽게 이해하고 협업할 수 있는 환경이 조성되었습니다.

0개의 댓글