[원티드 프리온보딩 프론트엔드][1주차 1차 과제] 환율 계산기 (공통함수/변수에 대한 고민 - utils, contants )

GY·2022년 1월 25일
1

원티드 프리온보딩

목록 보기
1/12
post-thumbnail

✔️ 환율계산기 만들기

배포주소

http://react18-week1.s3-website.ap-northeast-2.amazonaws.com

디렉토리 구조

├─ README.md
├─ package.json
├─ public
│	 └─ index.html
└─ src
   ├─ pages   
	 │	└─ mission1
 	 │	└─ mission2
	 │	└─ Home.jsx
   ├─ routes    
	 ├─ utils     
	 ├─ constants
	 └─ api
   └─ App.js
   └─ index.js


Mission 1

구현사항

  • 수취국가 선택 시 해당 국가 환율 표시
  • submit 버튼 클릭 시 계산된 수취금액, 환율, 수취국가 표시
  • 환율과 수취금액은 소숫점 2째자리까지, 3자리 이상일 경우 “,”를 찍어 표시
  • 환율정보를 실시간으로 가져오기 - 최초 렌더링 시 가져온 데이터로 계산한 금액을 수취국가를 변경할 때마다 표시
  • 올바르지 않은 수취금액 입력시 에러메시지 “송금액이 바르지 않습니다”를 빨간 글씨로 표시
  • 세부적인 에러메세지 추가
    • 0보다 작은 금액일 경우 ‘ 0보다 큰 값을 입력해주세요’
    • 10,000 USD보다 큰 금액일 경우 ‘10,000 USD보다 작은 금액을 입력해주세요’
    • 바른 숫자가 아닐경우 ‘숫자만 입력해주세요’

ezgif com-gif-maker (14)


이번 환율계산기는 vsc에서 live share기능을 사용해 두 명이서 함께 코딩을 진행했다.
아예 페어프로그래밍으로 진행한 건 처음이었는데 걱정했던 것과 다르게 팀원분과 잘 해낼 수 있었고 좋은 경험이었다.



✔️ 고민했던 포인트

🔺 utils

이번 과제는 조금씩 다른 환율 계산기 2개를 2명이서 한팀이 되어 각각 하나씩 맡았다.
한 레퍼지토리 안에서 각각의 계산기를 2명이 함께 작업하다보니, 자연히 중요해진 것은 공통으로 사용할 변수와 함수였다.


🔸 소숫점, 천단위 콤마를 포함해 금액을 변환하는 함수

나름대로 어떤 기능의 util함수를 만들어 함께 사용할지를 정했는데, 의외로 함께 사용하기에 어려웠던 점이 있었다.

  1. 금액 소숫점 둘째자리까지 보여주기, 천 단위로 콤마찍기
    첫번째 환율계산기를 만든 우리는 받아온 환율정보로 수취금액을 계산한다음, 소숫점 둘째자리까지 자르고 천 단위로 콤마를 찍어서 표시해주어야했다.
    그래서 convertPrice를 만들어주었다.
export const convertPrice = (stringPrice) => {
  const decimal = Number(stringPrice).toFixed(2).replace(/(^\d*[.]\d{3}$) | ([^0-9.]) |(^\d*[.]{2})/, '');
  const formatted = decimal.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ',');
  return formatted;
};

각자 기능을 구현한 뒤 util함수를 점검하기 위해 모였을 때, 계산기 2를 작업하는 팀이 사용한 함수 중 convertPrice와 비슷한 것을 찾을 수 있었다.


export const removeCommaAmount = (strAmount) => {
  const stringAmount = strAmount.replace(/,/g, '');
  const numberAmount = Number(stringAmount);

  return { stringAmount, numberAmount };
};

convertPrice는 소숫점과 ,를 추가하는 반면
removeCommaAmount는 ,를 삭제하는 기능이지만,
removeCommaAmount의 리턴값에 추가로직을 더해 convertPrice와 비슷하게 사용하는 코드가 있어 공통으로 합칠 수 있을 것 같았다. 예를 들면,

    const { stringAmount, numberAmount } = removeCommaAmount(value);

    if (stringAmount.length > 17) return;
    if (!checkRegax(stringAmount)) return;

    const commaAmount = numberAmount.toLocaleString();

    if (commaAmount === '0') {
      onChangeAmount('');
    } else {
      onChangeAmount(commaAmount);
    }

input값을 받아 removeCommaAmount로 콤마를 제거 한 뒤, 조건에 부합한다면 다시 콤마를 찍어 commaAmount라는 값으로 사용하는 것을 볼 수 있었다.

convertPrice에 정규표현식을 더해 가정 먼저 콤마가 있다면 삭제하는 로직을 추가한다면 가능했다.

다만, 어디서 어떻게 removeCommaAmount를 다른 방식으로 사용하는지를 일일히 찾기는 어려웠는데, 주어진 시간 내에서 작성한 코드의 구조를 최대한 바꾸지 않고 convertPrice와 removeCommaAmount함수를 합쳐 공통으로 사용하려면 .. 마치 자판기처럼 convertPrice에서 구조분해할당으로 필요한 리턴값을 가져다 사용할 수 있도록 해야하지 않나, 하는 생각이었다.

예를들면, 아래와 같이 필요한 형태를 convertPrice에서 모두 만들어 받아올 수 있도록 하는 것이다.

export const convertPrice = (stringPrice) => {
  const decimal = Number(stringPrice).toFixed(2).replace(/(^\d*[.]\d{3}$) | ([^0-9.]) |(^\d*[.]{2})/, ''); //소숫점 둘째자리까지의 문자열 값
  const formatted = decimal.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ',');//소숫점 둘째 자리와 천단위 콤마를 포함한 문자열 값
  const stringAmount = strAmount.replace(/,/g, '');//콤마를 없앤 문자열 값
  const numberAmount = Number(stringAmount);//소숫점 없는 숫자값
  const numberAmountComma = numberAmount.toLocaleString();//소숫점 없이 천단위 콤마를 포함한 문자열값

 return { decimal, formatted, stringAmount, numberAmount, numberAmountComma };
};


그리고 위의 코드를 이를 사용해 변경하면 다음과 같을 것이다.

    const { value } = e.target;
    const { stringAmount, numberAmount, numberAmountComma } = removeCommaAmount(value);

    if (stringAmount.length > 17) return;
    if (!checkRegax(stringAmount)) return;

    if (numberAmountComma === '0') {
      onChangeAmount('');
    } else {
      onChangeAmount(numberAmountComma);
    }

정규표현식을 사용한 이유

toLocaleString()메서드도 고려했으나, 정규표현식을 사용한 이유는 다음과 같다.
3,000원과 같이 정수로 계산된 수취금액이 딱 떨어질 때에도 소숫점 둘째자리까지 표시해주어야 한다. 그런데 아래와 같이 두 가지를 한꺼번에 하기 어려운 부분이 있었다.

function convertPrice2(stringPrice) {
  const stringAmount = stringPrice.toFixed(2);
  const result = Number(stringAmount).toLocaleString();//3,000
  return result;
}

function convertPrice2(stringPrice) {
  const stringAmount = stringPrice.toFixed(2);
  const result = stringAmount.toLocaleString();//3000.00
  return result;
}

정규표현식을 사용하면 인풋값으로 받은 string 타입의 값을 처리하기도 편리했고, 위와 같은 어려움을 해결할 수 있었다.


인자를 string 타입으로 지정한 이유

계산기 2를 작업한 팀은 인풋값을 string으로 받아야 했다. 숫자를 입력하면 입력창에서 자동으로 ,가 천단위로 찍혀 표시되어야 했던 게 주 이유였다. 그리고 공통함수 사용과 예외처리를 고려했을 때 계산기 1을 작업할 때도 인풋값을 string으로 받는 것이 좋겠다고 생각했다.


🔸 API 호출

https://currencylayer.com/의 API를 사용했다.
사용할 엔드포인트는 다음과 같았다.

Example API Request:

Run API Requesthttps://api.currencylayer.com/live
    ? access_key = YOUR_ACCESS_KEY
    & currencies = AUD,EUR,GBP,PLN

따라서 공통으로 사용할 API 호출 함수를 만들었는데, 이 때 필요한 국가 문자열을 배열형태로 전달받아 join으로 묶어 파라미터를 완성해 호출하도록 했다.

const { default: axios } = require('axios');

const getExchangeRate = (countries) => {
  const params = countries.length ? `&currencies=${countries.join()}` : '';
  return axios.get(`http://api.currencylayer.com/live?access_key=${process.env.REACT_APP_API_KEY}${params}`);
};

export default getExchangeRate;

🔸 공통함수와 상수화에 대한 고민

어떤 부분가지 상수화를 하고, 공통함수로 묶을 것인지에 대한 고민을 하게 되었다.

  • 함수명으로 그 역할과 목적을 추론할 수 있다는 장점은 있지만
    너무 잘게 쪼개서 상수화를 하거나 공통함수로 묶게되면 일일히 선언 위치를 찾아 어떤 함수인지 로직을 확인해야 하는데, 간단한 로직이라면 직접 선언해주는 것이 가독성이 좋을 것이라고 생각했다.
  • 이럴 경우에도 별도로 상수화가 필요한 경우가 있다면 코드 수정이나 유지 보수에 있어 오타 등으로 혼란이 생길 우려를 방지하기 위함일 것이다.
//utils
export const convertExchangeRate = (params) => {
  const { sendingRate, receivingRate } = params;

  return receivingRate / sendingRate;
};

export const getRateKey = (selectedCountry) => `USD${selectedCountry}`;

상수화

//constants
export const USD = 'USD';
export const CAD = 'CAD';
export const KRW = 'KRW';
export const HKD = 'HKD';
export const JPY = 'JPY';
export const CNY = 'CNY';
export const PHP = 'PHP';
export const countryList1 = [JPY, PHP, KRW];
export const countryList2 = [USD, CAD, KRW, HKD, JPY, CNY];
export const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dev'];

  1. 드롭다운의 메뉴 option의 value값은 고정적이므로 별도로 사용할 필요가 없다고 판단했다.
<label htmlFor="reception" className={styles.item}>
  수취국가:
    <select name="reception" id="reception" onChange={selectCountry}>
      <option value="KRW">한국(KRW)</option>
<option value="JPY">일본(JPY)</option>
<option value="PHP">필리핀(PHP)</option>
</select>
</label>

  1. API를 호출할 때 환율이 필요한 국가 문자열을 배열형태로 파라미터에 전달해주어야 했다. (위 공통함수 API 참고) 해당 부분은 현업을 가정했을 때 추가되거나, 변경될 여지가 있으므로 별도로 countryList1,2를 상수화해 사용했다.
//contants
export const countryList1 = [JPY, PHP, KRW];

// Mission1.jsx
  useEffect(() => {
    api(countryList1).then((res) => setExchangeRates(res.data.quotes));
  }, []);

  1. API를 호출했을 때 기본 응답값의 형태는 다음과 같다.
{
    "success": true,
    "terms": "https://currencylayer.com/terms",
    "privacy": "https://currencylayer.com/privacy",
    "timestamp": 1432400348,
    "source": "USD",
    "quotes": {
        "USDAUD": 1.278342,
        "USDEUR": 1.278342,
        "USDGBP": 0.908019,
        "USDPLN": 3.731504
    }
}       

과제에서는 송금국가는 USD 고정이었기 때문에 USD 뒤에 붙는 국가만 지정해 밸류를 가져오면 되었다.
이 부분을 getRateKey라는 utils함수로 만들어주었다.

//utils
export const getRateKey = (selectedCountry) => `USD${selectedCountry}`;

여기서 고민이 되었다.
한 프로젝트에서 동일한 API엔드포인트를 사용한다면, 아래와 같이 사용하는게 응답값의 형태를 아는 상황에서 직관적일 수 있을 것 같다.

//Mission1.jsx
  const handleRemit = (e) => {
    e.preventDefault();
    const val = inputRef.current.value;
    validateRemit(val);
    const rate = exchangeRates[`USD${selectedCountry}`];//
    const result = convertPrice(String(rate * val));
    setConvertedPrice(result);
  };

반대로 프로젝트의 규모가 커지고 API의 엔드포인트 혹은 파라미터를 다양하게 조합해 사용해야 한다면 getRateKey와 그 외 다른 함수들을 만들어 사용하고 하나의 파일에서 API호출 방식을 관리하는 것이 더 효율적일 수 있을 것 같다.
아래 선택한 국가의 환율을 표시하는 영역에서는 getRateKey를 사용해주었다.

//Mission1.jsx
          <p className={styles.item}>
            환율 :
            {`${convertPrice(String(exchangeRates[getRateKey(selectedCountry)]))} ${selectedCountry}`}
            /USD
          </p>


✔️ 회고

이번 과제는 기능 구현 자체보다는 어떻게 공통된 기능을 묶어 함수와 변수로 관리할지가 핵심이었던 것 같다.
하루 안에 완성해야하는 첫 과제였던만큼 팀원들과 최선을 다했지만, 돌아보고 나니 utils함수에 대해 더 세부적으로 미리 이야기를 나누고 일찍 검토해보면 더 좋았을 것 같다는 생각이 들었다.

  • 어떤 input과 parameter를 받고
  • 어떤 값을 반환할지
  • 어떤 로직을 가지고 있는지
  • 각 과제별로 어떤 부분에 사용할지

와 같은 부분을 다음에는 더 세부적으로 최대한 생각하고, 얘기를 나누어보고 싶다.

첫 프로젝트 시작 당일 코로나 확진으로 다소 정신없는 하루였고 회의중에 전화를 받거나 시설로 이동하는 일들 때문에 미팅과 프로젝트 진행 중간에 흐름을 끊게 되는 일들이 있었어서 함께하는 팀원분에게 미안했다. 다들 배려해주셔서 감사했고, 컨디션을 회복하고 있으니 다시 팀원분들과 열정적으로 프로젝트에 참여해야겠다.



Reference

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.

0개의 댓글