[React-native CLI] RN 기록화면 구현하기 2부(useReducer 사용하기)
1부에서는 +버튼을 클릭하면 현재날짜의 데이터에서 운행정보를 기록할 수 있도록 달력에서 다른 날짜를 선택하면 선택한 날짜의 데이터로 기록 운행 정보 기록할 수 있도록 구현하기 했었습니다.
https://development-piece.tistory.com/448
[React-native CLI] RN 기록화면 구현하기(선택한 날짜에 따라 Creact, Update하기)
+버튼을 클릭하면 현재날짜의 데이터에서 운행정보를 기록할 수 있도록 달력에서 다른 날짜를 선택하면 선택한 날짜의 데이터로 기록 운행 정보 기록할 수 있도록 구현하기 그리고 기본 운행
development-piece.tistory.com
이번 2부에서는 기본 운행 정보 기록하기에서 카드, 현금, LPG 주입량, LPG 단가, 주행거리, 영업거리, 통행료를 입력하면
기본 운행 정보 기록하기에 기록한 값을 가지고 운행 정보를 출력하기
구현 화면
기본 운행 정보 기록하기에서 카드, 현금, LPG 주입량, LPG 단가, 주행거리, 영업거리, 통행료를 입력하면
기본 운행 정보 기록하기에 기록한 값을 가지고 운행 정보를 출력하고 realm에 데이터 저장하기
1부 내용 코드
screens/Record.tsx
// react, react-native
import {useCallback, useEffect, useRef, useState} from 'react';
import {NativeStackScreenProps} from '@react-navigation/native-stack';
// library
import Realm from 'realm';
// assets, utils, realm
import {RecordSchema} from '../realm/schema';
// component
import SubHeader from '../components/record/SubHeader';
import DrivingInfoRecord from '../components/record/DrivingInfoRecord';
import DrivingInfo from '../components/record/DrivingInfo';
import ButtonWrap from '../components/record/ButtonWrap';
// style
import {Record as Style} from '../styles/record.styles';
// 네비게이션 스택에 해당하는 파라미터 타입 정의
type RootStackParamList = {
Profile: {postDate: string};
};
// 네비게이션 스택의 네비게이션 프롭스 및 라우트 프롭스를 제공하는 컴포넌트의 프롭스 타입 정의
type profileProps = NativeStackScreenProps<RootStackParamList, 'Profile'>;
const Record = ({route}: profileProps) => {
const {postDate} = route.params;
const realm = useRef<Realm>();
const [selectDate, setSelectDate] = useState(postDate); // 선택한 날짜
const [record, setRecord] = useState('');
// selectDate가 realm에 있는지 체크
const readDB = useCallback(() => {
const selectDateData = realm.current
?.objects('Record')
.filtered(`date = '${selectDate}'`)[0];
if (selectDateData) {
setRecord('UPDATE'); // 데이터 수정
console.log('데이터가 있습니다.', selectDateData);
} else {
setRecord('CREATE'); // 데이터 생성
console.log(데이터가 없습니다.);
}
}, [selectDate]);
// selectDate가 변경되면 readDB()렌더링
useEffect(() => {
readDB();
}, [readDB, selectDate]);
const openLocalDB = async () => {
realm.current = await Realm.open({schema: [RecordSchema]});
console.log('realmDB 열기!');
readDB();
};
// realmDB 열기, 닫기
useEffect(() => {
openLocalDB();
return () => {
realm.current?.close();
console.log('realmDB 닫기!');
};
}, []);
return (
<Style.container>
{/* 헤더 */}
<SubHeader selectDate={selectDate} setSelectDate={setSelectDate} />
<Style.scrollView>
{/* 기본 운행 정보 기록하기 */}
<DrivingInfoRecord state={state} dispatch={dispatch} />
{/* 운행정보 */}
<DrivingInfo state={state} />
</Style.scrollView>
{/* 취소, 저장 */}
<ButtonWrap record={record} createDB={createDB} updateDB={updateDB} />
</Style.container>
);
}
이 코드에서 추가적으로 더 작성하겠습니다.
useReducer를 사용하여 기본 운행 정보 기록하기에서 기록하면 운행정보 데이터가 바뀌게 하기
이전에는 useState를 사용했겠지만, 이번에는 useReducer을 사용해보려고 합니다.
useReducer를 사용하는 이유
useReducer hook은 리액트에서 상태를 관리하는 훅 중 하나로, 컴포넌트의 상태를 업데이트하는 데 사용됩니다.
useReducer를 사용하면 상태 업데이트 로직이 복잡한 경우 코드를 더 관리하기 쉽고, 컴포넌트의 성능을 최적화할 수 있는 장접이 있습니다.
또한 컴포넌트 간의 상태를 전달하기 쉽고, 상태 업데이트 로직을 중앙 집중화할 수 있어 유지보수가 편리합니다.
useReducer를 사용하면 좋은 상황
- 상태의 구조가 복잡하고 중첩된 경우
- 여러 개의 하위 컴포넌트에서 같은 상태를 사용해야 하는 경우
- 상태 갱신 로직이 복잡하거나, 여러 단계를 거쳐야 하는 경우
- 컴포넌트가 다른 상태와 상호작용해야 하는 경우
이번코드 같은 경우에는 아마 1, 2, 3에 해당돼서 사용하게 되었습니다.
카드, 현금, LPG 주입량, LPG 단가, 주행거리, 영업거리, 통행료, 영업금액, LPG 충전 금액, 연비, LPG 사용량 총 11개의 상태를 관리해야 되고,기본 운행 정보 기록하기에서 값을 기록하면 그 값에 따라 운행 정보에서 값이 변환이 되고, 하위컴포넌트에 전달해야 되는 값도 많기 때문에 useReducer를 사용했습니다.
useReducer 훅은 리액트에서 상태를 관리하는 훅 중 하나로, 컴포넌트의 상태를 업데이트하는 데 사용됩니다. 이 훅은 전역 상태 관리나 복잡한 상태 로직을 구현할 때 특히 유용합니다.
useReducer를 사용하려면 먼저 리듀서 함수를 정의해야 합니다. 리듀서 함수는 현재 상태와 액션을 인자로 받아 새로운 상태를 반환하는 함수입니다. 그런 다음, useReducer 훅을 사용하여 해당 리듀서 함수와 초기 상태를 전달하여 상태와 디스패치 함수를 얻을 수 있습니다.
다음은 useReducer의 기본적인 사용 방법입니다:
useReducer 사용방법
- 초기 상태 설정
- 리듀서 함수 정의 : 리듀서 함수는 현재 상태(state)와 액션(action)을 인자로 받아 새로운 상태를 변환하는 함수입니다.
- useReducer훅을 사용하기 : 해당 리듀서 함수와 초기 상태를 전달하여 상태와 디스패치 함수 얻기
기본 예제
import React, { useReducer } from 'react';
// 리듀서 함수 정의
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
const initialState = { count: 0 };
const Counter = () => {
// useReducer를 통해 상태와 디스패치 함수 얻기
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
</div>
);
};
export default Counter;
reducer 함수는 현재 상태와 액션을 받아서 액션에 따라 새로운 상태를 반환합니다.
useReducer 훅을 사용하여 상태와 디스패치 함수를 얻은 후에는 해당 상태를 컴포넌트에 렌더링 하고, 디스패치 함수를 이용하여 액션을 디스패치하여 상태를 업데이트할 수 있습니다.
기록장화면에서 useReduce 사용하기
1. 초기 상태 설정
import {RecordType} from '../types/types';
// 초기 상태
const initialRecord: RecordType = {
date: '', // 날짜
card: 0, // 카드
cash: 0, // 현금
lpgInjectionVolume: 0, // LPG 주입량
lpgUnitPrice: 0, // LPG 단가
mileage: 0, // 주행거리
businessDistance: 0, // 영업거리
toll: 0, // 통행료
operatingAmount: 0, // 영업금액
lpgChargeAmount: 0, // LPG 충전 금액
fuelEfficiency: 0, // 연비
lpgUsage: 0, // LPG 사용량
};
2. 리듀서 함수 정의
저는 리듀서 함수에 6가지 케이스에 대해 정의해 볼까 합니다.
- initialize : 해당 날짜(선택한 날짜)가 realm에 있는지 여부에 따라 상태 업데이트
- 해당 날짜가 realm에 없을 경우 {date: '해당날짜', 나머지는 0}
- 해당 날짜가 realm에 있을 경우 {해당날짜 데이터}
- updateInput : 기본 운행 정보 기록하기에서 기록할 정보들이 onChangeText가 변경될 때마다 상태 업데이트
- 카드, 현금, LPG 주입량, LPG 단가, 주행거리, 영업거리, 통행료
- updateOperationgAmount : 카드, 현금 변경되면 영업금액이 카드 + 현금되도록 업데이트
- updateLpgChargeAmount : LPG 주입량, LPG 단가 변경되면 LPG 충전 금액이 LPG 주입량 x LPG 단가 되도록 업데이트
- updateFuelEfficiency : 주행거리, LPG 주입량 변경되면 연비가 주행거리 / LPG 주입량 되도록 업데이트
- updateLpgUsage : 주행거리, 연비가 변경되면 LPG 사용량이 주행거리 / 연비 되도록 업데이트
// 리듀서 함수
const reducer = (state: RecordType, action: {type: string; payload?: any}) => {
switch (action.type) {
case 'initialize':
return {...state, ...action.payload};
case 'updateInput':
return {...state, [action.payload.name]: action.payload.value};
case 'updateOperatingAmount':
return {...state, operatingAmount: state.card + state.cash};
case 'updateLpgChargeAmount':
return {
...state,
lpgChargeAmount: state.lpgInjectionVolume * state.lpgUnitPrice,
};
case 'updateFuelEfficiency':
return {
...state,
fuelEfficiency: state.mileage / state.lpgInjectionVolume,
};
case 'updateLpgUsage':
return {...state, lpgUsage: state.mileage / state.fuelEfficiency};
default:
return state;
}
};
3. useReducer훅 사용하기
const [state, dispatch] = useReducer(reducer, initialRecord);
기본적인 useReducer세팅을 다했습니다.
이제 state, dispatch를 통해 reducer를 사용해 보겠습니다.
reducer 사용하기 : initialize 사용하기
const [selectDate, setSelectDate] = useState(postDate); // 선택한 날짜
선택된 날짜에 따라 아래와 같이 해주어야 됩니다.
- 해당 날짜가 realm에 없을 경우 {date: '해당날짜', 나머지는 0}
- 해당 날짜가 realm에 있을 경우 {해당날짜 데이터}
이전에 포스팅에서 작성해 준 코드에 추가적으로 작성하겠습니다.
// selectDate가 realm에 있는지 체크
const readDB = useCallback(() => {
const selectDateData = realm.current
?.objects('Record')
.filtered(`date = '${selectDate}'`)[0];
if (selectDateData) {
setRecord('UPDATE'); // 데이터 수정
dispatch({type: 'initialize', payload: selectDateData});
} else {
setRecord('CREATE'); // 데이터 생성
const initialData = {...RecordReducer.initialRecord, date: selectDate};
dispatch({type: 'initialize', payload: initialData});
}
}, [selectDate]);
지금까지 작성한 전체코드
reducers/recordReducer.ts
// types
import {RecordType} from '../types/types';
// 초기 상태
export const initialRecord: RecordType = {
date: '', // 날짜
card: 0, // 카드
cash: 0, // 현금
lpgInjectionVolume: 0, // LPG 주입량
lpgUnitPrice: 0, // LPG 단가
mileage: 0, // 주행거리
businessDistance: 0, // 영업거리
toll: 0, // 통행료
operatingAmount: 0, // 영업금액
lpgChargeAmount: 0, // LPG 충전 금액
fuelEfficiency: 0, // 연비
lpgUsage: 0, // LPG 사용량
};
// 리듀서 함수
export const reducer = (
state: RecordType,
action: {type: string; payload?: any},
) => {
switch (action.type) {
case 'initialize':
return {...state, ...action.payload};
case 'updateInput':
return {...state, [action.payload.name]: action.payload.value};
case 'updateOperatingAmount':
return {...state, operatingAmount: state.card + state.cash};
case 'updateLpgChargeAmount':
return {
...state,
lpgChargeAmount: state.lpgInjectionVolume * state.lpgUnitPrice,
};
case 'updateFuelEfficiency':
return {
...state,
fuelEfficiency: state.mileage / state.lpgInjectionVolume,
};
case 'updateLpgUsage':
return {...state, lpgUsage: state.mileage / state.fuelEfficiency};
default:
return state;
}
};
screens/Record.tsx
// react, react-native
import {useCallback, useEffect, useReducer, useRef, useState} from 'react';
import {NativeStackScreenProps} from '@react-navigation/native-stack';
// library
import Realm from 'realm';
// assets, utils, realm, types
import {RecordSchema} from '../realm/schema';
// component
import SubHeader from '../components/record/SubHeader';
import DrivingInfoRecord from '../components/record/DrivingInfoRecord';
import DrivingInfo from '../components/record/DrivingInfo';
import ButtonWrap from '../components/record/ButtonWrap';
// style
import {Record as Style} from '../styles/record.styles';
import * as RecordReducer from '../reducers/recordReducer';
type RootStackParamList = {
Profile: {postDate: string};
};
type profileProps = NativeStackScreenProps<RootStackParamList, 'Profile'>;
// 추가하기(+버튼 클릭시), 수정하기(달력에서 날짜 클릭, 수정 클릭)
const Record = ({route}: profileProps) => {
const {postDate} = route.params;
const realm = useRef<Realm>();
const [selectDate, setSelectDate] = useState(postDate); // 선택한 날짜
const [record, setRecord] = useState(''); // CREATE or UPDATE
const [state, dispatch] = useReducer(
RecordReducer.reducer,
RecordReducer.initialRecord,
);
// selectDate가 realm에 있는지 체크
const readDB = useCallback(() => {
const selectDateData = realm.current
?.objects('Record')
.filtered(`date = '${selectDate}'`)[0];
if (selectDateData) {
setRecord('UPDATE'); // 데이터 수정
dispatch({type: 'initialize', payload: selectDateData});
} else {
setRecord('CREATE'); // 데이터 생성
const initialData = {...RecordReducer.initialRecord, date: selectDate};
dispatch({type: 'initialize', payload: initialData});
}
}, [selectDate]);
// selectDate가 변경되면 readDB()렌더링
useEffect(() => {
readDB();
}, [readDB, selectDate]);
const openLocalDB = async () => {
realm.current = await Realm.open({schema: [RecordSchema]});
console.log('realmDB 열기!');
readDB();
};
// realmDB 열기, 닫기
useEffect(() => {
openLocalDB();
return () => {
realm.current?.close();
console.log('realmDB 닫기!');
};
}, []);
const createDB = () => {
realm.current?.write(() => {
realm.current?.create('Record', state);
});
console.log('데이터가 생성되었습니다.');
readDB();
};
const updateDB = () => {
const selectDateData = realm.current
?.objects('Record')
.filtered(`date = '${selectDate}'`)[0];
if (selectDateData) {
selectDateData.current = state;
}
console.log('데이터가 수정되었습니다.');
readDB();
};
return (
<Style.container>
{/* 헤더 */}
<SubHeader selectDate={selectDate} setSelectDate={setSelectDate} />
<Style.scrollView>
{/* 기본 운행 정보 기록하기 */}
<DrivingInfoRecord state={state} dispatch={dispatch} />
{/* 운행정보 */}
<DrivingInfo state={state} />
</Style.scrollView>
{/* 취소, 저장 */}
<ButtonWrap record={record} createDB={createDB} updateDB={updateDB} />
</Style.container>
);
};
export default Record;
하위컴포넌트에서 reducer 사용하기
기본 운행 정보 기록하기 : updateInput 사용하기
components/record/DrivingInfoRecord.tsx
// react, react-native
import {Dispatch} from 'react';
// component
import RecordInputBox from './RecordInputBox';
// style
import {DrivingInfoRecord as Style} from '../../styles/record.styles';
import {RecordType} from '../../types/types';
type Action = {
type: string;
payload?: any;
};
interface PropsType {
state: RecordType;
dispatch: Dispatch<Action>;
}
const DrivingInfoRecord = ({state, dispatch}: PropsType) => {
const {
card,
cash,
lpgInjectionVolume,
lpgUnitPrice,
mileage,
businessDistance,
toll,
} = state;
return (
<Style.container>
<Style.title>기본 운행 정보 기록하기</Style.title>
{/* 카드, 현금 */}
<Style.InputBoxWrap>
<RecordInputBox
title="카드"
state={card}
category="card"
dispatch={dispatch}
/>
<RecordInputBox
title="현금"
state={cash}
category="cash"
dispatch={dispatch}
/>
</Style.InputBoxWrap>
{/* LPG 주입량, LPG 단가 */}
<Style.InputBoxWrap>
<RecordInputBox
title="LPG 주입량"
state={lpgInjectionVolume}
category="lpgInjectionVolume"
unit="L"
dispatch={dispatch}
/>
<RecordInputBox
title="LPG 단가"
state={lpgUnitPrice}
category="lpgUnitPrice"
dispatch={dispatch}
/>
</Style.InputBoxWrap>
{/* 주행거리, 영업거리 */}
<Style.InputBoxWrap>
<RecordInputBox
title="주행거리"
state={mileage}
category="mileage"
unit="km"
dispatch={dispatch}
/>
<RecordInputBox
title="영업거리"
state={businessDistance}
category="businessDistance"
unit="km"
dispatch={dispatch}
/>
</Style.InputBoxWrap>
{/* 통행료 */}
<Style.InputBoxWrap>
<RecordInputBox
title="통행료"
state={toll}
category="toll"
dispatch={dispatch}
/>
<Style.fakeView></Style.fakeView>
</Style.InputBoxWrap>
</Style.container>
);
};
export default DrivingInfoRecord;
components/record/RecordInputBox.tsx
// react, react-native
import {Dispatch} from 'react';
// library
// assets, utils, realm, types
import {RecordBoxType, RecordType} from '../../types/types';
// component
// style
import {RecordInputBox as Style} from '../../styles/record.styles';
type Action = {
type: string;
payload?: any;
};
interface PropTypes extends RecordBoxType {
category: Exclude<keyof RecordType, 'date'>;
dispatch: Dispatch<Action>;
}
const RecordInputBox = ({
title,
state,
category,
unit = '원',
dispatch,
}: PropTypes) => {
// 입력값 변경 처리 함수
const onChange = (name: string, value: number) => {
dispatch({type: 'updateInput', payload: {name, value}});
// 카드 또는 현금이 변경될 때 영업금액 업데이트
if (name === 'card' || name === 'cash') {
dispatch({type: 'updateOperatingAmount'});
}
// LPG 주입량 또는 LPG 단가 변경될 때 LPG 충전 금액 업데이트
if (name === 'lpgInjectionVolume' || name === 'lpgUnitPrice') {
dispatch({type: 'updateLpgChargeAmount'});
}
// 주행거리 또는 LPG 주입량 변경될 때 연비 업데이트
if (name === 'mileage' || name === 'lpgInjectionVolume') {
dispatch({type: 'updateFuelEfficiency'});
}
// 주행거리 또는 연비 변경될 때 LPG 사용량 업데이트
if (name === 'mileage' || name === 'fuelEfficiency') {
dispatch({type: 'updateLpgUsage'});
}
};
return (
<Style.container>
<Style.title>{title}</Style.title>
<Style.inputWrap>
<Style.textInput
placeholder="0"
keyboardType="numeric"
value={state.toString()}
onChangeText={text => onChange(category, parseInt(text))}
/>
<Style.unitText>{unit}</Style.unitText>
</Style.inputWrap>
</Style.container>
);
};
export default RecordInputBox;
주요 코드
const onChange = (name: string, value: number) => {
dispatch({type: 'updateInput', payload: {name, value}});
// 카드 또는 현금이 변경될 때 영업금액 업데이트
if (name === 'card' || name === 'cash') {
dispatch({type: 'updateOperatingAmount'});
}
// LPG 주입량 또는 LPG 단가 변경될 때 LPG 충전 금액 업데이트
if (name === 'lpgInjectionVolume' || name === 'lpgUnitPrice') {
dispatch({type: 'updateLpgChargeAmount'});
}
// 주행거리 또는 LPG 주입량 변경될 때 연비 업데이트
if (name === 'mileage' || name === 'lpgInjectionVolume') {
dispatch({type: 'updateFuelEfficiency'});
}
// 주행거리 또는 연비 변경될 때 LPG 사용량 업데이트
if (name === 'mileage' || name === 'fuelEfficiency') {
dispatch({type: 'updateLpgUsage'});
}
};
운행 정보 구현하기
components/record/DrivingInfo.tsx
// react, react-native
import {SvgXml} from 'react-native-svg';
// library
// assets, utils, realm
import {svg} from '../../assets/svg';
import {RecordType} from '../../types/types';
// component
import RecordBox from '../common/RecordBox';
// style
import {DrivingInfo as Style} from '../../styles/record.styles';
interface PropsType {
state: RecordType;
}
const DrivingInfo = ({state}: PropsType) => {
const {
card,
cash,
lpgInjectionVolume,
lpgUnitPrice,
mileage,
operatingAmount,
lpgChargeAmount,
fuelEfficiency,
lpgUsage,
} = state;
return (
<Style.container>
<Style.title>운행 정보</Style.title>
<Style.RecordBoxWrap>
<RecordBox title="카드" state={card} />
<SvgXml xml={svg.plus} width={10} fill="#333" />
<RecordBox title="현금" state={cash} />
<SvgXml xml={svg.equals} />
<RecordBox title="영업금액" state={operatingAmount} option="orange" />
</Style.RecordBoxWrap>
<Style.RecordBoxWrap>
<RecordBox title="LPG 주입량" state={lpgInjectionVolume} unit="L" />
<SvgXml xml={svg.multiplication} />
<RecordBox title="LPG 단가" state={lpgUnitPrice} />
<SvgXml xml={svg.equals} />
<RecordBox
title="LPG 충전 금액"
state={lpgChargeAmount}
option="orange"
/>
</Style.RecordBoxWrap>
<Style.RecordBoxWrap>
<RecordBox title="주행거리" state={mileage} unit="km" />
<SvgXml xml={svg.division} />
<RecordBox title="LPG 주입량" state={lpgInjectionVolume} unit="L" />
<SvgXml xml={svg.equals} />
<RecordBox
title="연비"
state={fuelEfficiency}
unit="km/L"
option="orange"
/>
</Style.RecordBoxWrap>
<Style.RecordBoxWrap>
<RecordBox title="주행거리" state={mileage} unit="km" />
<SvgXml xml={svg.division} />
<RecordBox title="연비" state={fuelEfficiency} unit="km/L" />
<SvgXml xml={svg.equals} />
<RecordBox
title="LPG 사용량"
state={lpgUsage}
unit="L"
option="orange"
/>
</Style.RecordBoxWrap>
</Style.container>
);
};
export default DrivingInfo;
components/common/RecordBox.tsx
// assets, utils, realm, type
import {RecordBoxType} from '../../types/types';
// style
import {RecordBox as Style} from '../../styles/common.styles';
const RecordBox = ({
title,
state,
unit = '원',
option = 'basics',
}: RecordBoxType) => {
return (
<Style.box option={option}>
<Style.title option={option}>{title}</Style.title>
<Style.valueTextWrap>
<Style.valueText>{`${state}${unit}`}</Style.valueText>
</Style.valueTextWrap>
</Style.box>
);
};
export default RecordBox;
realm에 데이터 저장하기는 다음 글에 추가하도록 하겠습니다.