사용자가 모인 웹에서 송금을 신청할 때 은행에 대한 정보를 입력해야 했다.
이를 위해서는 은행목록을 서버로 부터 받아서 사용자가 직접 선택할 수 있도록 해야 했다.
기존 redux 를 사용할 때는 thunk 은행 목록을 받아오는 action 을 전역 store 에 은행목록을 저장한 후
useSelector 등으로 받아서 출력했을 것이다.
하지만 생각해보면 은행 목록은 전역에서 사용하는 상태가 아니라 은행을 입력하는 form 에서만 사용하는 상태이다.
side-effect 를 react component 에서 분리시키기 위해 redux 를 남발하게 되면 굳이 전역으로 관리해야할 상태가 아닌데도 redux store 에 다 때려박게 된다.
결국 모인 웹의 legacy 같은 경우 store 에서 관리하는 상태의 field 만 20여개가 되었고
redux 의 장점인 상태 추적 및 디버깅도 제대로 하기 힘든 상태가 되었다.
때문에 이번 송금 flow 개선 프로젝트를 하면서 단순히 side-efect 를 분리하기 위한 용도로 redux 를 사용하지 않고 전역으로 관리해야할 상태가 아니면 redux-store 에서 상태를 관리하지 않기로 했다.
이에 react-component 에서 side-effect 를 분리하기 위해 custonhook 을 사용했다.
useEffect 등으로 네트워크 요청 및 가져온 데이터를 client 에 맞게 가공하는 로직을
customhook 으로 분리시켰다.
그리고 @testing-library/react-hooks
을 사용해 해당 hook 들의 test code 를 작성하고 react component 에서 해당 hook 을 호출해 사용하는 것으로 했다.
legacy RecipientBankPopup
import { t as tt } from 'i18next'
import PropTypes from 'prop-types'
import { fetchBanks } from '../../model'
import ListSelectPopup from './ListSelectPopup'
import { BankMethod } from '../../types/remit'
import { RecipientType } from '../../types/recipient'
const UnitToCountryCode = {
JPY: 'JP',
CNY: 'CN',
SGD: 'SG',
USD: 'US',
AUD: 'AU',
GBP: 'GB',
HKD: 'HK',
NPR: 'NP',
THB: 'TH',
CAD: 'CA',
MYR: 'MY',
VND: 'VN',
RUB: 'RU',
}
export default class RecipientBankPopup extends ListSelectPopup {
constructor(props) {
super(props)
const [title, placeholder] = (() => {
const { unit, recipient_type } = props
const title = tt('verification.bank_popup.title')
const placeholder = tt('verification.bank_popup.placeholder')
if (unit === 'JPY' && recipient_type === RecipientType.PAYEASY) {
return [tt('verification.bank_popup.payeasy_title'), tt('verification.bank_popup.payeasy_placeholder')]
}
if (unit === 'HKD') {
return [title, tt('verification.bank_popup.hkd_placeholder')]
}
return [title, placeholder]
})()
this.component = {
...this.component,
title,
placeholder,
}
}
async componentDidMount() {
const { unit, recipient_type } = this.props
const countryCode = UnitToCountryCode[unit]
const { banks } = await fetchBanks({
countryCode,
unit,
method: recipient_type === RecipientType.PAYEASY ? BankMethod.PAYEASY : BankMethod.BANK_WIRE,
})
if (unit !== 'CNY') {
this.setState({ items: banks, loading: false })
} else {
const { banks: unionPayBanks } = await fetchBanks({
countryCode,
unit,
method: BankMethod.CARD_TRANSFER,
})
const bankNames = banks.map(bank => bank.name_ko)
const total = banks.concat(
unionPayBanks.filter(bank => !bankNames.includes(bank.name_ko)),
)
this.setState({
items: total,
unionPayBanks,
loading: false,
})
}
}
getItemName(bank) {
const { name, name_ko, name_en, code } = bank
return `${code}|${name}|${name_ko}|${name_en}`
}
onSelect(bank) {
const { unit, recipient_type } = this.props
const { unionPayBanks } = this.state
const { name, name_ko, name_en } = bank
const isPayEasy = recipient_type === RecipientType.PAYEASY
const extraCode = unionPayBanks && unionPayBanks.map(_bank => _bank.name_ko).includes(name_ko) ? RecipientBankPopup.extraBankCode : null
const value = (() => {
if (isPayEasy) {
return `${name_ko} / ${name}`
}
if (['HKD', 'NPR', 'CAD'].includes(unit)) {
return name
}
return String(!name_ko && !name_en ? name : `${name_ko || ''} ${name_en || ''}`).trim()
})()
this.close({
value,
code: bank.code,
extraCode,
})
}
renderItem(bank) {
const { unit, recipient_type } = this.props
const { code, name, name_ko, name_en } = bank
const isPayEasy = recipient_type === RecipientType.PAYEASY
let titleText = name_ko ? `${name} / ${name_ko}` : `${name}`
let subText = name_en || null
if (unit === 'JPY' && isPayEasy) {
titleText = code
subText = `${name} / ${name_ko}`
}
if (unit === 'NPR') {
titleText = name
}
if (unit === 'HKD') {
titleText = name_en
subText = null
}
return this.renderTitleDescItem(bank, code, titleText, subText, true)
}
}
RecipientBankPopup.propTypes = { unit: PropTypes.string }
RecipientBankPopup.extraBankCode = '67'
useBankList
import { useEffect, useReducer } from 'react'
import { fetchBanks } from '@/model'
import { FETCHED, FETCH_ERROR } from '@/types/remit'
import { Method } from './useRemitForm'
interface UseBankList {
countryCode: string
unit: string
method: Method
}
const methods = {
account: 1,
cardTransfer: 2,
payeasy: 3,
}
const initialState = {
loaded: false,
error: '',
bankList: [],
}
const reducer = (state, action) => {
switch (action.type) {
case FETCHED:
return { ...initialState, loaded: true, bankList: action.payload }
case FETCH_ERROR:
return { ...initialState, loaded: true, error: action.payload }
default:
return state
}
}
export const getBankList = async ({ countryCode, unit, method }, _fetchBanks = fetchBanks) => {
const { banks } = await _fetchBanks({ countryCode, unit, method })
if (countryCode === 'CN') {
const { banks: unionPayBanks } = await _fetchBanks({ countryCode, unit, method: '2' })
const filteredUnionPayBanks = unionPayBanks.filter(({ code }) => code >= 1000)
const totalBanks = [...banks, ...filteredUnionPayBanks]
return totalBanks.map(({ code, name, name_ko, name_en }) => ({
text: name,
label: name_ko,
value: { bank: `${name_ko} ${name_en}`, code },
}))
}
if (method === '3') {
return banks.map(({ code, name, name_ko }) => ({
text: name,
label: name_ko,
value: { bank: `${name_ko} / ${name}`, code },
}))
}
return banks.map(({ code, name, name_ko, name_en }) => ({
text: name,
label: name_ko || '',
value: { bank: name_ko ? `${name_ko} ${name_en}` : name, code },
}))
}
const useBankList = ({ countryCode, unit, method }: UseBankList, _getBankList = getBankList) => {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
const _fetch = async () => {
try {
const bankList = await _getBankList({ countryCode, unit, method: methods[method] || '1' })
dispatch({ type: FETCHED, payload: bankList || [] })
} catch (error) {
alert(error.message)
dispatch({ type: FETCH_ERROR, payload: error.message })
}
}
_fetch()
}, [])
return state
}
BankSelectModal
const BankSelectModal = ({
bank, countryCode, unit, method, onChangeRecipientField,
}: BankSelectModalProps) => {
const { loaded, error, bankList } = useBankList({ countryCode, unit, method })
const list = loaded && !error ? bankList : [{ text: '은행 목록을 불러오는 중...', value: { bank: '', code: '' }, label: '' }]
return (
<Dropdown
data={bank}
label='은행명'
setData={({ value }) => {
onChangeRecipientField('bank', value.bank)
onChangeRecipientField('bank_code', value.code)
onChangeRecipientField('branch', '')
onChangeRecipientField('bank_province', '')
onChangeRecipientField('provinceCode', '')
onChangeRecipientField('bank_city', '')
}}
list={list}
popup
popupTitle='은행명'
validator={error ? 'error' : undefined}
validatorLabel={error}
>
<InitialInputText
value={bank}
intialValue='은행명을 선택해주세요'
/>
</Dropdown>
)
결과적으로 react 컴포넌트안에서 useEffect 나 기타 복잡한 로직들을 전부 분리해
가독성과 유지보수성을 높였다.