React 실시간 환율 계산기 — API 연동
왜 만들었나
환율 계산기는 API 연동을 처음 배울 때 딱 좋은 예제다. 구조가 단순하고, 실시간 데이터가 오가는 흐름을 눈으로 확인할 수 있다. 이번에는 빌드 도구 없이 React CDN + Babel Standalone으로 index.html 한 파일로 만들었다. npm install 없이 브라우저에서 바로 열면 동작한다.
구현 기능:
- 실시간 환율 fetch (ExchangeRate-API, 무료 / 인증 없음)
- 통화 선택 + 금액 입력 → 즉시 환산
- 통화 스왑 버튼
- 주요 통화 빠른 비교 패널
- 로딩 / 에러 / 성공 상태 처리
기술 상세
빌드 없는 React 세팅
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel"> 태그 안에 JSX를 쓰면 Babel이 브라우저에서 실시간 트랜스파일한다. 튜토리얼이나 프로토타입 용도에 적합하다. 프로덕션에는 Vite 기반 빌드를 쓴다.
아키텍처
App
├── useExchangeRates(baseCurrency) ← 커스텀 훅: API 호출 담당
│ ├── rates: { KRW: 1370, JPY: 150, ... }
│ ├── status: 'loading' | 'live' | 'error'
│ └── updatedAt, refetch
├── fromCurrency / toCurrency ← 선택된 통화 상태
└── amount ← 입력 금액 상태useExchangeRates가 API 로직을 캡슐화하고, App은 UI와 상태만 관리한다.
핵심 소스 코드
커스텀 훅 — useExchangeRates
function useExchangeRates(baseCurrency) {
const [rates, setRates] = useState(null);
const [status, setStatus] = useState('loading');
const [updatedAt, setUpdatedAt] = useState(null);
const fetchRates = useCallback(async () => {
setStatus('loading');
try {
const res = await fetch(`https://api.exchangerate-api.com/v4/latest/${baseCurrency}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setRates(data.rates);
setUpdatedAt(new Date().toLocaleTimeString('ko-KR'));
setStatus('live');
} catch (e) {
setStatus('error');
}
}, [baseCurrency]);
useEffect(() => {
fetchRates();
}, [fetchRates]);
return { rates, status, updatedAt, refetch: fetchRates };
}
useCallback으로 fetchRates를 메모이제이션하고, useEffect의 의존성 배열에 넣는다. baseCurrency가 바뀌면 fetchRates가 새로 생성되고, useEffect가 재실행된다.
환산 계산
const converted = (() => {
if (!rates || !amount) return null;
const num = parseFloat(amount.replace(/,/g, ''));
if (isNaN(num)) return null;
return num * (rates[toCurrency] ?? 0);
})();
API 응답의 rates 객체는 { KRW: 1370.5, JPY: 149.8, EUR: 0.92, ... } 구조다. 베이스 통화(fromCurrency) 기준 각 통화의 환율이 들어있다. 즉시 실행 함수(IIFE)로 계산 로직을 격리했다.
통화 스왑
function handleSwap() {
setFromCurrency(toCurrency);
setToCurrency(fromCurrency);
}
두 상태를 동시에 교환한다. fromCurrency가 바뀌면 useExchangeRates 훅이 새 베이스로 API를 다시 호출한다.
숫자 포맷
function fmt(val, code) {
if (!val && val !== 0) return '—';
const locale = code === 'KRW' || code === 'JPY' ? 'ko-KR' : 'en-US';
const digits = (code === 'KRW' || code === 'JPY') ? 0 : 2;
return new Intl.NumberFormat(locale, {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
}).format(val);
}
Intl.NumberFormat으로 통화별 소수점 자리수를 다르게 처리한다. 원화/엔화는 소수점 없음, 나머지는 2자리.
기술 선택 비교
| 항목 | 이번 선택 | 대안 |
|---|---|---|
| 빌드 환경 | CDN + Babel Standalone | Vite, CRA |
| API | ExchangeRate-API (무료/키 없음) | Open Exchange Rates (유료), Fixer.io |
| 상태 관리 | useState + 커스텀 훅 | Redux, Zustand |
| 숫자 포맷 | Intl.NumberFormat (브라우저 내장) | numeral.js, accounting.js |
삽질 기록
1. useCallback 빠뜨려서 무한 루프
// 잘못된 코드
useEffect(() => {
fetchRates(); // fetchRates가 매 렌더마다 새로 생성 → 무한 루프
}, [fetchRates]);
fetchRates를 useCallback 없이 컴포넌트 안에 선언하면, 렌더마다 새 함수가 생성된다. 이 함수를 useEffect 의존성에 넣으면 루프가 된다. useCallback으로 감싸서 해결.
2. baseCurrency가 바뀌어도 rates가 안 바뀜
useEffect의 의존성 배열을 []로 고정하면 마운트 시 한 번만 실행된다. [fetchRates]로 바꾸고, fetchRates를 useCallback([baseCurrency])로 감싸야 통화 변경 시 재호출된다.
3. 엔화/원화 소수점 표시 문제
149.8 JPY 같은 결과가 나왔다. 원화와 엔화는 소수점 없이 표시해야 자연스럽다. Intl.NumberFormat의 maximumFractionDigits 옵션으로 통화별 분기 처리.
마무리
React의 핵심 패턴 세 가지를 이 예제에서 다 볼 수 있다.
useEffect+fetch→ 외부 데이터 가져오기useCallback+ 의존성 배열 → 불필요한 재실행 방지- 커스텀 훅 → API 로직 분리
빌드 없이 CDN으로 만들어서 파일 하나로 동작한다는 것도 포인트다. React를 처음 배울 때 프로젝트 셋업 없이 바로 실행해보기 좋다.
기술 스택: React 18, Babel Standalone, ExchangeRate-API, Intl.NumberFormat
소스 코드: GitHub
'튜토리얼 > 웹' 카테고리의 다른 글
| [HTML/CSS/JS] Canvas API로 그림판 만들기 (0) | 2026.03.01 |
|---|---|
| [HTML/CSS/JS] 로컬스토리지 메모장 앱 만들기 (0) | 2026.02.28 |
| [HTML/CSS/JS] 다크모드 토글 포트폴리오 페이지 만들기 (0) | 2026.02.28 |
| [Next.js] Supabase로 방명록 만들기 (풀스택) (0) | 2026.02.27 |
| [HTML/CSS/JS] 드래그앤드롭 Todo 앱 만들기 (0) | 2026.02.26 |