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

사진의 diff 처럼 이전에는 다양한 자료형의 상태를 각 화면 간에 props로 넘기면서 다음과 같은 문제점들을 만들어 냈었습니다:
제가 인계받은 프로세스에서는 상태 관리가 별도로 적용되지 않았으며, 화면 간 데이터를 직접 전달하는 방식으로 이루어졌습니다. 다른 프로세스에서는 Context API를 사용하여 전역 상태를 관리하고 있었지만
이는 상태가 변경되면 해당 상태를 구독하는 모든 컴포넌트가 리렌더링되는 문제가 있었습니다. 특히, 여러 화면에서 상태를 공유하고, 성능과 유지보수성을 고려해야 하는 상황에서 Recoil이 더 나은 선택이었습니다.
그렇기 때문에 Context API 대신 Recoil을 도입하고 추후 다른 전역 상태 기능들도 Recoil로 교체하는것이 효율면에서 적합하다고 판단했습니다.
Recoil을 도입해 기대한 효과는 다음과 같습니다:
Recoil은 훅과의 연계를 통해서 리듀서, 액션, 스토어를 설정하고 dispatch로 상태를 변경하는 구조인 Redux보다도 더 간결하고 직관적인 API를 제공해, 상태 관리를 보다 쉽게 할 수 있었습니다.Context API와 달리, Recoil은 상태 변화가 필요한 구독 중인 컴포넌트만 리렌더링하므로 성능 향상에 기여했습니다.이 항목에서는 Recoil을 도입하는 과정을 단계별로 설명하겠습니다. Recoil 설정부터 아톰 정의, 커스텀 훅 작성까지의 순서를 통해, Recoil을 어떻게 적용했는지 보여드립니다.
Recoil을 도입하여 상태 관리를 개선한 과정은 크게 Recoil 설정, 아톰(atom) 정의, 커스텀 훅 정의의 세 가지 주요 단계로 나뉩니다.
먼저, Recoil의 상태 관리 기능을 사용하기 위해 애플리케이션의 최상단에 RecoilRoot를 추가했습니다. 이 작업은 Recoil에서 제공하는 전역 상태 관리를 설정하는 필수 단계입니다.
import { RecoilRoot } from 'recoil';
function App() {
return (
<RecoilRoot>
{/* 다른 컴포넌트들 */}
</RecoilRoot>
);
}
상태를 관리하기 위해 아톰(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: '',
},
});
상태 관리 로직을 더 깔끔하게 정리하고, 재사용 가능하게 만들기 위해 커스텀 훅 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 };
}
이 항목에서는 Recoil 도입 전후의 상태 관리 방식을 세 가지 예시로 비교하여 설명하겠습니다. Recoil을 통해 코드 구조와 상태 관리가 어떻게 개선되었는지, 구체적인 예시를 통해 보여드립니다.
비포:
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 훅을 통해 전역 상태로 관리하면서, 화면 간 상태 전달 과정이 간결해졌습니다.비포:
상태 변경 시 직접 데이터를 설정하고 전달하는 방식이 중복되었으며, 이로 인해 수정 시 여러 곳에서 변경이 필요했습니다.
// 비포: 직접 상태를 설정하고 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을 도입하기 전에는, 상태 데이터를 자식 컴포넌트로 전달하기 위해 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에서 직접 상태를 가져다 씀으로써 컴포넌트 간 결합도가 줄어들고, 유지보수성이 개선되었습니다.결론적으로, Context API 대신 Recoil을 도입한 것은 상태를 더 효율적으로 관리하고, 코드의 복잡성을 줄이며 성능을 최적화하는 데 큰 도움이 되었으며 이런 효과를 얻을 수 있었습니다: