[원티드 프리온보딩 프론트엔드 과정] 1차 과제, 환율 계산기

Teasan·2022년 2월 1일
3
post-thumbnail

♦️ 1차 과제 : 환율 계산기

프로젝트 소개

currencylayer API를 활용하여 Select Box 전환기와 Tab Select 전환기, 두 종류의 환율 전환기를 각각 동작하도록 구현하는 것을 목표로 하는 프로젝트입니다.

과제 repo 링크

github repo
배포 링크

디렉토리 구조

├── node_modules
├── .github
├── public
│   └── index.html
├── src
│   ├── components
│   │   └── componentName
│   ├── costants
│   ├── hooks
│   ├── pages
│   ├── styles
│   └── utils
│
├── .gitignore
├── package-lock.json
├── package.json
└── README.md```

구현사항

Select Box 전환기

☑️ currencylayer API 데이터를 실시간으로 활용

☑️ select 에서 option 값(수취국가) 선택하여 저장하고, option 값(수취국가)수취국가에 따라 하단 환율값 변동 구현

☑️ Submit을 누르면 수취금액이 KRW, JPY, PHP 중 하나로 계산, 결과값 출력

☑️ utils 활용해 환율과 수취금액 소숫점 2째자리까지, 3자리 이상 되면 콤마(,) 처리

☑️ 수취금액을 입력하지 않거나, 0보다 작은 금액이거나 10,000 USD보다 큰 금액, 혹은 바른 숫자가 아니라면 “송금액이 바르지 않습니다"라는 에러 메시지를 alert 창으로 띄우도록 처리

☑️ proxy 서버(cors-anywher)를 기반으로 한 Heroku 배포

기능별 영상

  • 실시간으로 currencylayer API data를 받아온 뒤 select 된 option 값(나라)에 따른 환율값 실시간 변동 구현
  • utils 활용해 환율과 수취금액 소숫점 2째자리까지, 3자리 이상 되면 콤마(,) 처리
  • 조건식에 따른 input 값이 올바르지 않을 경우 “송금액이 바르지 않습니다"라는 에러 메시지를 alert 창으로 띄우도록 처리


[원티드 프리온보딩 코스] 첫번째 과제는 2인이 조를 이뤄 하나의 기능(환율계산기)을 만들어야 하는 과제였다. 각 조원들이 하나의 기능을 함께 구현해야했기 때문에, 조원들 간에 충분한 소통이 필요한 부분이었다.

새로 배웠거나 고민했던 지점

🔹 utils를 이용하는 방법

utils를 적극적으로 사용한 적이 없었기에 팀원들과 어떻게 공통된 기능을 찾아서 나누어야 할지 초반부터 조금은 막막했었다. 또한 기초세팅 부분에서 생각보다 많은 시간을 소모했기에 논의를 많이 하지 못한 상태에서 일단 각자 기능을 구현하다 비슷한 기능이 있다고 생각되면 그때 함께 고민해보기로 하였다.

먼저, 환율 계산기를 맡은 우리조는 해당 컴포넌트 안에서 2자리 소숫점과 수취금액을 1000 단위로 끊어주는 메소드와 정규식을 setTotal 값에 한번에 넣어주는 방식을 사용했다.


  setTotal(result.toFixed(2).replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ","));

두번째 환율 계산기를 구현하는 팀에서도 같은 조건(소숫점, 천단위 콤마)의 기능을 구현해야 했기에, 논의 끝에 utils에 addComma 라는 동일 기능의 함수를 사용하기로 결정했다. utils의 addComma는 이렇게 생겼고


export function addComma(num) {
  return num.toLocaleString(undefined, { maximumFractionDigits: 2 });
}

addComma 함수에 number type의 인자를 받아toLocaleString()이라는 내장함수로 처리해주는 식이었다. 팀원이 찾아낸 방법인데, 처음 보는 형태라 검색해서 찾아보니 정규표현식과 toFixed를 사용하지 않아도 옵션만 정해주면 아주 간단하게 형 변환을 해주고 동시에 콤마도 찍을 수 있었다.

toLocaleString은 type이 number인 값에서 toLocaleString 메소드를 인자 없이 호출하면 자동으로 String으로 형 변환된 뒤 콤마가 찍히도록 만들어준다.(*레퍼런스 참고) 기본적으로 소수점 3자리까지만 나타나기 때문에, 소수점 2자리 까지만 원하는 우리는 2번째 인자로 maximumFractionDigits 프로퍼티를 가진 객체에 2 라는 값을 주어 넘겨주면 되었다.

import { addComma } from "../utils/comma";

const handleToAmountChange = (e) => {
    e.preventDefault();
    const calculate = inputs * exchangeRate;

    if (inputs === "" || inputs < 0 || inputs > 10000 || inputs % 1 !== 0) {
      alert("송금액이 바르지 않습니다.");
    } else {
      const result = addComma(calculate);
      setTotal(result);
    }
  };

그리고 사용하고자 하는 컴포넌트에 import로 불러와 최종 값을 담아주는 setTotal에 addComma로 넘겨준 값을 넣어주면서 소숫점과 콤마를 처리해줄 수 있도록 했다.

🔹 공통 함수로 API 호출하기

API를 호출했을 때 기본 응답값의 형태는 다음과 같다.

{
    "success": true,
    "terms": "https://currencylayer.com/terms",
    "privacy": "https://currencylayer.com/privacy",
    "currencies": {
        "AED": "United Arab Emirates Dirham",
        "AFN": "Afghan Afghani",
        "ALL": "Albanian Lek",
        "AMD": "Armenian Dram",
        "ANG": "Netherlands Antillean Guilder",  
        [...] 
    }
} 

먼저 데이터를 받아올 수 있는지 확인하기 위해 사용하고자 하는 컴포넌트 내에서 useEffectaxios를 이용하여 API를 호출했다. 보안상의 이슈로 API key는 환경변수.env에서 설정하고 불러오는 방식을 택했다.

axios를 사용한 이유
팀원이 axios를 사용해보고 싶다는 의사를 비췄기 때문이다. 반드시 axios를 사용해야 하는 정당한 이유가 있었다기 보단 단지 학습 목적으로 사용하게 되었지만, 다음에는 의존하는 라이브러리에 대한 사용을 신중하게 고려하고 장단점을 따져본 뒤에 선택할 생각이다.


useEffect(() => {
    async function getData() {
      try {
        const response = await axios.get(
          `http://api.currencylayer.com/live?access_key=${process.env.REACT_APP_CURRENCYLAYER_API_KEY}`,
        );
        const { data } = response;
        setExchangeRateDict(data.quotes);
      } catch (error) {
        console.error(error);
      }
    }
    getData();
  }, []);

데이터를 제대로 받아오고 있는지 확인이 끝나고 기능을 모두 구현한 상태에서 해당 API 호출을 공통 함수로 사용하고 싶다는 다른 조원들의 요청을 받았다. 생각해보면 다른 조원의 프로젝트에서 사용할 API 데이터가 동일했기 때문에 따로따로 받아올 필요가 없었다. 그래서 일단 API를 공통으로 받아올 호출 함수가 필요했기에 먼저 data 폴더 내에서 API만 호출하는 getApi 호출 함수를 만들었다. response.data 안에 있는 quotes 의 data만 가져오고 싶었기 때문에 API가 호출되면 response.data 로 데이터를 전달할 수 있도록 했다.


import axios from "axios";

export const getApi = async () => {
  const response = await axios.get(
    `http://api.currencylayer.com/live?access_key=${process.env.REACT_APP_CURRENCYLAYER_API_KEY}`
  );

  return response.data;
};

그리고 data를 받아와서 useState에 담아주고 error 처리도 해주는 custom hooks를 만들었다. 언제든 재사용이 가능하도록 하기 위해서다.


import { useEffect, useState } from "react";
import { getApi } from "../utils/api";

export default function useData() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function getData() {
      try {
        const { quotes } = await getApi();
        setData(quotes);
      } catch (error) {
        console.error(error);
      }
    }
    getData();
  }, []);

  return { data };
}

사용하고자 하는 컴포넌트에서 getApi를 import 하고 useEffect를 사용하여 setExchangeRateDict에 data를 받아오도록 처리했다.


const { data } = useData();

useEffect(() => {
    setExchangeRateDict(data);
  }, [data]);

🔹 구조분해 할당을 통한 코드 경량화

구조분해 할당 : '분해(destructuring)'는 '파괴(destructive)'를 의미하지 않습니다. 구조 분해 할당이란 명칭은 어떤 것을 복사한 이후에 변수로 '분해(destructurize)'해준다는 의미 때문에 붙여졌습니다. 이 과정에서 분해 대상은 수정 또는 파괴되지 않습니다. 배열의 요소를 직접 변수에 할당하는 것보다 코드 양이 줄어든다는 점만 다릅니다. *레퍼런스 보러가기

이전에는 일일이 setState를 생성하여 함수로 값을 담아주는 방식을 사용했었다. 그러다보니, 코드를 짜는 입장으로서도 직관적으로 로직을 파악하기에도 복잡했고, 코드가 길어진다는 단점이 있었다.


const countryToCurrencyDict = {
  한국: "KRW",
  일본: "JPY",
  필리핀: "PHP",
};

function FirstConverter() {
  const [countryOption, setCoutryOption] = useState("한국");
  const toCurrency = countryToCurrencyDict[countryOption]; // KRW
  const [exchangeRateDict, setExchangeRateDict] = useState(null);
  const exchangeRate = exchangeRateDict && exchangeRateDict[`USD${toCurrency}`]; // USDKRW

이런 경우에, 구조분해 할당을 사용하면 좋을 것이라 생각했고 확연히 코드의 양이 줄게 되었다. 함께 페어 코딩을 했던 조원에게도 로직을 설명하는데에도 조금 더 수월했다.

🔹 추가 리팩토링 : CORS Anywhere와 Heroku로 프록시 서버를 생성하고 배포하기 (update : 2/11)

첫번째 과제를 배포하지 못한 게 계속 마음에 걸렸던 터라 시간적 여유가 생기자마자 혼자서라도 배포를 시도 해보기로 했다. 이전에 Heroku로 배포를 해본 상태였기 때문에 (세번째 과제 회고 보러가기) 동일한 방식으로 배포를 하면 될 것이라 생각하고 바로 배포를 진행하게 되었다.

☑️ 이슈 확인

Heroku로 배포를 하던 중 나는 이런 에러 메세지를 마주하게 된다 🚨

'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

그간 currencylayer API로 데이터를 받아오고 있었기 때문에 공공 API와 같은 교차 URL을 호출할 때 경험하게 되는 CORS 에러가 발생한 것 같았다. 에러를 해결해보겠다고 검색했다가 어느 답변에서는 프록시 서버를 직접 구축해야 한다는 이야기도 있었기에 약간 겁을 먹기도 했다. 간단한 프로젝트인데 프록시 서버를 직접 구축해야 한다니..?! 그러던 중에 CORS Anywhere와 Heroku를 이용하면 간단하게 프록시 서버를 구축할 수 있다는 또 다른 온라인 사수님들의 조언을 듣고 그래 바로 이 방법이다! 싶어서 여러 레퍼런스를 참고하여 배포를 진행하게 되었다.

☑️ 어떻게 해결했을까?

CORS Anywhere는 프록시 된 요청에 CORS 헤더를 추가하는 NodeJS 프록시다. MIT 라이선스로 자유롭게 사용할 수 있으며, whitelist, blacklist, rate limit 등의 다양한 설정도 간단하게 할 수 있는 장점이 있다.

CORS Anywhere Repository Fork

CORS Anywhere 를 이용해서 프록시 서버를 구축하기 전, 가장 첫번째로 해야할 일은 cors-anywhere의 Repository 로 이동하여 Fork를 해주는 일이었다. 바로 해당 Git Repository 로 이동하여 내 계정으로 Fork를 해주었다.

Heroku 설정하기

이제 NodeJS 프록시 배포를 위한 Heroku 설정이 필요하다. Heroku에 가입되어 있지 않다면, 미리 가입을 해준다. (혹시 Heroku에 가입하는 방법이 궁금하다면 이 분의 레퍼런스 를 참고하자.) 나는 이미 Heroku에 가입이 된 상태였기 때문에 로그인을 한 뒤 Create new app 을 눌러 새로운 app을 생성해주었다.

Heroku app과 Github repository 연결 및 배포하기

생성한 app 페이지로 이동하여 Deployment method 섹션에서 나의 Github 계정과 연동시켜준다. 그리고 Fork 해주었던 cors-anywhere의 Repository를 연결해주었다.

Repository의 연결이 완료된 후, 이 Repository 의 master branch 를 선택해서 Deploy Branch으로 최종 배포를 진행해주었다.

드디어 heroku 의 서브도메인이 생성되고 최종적으로 나만의 CORS proxy 서버가 만들어졌다. 👏 이제 Open app 버튼을 눌러 deploy 된 사이트가 제대로 배포가 되었는지 확인해주었다.

성공적으로 배포가 되었음을 알 수 있었다. 튜토리얼에 따르면, 이제 마지막으로 프록시 서버 주소(https://sixted-proxy-cors-anywhere.herokuapp.com)에 호출하고자 했던 API URL만 붙여주면 프록시 서버와 연결하는 과정은 모두 완료한 것이나 다름 없었다. (거의 다왔다!)

ex) 만약 https://first.sample.com을 호출하고 싶으면 아래처럼 호출하면 된다.

https://sixted-proxy-cors-anywhere.herokuapp.com/https://first.sample.com

이제 API 를 호출해주었던 getApi 로직으로 이동해서 URL을 수정해주기로 했다.

주소 수정하기

import axios from "axios";

export const getApi = async () => {
  const proxyAddress =
    "https://sixted-proxy-cors-anywhere.herokuapp.com/" +
    "http://api.currencylayer.com/live?access_key=" +
    process.env.REACT_APP_CURRENCYLAYER_API_KEY;

  const response = await axios.get(proxyAddress, {
    mode: "cors",
  });

  return response.data;
};
 

이전에 호출하고자 했던 API 주소 앞에, 배포한 proxy 주소(https://sixted-proxy-cors-anywhere.herokuapp.com)를 붙여주었다. 참고로 API key는 환경변수 (.env)로 지정하여 사용하였다. 마지막으로 axios의 get()을 이용해서 주소를 받아오고, axios의 mode"cors"로 설정해서 response에 넣어줄 수 있도록 했다.

프로젝트 repo를 heroku 배포하기

드디어 프로젝트의 배포만 남았다! 앞서 진행한 이전과 동일한 방식으로 새로운 app을 만들고 → Github 계정과 연동하고 → 우리의 프로젝트 repo를 연결한 뒤 → main branch를 deploy 해주었다.

이렇게 cors-anywhere를 이용하여 간단하게 proxy 서버를 구축하고, cors 에러 없이 성공적으로 배포를 완료할 수 있었다.

⚡️ 배포한 사이트 바로가기!

✍🏻 회고

몇 번 팀 프로젝트를 경험했던 터라, 이번에는 조금은 수월하지 않을까 예상 했었는데 그건 대단한 착각이었다는 걸 다시 한번 느끼게 해준 시간이었다. 총 4명의 팀으로 각각 2명씩 조를 나눠 구현 조건이 조금씩 다른 환율 계산기를 구현해야 했는데, 초기 세팅하는 과정이 원활하지 않았고 각자 구현해야 하는 기능에 급급해 초반에 세팅하면서 세팅한 eslint와 prettier 이슈로 모든 팀원들이 고생을 많이 했다. 돌아보니 프로젝트를 시작하기 전 팀원들 간에 충분한 협의가 필요했다고 느낀다.

  • 공통으로 사용할 수 있는 부분은 무엇인지
  • 어떻게 분리를 하여 사용할 것인지
  • 부여받은 과제 뿐만 아니라 다른 팀의 과제도 파악하는 시간이 필요

처음 받은 과제에 당황하여 내가 담당한 환율 계산기를 구현하느라 다른 조 팀원들과 커뮤니케이션이 조금 부족했던 것 같아 아쉬움도 남는다. 또한, 공통 함수를 조금 더 적극적으로 사용해볼 걸 하는 작은 아쉬움도 있다. (utils를 처음 사용해보았기에, 괜히 두려움이 앞서 구현 속도가 느려진 점도 있었다.) 하지만 이제 첫주차 첫번째 팀 과제이기에 첫술에 배부를 수는 없으리라 생각한다. 각자 피곤하고 벅찬 하루하루를 보내고 있지만 앞으로도 서로 배려하며 끝까지 완주하길 기대한다.

🗂 Reference


JavaScript 내장 메소드를 사용하여 숫자 천단위마다 콤마 찍기
ES6 문법 정리-(6) 구조 분해 할당(Destructuring Assignment)
모던 JavaScript 튜토리얼, 구조분해할당
CORS 프록시 서버 만들기
CORS 프록시 서버 구축(Feat. CORS Anywhere & Heroku)

profile
일단 공부가 '적성'에 맞는 개발자. 근성있습니다.

0개의 댓글