[moin-review] 2021-11-12

김_리트리버·2021년 11월 15일
0

Facts

  • 일본 은행리스트 api 연결
  • 중국 은행리스트 api 연결
  • 중국 은행 지점 리스트 api 연결
  • 중국 은행 지방 리스트 api 연결
  • 중국 은해 도시 리스트 api 연결
  • 일본송금시 송금방법이 payeasy 일 때 form 화면
  • 일본 송금시 통장사진 upload 화면
  • 중국 개인송금시 form 화면
  • 중국 법인 송금시 form 화면
  • 중국 알리페이 송금시 form 화면

Findings

  • side effect 함수의 부작용 원래 함수가 input 받아서 output 출력하는 일 외에 하는 일 ex > 콘솔 로깅, 네트워크 요청 등
  • windowing 사용자가 관찰가능할 때만 요소를 렌더링 하는 것 굳이 보지 않으면 렌더링 하지 않아서 쓸데없는 연산을 하지 않는 것 실제 세상도 관측가능할 때 입자가 존재한다.
  • react component 에서 side effect 를 발생시키는 부분을 전부 customhook 으로 빼면 react 에서 view 만을 담당하게 할 수 있고 굳이 redux 를 사용하지 않고 local component 에서만 사용하는 복잡한 상태로직을 분리시킬 수 있다.

Feelings

사용자가 모인 웹에서 송금을 신청할 때 은행에 대한 정보를 입력해야 했다.

이를 위해서는 은행목록을 서버로 부터 받아서 사용자가 직접 선택할 수 있도록 해야 했다.

기존 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 을 호출해 사용하는 것으로 했다.

before

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'

After

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 나 기타 복잡한 로직들을 전부 분리해

가독성과 유지보수성을 높였다.

profile
web-developer

0개의 댓글