실시간 API에서 환율을 가져오는 Vue 환율 변환기가 필요하다면, 이 튜토리얼이 처음부터 끝까지 안내합니다. Composition API, TypeScript, 무료 환율 API를 사용해 프로덕션에 바로 투입할 수 있는 Vue 3 컴포넌트를 만듭니다 — 적절한 디바운싱, 캐싱, 에러 처리, Nuxt용 SSR-안전 패턴 포함.
마지막에는 재사용 가능한 <CurrencyConverter /> 컴포넌트, 어떤 Vue 3 프로젝트에도 넣을 수 있는 useCurrencyRates 컴포저블, 그리고 실제 사용자에게 통화 변환을 배포할 때 중요한 트레이드오프에 대한 명확한 이해가 남습니다.
무엇을 만드는가
다음 기능을 갖춘 Vue 3 환율 변환기:
- Finexly API를 통한 170개 이상 통화의 실시간 환율
- 사용자가 입력하는 동안 반응형으로 갱신되는 변환
- 통화 교체 버튼(USD → EUR이 한 번 클릭으로 EUR → USD가 됨)
- 네트워크를 낭비하지 않는 디바운스된 API 호출
- 지연 시간을 낮게 유지하고 무료 할당량을 지키는 인메모리 캐시
- 에러 처리, 로딩 상태, 깔끔한 TypeScript 타입
- Nuxt 3에서 hydration 불일치 없이 동작하는 SSR-친화적
최종 컴포넌트는 약 80줄. 컴포저블은 60줄 더. 그게 전부입니다.
사전 요구 사항
다음에 익숙해야 합니다:
- Vue 3 문법 (
<script setup>형태) - 기본 TypeScript
fetch로 REST API 호출
Finexly API 키도 필요합니다. 대시보드에서 받으세요 — 약 30초 걸리고 무료 플랜은 신용카드 없이 월 1,000회 요청을 줍니다. 서비스를 처음 쓴다면 Finexly API 문서에 5분짜리 퀵스타트가 있습니다.
1단계: Vite로 Vue 3 프로젝트 세팅
처음 시작한다면:
npm create vite@latest finexly-converter -- --template vue-ts
cd finexly-converter
npm install
npm run devVite는 Vue 3, TypeScript, hot module reload를 즉시 제공합니다. src/App.vue를 열어 보일러플레이트를 비우세요 — 곧 변환기로 교체합니다.
기존 Nuxt 3 프로젝트에 통합한다면 이 단계를 건너뛸 수 있습니다. 아래 컴포저블은 vue의 표준 ref와 computed를 사용하므로 Nuxt에서도 동일하게 동작합니다.
2단계: API 키를 안전하게 보관
Finexly API 키를 컴포넌트에 직접 붙이지 마세요. .env.local에 둡니다:
VITE_FINEXLY_API_KEY=your_key_hereVite는 VITE_ 접두사가 붙은 변수를 클라이언트에 노출합니다. Nuxt에서는 NUXT_PUBLIC_FINEXLY_API_KEY를 사용하고 useRuntimeConfig().public.finexlyApiKey에서 읽습니다.
클라이언트 번들에 키가 노출되는 게 걱정되면 백엔드 라우트나 서버리스 함수로 요청을 프록시하세요. 그 패턴은 7단계에서 보여줍니다.
3단계: useCurrencyRates 컴포저블 만들기
컴포저블은 변환기의 핵심입니다. fetch, 캐시, 로딩/에러 상태를 책임지고 — 컴포넌트는 표시 전용으로 유지합니다.
src/composables/useCurrencyRates.ts 생성:
import { ref, type Ref } from 'vue'
const API_KEY = import.meta.env.VITE_FINEXLY_API_KEY
const BASE_URL = 'https://api.finexly.com/v1'
// 모듈 레벨 캐시: 이 컴포저블을 호출하는 모든 컴포넌트 사이에서 공유.
// 각 항목은 5분 — 즉각적으로 느껴질 만큼 길고, 변동성 큰 FX 움직임에도
// 정확함을 유지할 만큼 짧다.
const cache = new Map<string, { rates: Record<string, number>; expires: number }>()
const TTL_MS = 5 * 60 * 1000
interface RatesResponse {
base: string
date: string
rates: Record<string, number>
}
export function useCurrencyRates() {
const rates: Ref<Record<string, number>> = ref({})
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchRates(base: string) {
const cached = cache.get(base)
if (cached && cached.expires > Date.now()) {
rates.value = cached.rates
return
}
loading.value = true
error.value = null
try {
const url = `${BASE_URL}/latest?base=${base}&apikey=${API_KEY}`
const res = await fetch(url)
if (!res.ok) throw new Error(`Finexly returned ${res.status}`)
const data: RatesResponse = await res.json()
rates.value = data.rates
cache.set(base, { rates: data.rates, expires: Date.now() + TTL_MS })
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load rates'
} finally {
loading.value = false
}
}
function convert(amount: number, from: string, to: string): number {
if (from === to) return amount
const fromRate = rates.value[from]
const toRate = rates.value[to]
if (!fromRate || !toRate) return 0
return (amount / fromRate) * toRate
}
return { rates, loading, error, fetchRates, convert }
}짚어둘 만한 몇 가지:
cache는 의도적으로 모듈 레벨에 둡니다. 컴포저블을 호출하는 두 컴포넌트가 같은 캐시를 공유하므로 라우트 전환 시 같은 기준 통화를 다시 받지 않습니다.rates는Record<string, number>타입이라 나중에<select>에 그대로 넣을 수 있습니다.convert함수는 크로스 환율 계산을 하므로 "from" 통화가 바뀔 때마다 다시 받을 필요가 없습니다. 자세한 건 5단계.
4단계: 변환기 컴포넌트
src/components/CurrencyConverter.vue 생성:
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useCurrencyRates } from '@/composables/useCurrencyRates'
const { rates, loading, error, fetchRates, convert } = useCurrencyRates()
const amount = ref(100)
const from = ref('USD')
const to = ref('EUR')
const converted = computed(() =>
convert(amount.value, from.value, to.value).toFixed(2)
)
const currencies = computed(() => Object.keys(rates.value).sort())
function swap() {
;[from.value, to.value] = [to.value, from.value]
}
let timeout: ReturnType<typeof setTimeout> | null = null
watch(from, (newBase) => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => fetchRates(newBase), 300)
})
onMounted(() => fetchRates(from.value))
</script>
<template>
<div class="converter">
<h2>Currency Converter</h2>
<div class="row">
<input v-model.number="amount" type="number" min="0" />
<select v-model="from">
<option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
</select>
</div>
<button @click="swap" aria-label="Swap currencies">⇅</button>
<div class="row">
<output>{{ loading ? '...' : converted }}</output>
<select v-model="to">
<option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
</select>
</div>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template><CurrencyConverter />를 App.vue에 넣으면 동작하는 변환기가 완성됩니다. 첫 fetch가 돌아오면 통화 목록이 채워집니다. 금액 입력 필드에 입력하면 네트워크 요청 없이 결과가 반응형으로 갱신됩니다. 통화 교체 버튼은 드롭다운을 재배열합니다. "from" 통화 변경은 디바운스된 재요청을 트리거합니다.
5단계: 크로스 환율 변환 (요청을 낭비하지 않기)
한 가지 기준에서만 변환한다면 이 섹션은 건너뛸 수 있습니다. 그러나 대부분의 변환기는 사용자가 양쪽을 선택할 수 있게 하며 — 순진한 구현은 "from" 변경마다 다시 받습니다.
크로스 환율 계산이 이를 해결합니다. USD를 기준으로 EUR과 JPY의 대 USD 환율이 있다면, EUR → JPY 환율은 그저 (1 / EUR_rate) * JPY_rate입니다. 3단계의 convert 함수가 정확히 그 일을 합니다:
return (amount / fromRate) * toRate즉 세션당 한 번만 받으면 됩니다. 무료 할당량에 큰 이득. from의 watcher는 방어 수단이 됩니다 — 사용자가 캐시된 환율 표에 없는 이국적인 통화를 고르면, 그 통화를 기준으로 다시 받음으로써 변환이 계속 동작합니다.
6단계: 로딩 스켈레톤과 에러 상태
스피너도 괜찮지만, 환율 로딩 중 레이아웃이 흔들리는 건 거슬립니다. 첫 fetch 동안 자리표시자 옵션을 렌더링:
<select v-model="from" :disabled="loading && currencies.length === 0">
<option v-if="currencies.length === 0">Loading...</option>
<option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
</select>에러 경로에서는 막다른 메시지 대신 재시도 버튼을 보여줍니다:
<div v-if="error" class="error-box">
<p>{{ error }}</p>
<button @click="fetchRates(from)">Retry</button>
</div>예의를 갖추려면 429(레이트 제한)와 5xx를 분리해서 처리하세요. Finexly의 월 1,000회 무료 요청과 5분 캐시면 정말로 큰 스파이크가 와야 한도에 닿습니다 — 그래도 깔끔한 재시도 경로가 UI를 든든하게 합니다.
7단계: 서버 프록시로 API 키 숨기기
import.meta.env.VITE_*에 있는 모든 것은 클라이언트 번들에 들어갑니다. 대부분의 읽기 전용 통화 위젯에서는 허용 가능한 수준입니다 — 최악은 누군가 키로 환율을 긁어가는 정도. 다층 방어가 필요하면 서버 측에서 요청을 프록시하세요.
Nuxt 3 서버 라우트 예:
// server/api/rates.get.ts
export default defineEventHandler(async (event) => {
const { base } = getQuery(event)
const config = useRuntimeConfig()
return await $fetch(
`https://api.finexly.com/v1/latest?base=${base}&apikey=${config.finexlyApiKey}`
)
})그런 다음 컴포저블이 Finexly origin 대신 /api/rates?base=${base}를 가리키게 하세요. 키는 절대 서버를 떠나지 않습니다. 같은 패턴이 모든 Vue 메타프레임워크에서 동작합니다 — Nuxt, Quasar, 그냥 Express 백엔드까지.
8단계: 프로덕션 체크리스트
배포 전:
- 공격적으로 캐시. FX 환율은 매초 변하지 않습니다. 클라이언트 5분 TTL과 서버 1분 TTL이면 거의 모든 표시 용도에 충분합니다. 프로덕션에서 검증된 패턴은 캐싱 및 에러 처리 가이드를 참고하세요.
- 올바르게 반올림. 통화 표시는 통화의 최소 단위에 맞춰 반올림합니다 (USD 2자리, JPY 0자리, KWD 3자리).
.toFixed(2)대신Intl.NumberFormat(locale, { style: 'currency', currency: to.value })를 사용하세요. Intl로 포맷팅.new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(123.45)는 "€123.45"를 주고 사용자 로케일을 존중합니다.- SSR 안전 fetch. Nuxt에서는
onMounted안의 생fetch보다useFetch를 선호하세요. 서버 렌더 중에 환율이 사용 가능해 hydration 불일치를 피할 수 있습니다. - 할당량 모니터링. 월 요청 예산의 80%를 소진하면 경고하는 작은 로거를 추가하세요. 더 많은 트래픽에는 요금제가 무료 티어 끝에서 시작합니다.
Vue 2 (Options API) 변형
Vue 2를 쓰고 있다면 같은 로직을 거의 줄 단위로 옮길 수 있습니다:
export default {
data() {
return { rates: {}, loading: false, error: null, amount: 100, from: 'USD', to: 'EUR' }
},
computed: {
converted() {
const fr = this.rates[this.from]
const tr = this.rates[this.to]
return fr && tr ? ((this.amount / fr) * tr).toFixed(2) : '0.00'
},
currencies() {
return Object.keys(this.rates).sort()
},
},
mounted() { this.fetchRates(this.from) },
watch: {
from(newBase) { this.fetchRates(newBase) },
},
methods: {
async fetchRates(base) {
this.loading = true
try {
const res = await fetch(
`https://api.finexly.com/v1/latest?base=${base}&apikey=${process.env.VUE_APP_FINEXLY_KEY}`
)
const data = await res.json()
this.rates = data.rates
} catch (e) {
this.error = e.message
} finally {
this.loading = false
}
},
},
}Composition API는 컴포넌트 간 로직 공유에 더 깔끔하지만, 단일 변환기 위젯이라면 Options API도 잘 동작합니다. 팀이 더 함수적인 접근을 선호한다면 JavaScript 통합 가이드가 프레임워크 비종속 패턴을 다룹니다.
흔한 함정 (그리고 피하는 법)
Nuxt의 hydration 불일치. onMounted에서 fetch를 호출하면 클라이언트에서는 동작하지만 SSR 일관성을 깹니다. 대신 useFetch나 useAsyncData를 사용하세요.
긴 세션 후 오래된 환율. 위의 5분 TTL은 하루 종일 열린 탭이 1시간 전 환율을 보여준다는 뜻. visibilitychange로 새로고침:
document.addEventListener('visibilitychange', () => {
if (!document.hidden) fetchRates(from.value)
})부동 소수점 드리프트. JavaScript 숫자는 double입니다. 0.1 + 0.2 !== 0.3. 금액에는 정수 센트로 곱한 뒤(amount * 100) 계산하고 다시 나눠주세요. 또는 결제 관련 작업에는 dinero.js 같은 라이브러리를 사용하세요.
개발 중 CORS 에러. 일부 통화 API는 브라우저 직접 호출을 금지합니다. Finexly는 클라이언트 사용을 위해 브라우저 origin을 허용하며, 그 외에는 7단계의 프록시가 해결합니다.
왜 Vue 프로젝트에 Finexly인가
프런트엔드 앱용 환율 API 선택 시 중요한 것 몇 가지: 응답 시간, 정확도, 무료 티어의 관대함, 깔끔한 JSON. Finexly는 네 가지 모두를 노립니다 — sub-50ms p95 지연, 60초마다 갱신되는 mid-market 환율, 170개 이상의 통화, 가공 없이 Vue ref에 바로 들어가는 JSON 형태.
대안과의 비교가 궁금하다면 통화 API 비교가 트레이드오프를 자세히 다룹니다. 또는 나란히 비교 페이지를 사용하세요.
자주 묻는 질문
이 튜토리얼을 Vue 2에서도 쓸 수 있나요?
네. 컴포저블 패턴은 Vue 3 전용이지만, 기저 로직 — 환율을 받아 data에 저장하고 변환을 계산하는 — 은 Options API에서도 동일하게 동작합니다. 위의 Vue 2 예시가 그대로 대체됩니다.
Finexly API는 Vue 프로젝트에 무료인가요?
무료 플랜은 월 1,000회 요청을 주며, 사이드 프로젝트, 포트폴리오, 작은 SaaS에는 충분합니다. 5분 캐시와 함께라면 일일 활성 사용자 약 200명을 지원합니다. 더 큰 트래픽은 요금제를 보세요.
API 키가 클라이언트 번들에 노출되지 않게 하려면?
7단계처럼 서버 라우트를 통해 요청을 프록시하세요. VITE_*와 NUXT_PUBLIC_* 접두사 모두 변수를 클라이언트에 보이게 만듭니다. 민감한 것은 서버 함수 뒤에 있어야 합니다.
환율은 얼마나 정확한가요?
Finexly는 여러 Tier-1 유동성 공급자의 mid-market 환율을 집계해 60초마다 갱신합니다. 표시와 대부분의 가격 책정 응용에 충분히 정확합니다. 거래 실행에는 스트리밍 피드가 필요합니다 — REST 대 WebSocket 가이드를 보세요.
과거 금액을 변환할 수 있나요?
네 — Finexly의 /historical 엔드포인트는 날짜 파라미터를 받아 임의의 영업일 환율을 반환합니다. 패턴은 위의 /latest 엔드포인트와 동일하며, URL만 바꾸면 됩니다. 과거 환율 API 가이드에서 자세히 다룹니다.
정리
이제 실제 운영 관심사 — 캐싱, 디바운싱, 에러 상태, SSR, API 키 보안 — 를 처리하는 Vue 3 환율 변환기를 갖췄습니다. 컴포저블 패턴 덕에 같은 useCurrencyRates를 내비게이션 바 위젯, 결제 페이지, 대시보드 차트에 fetch 로직을 다시 작성하지 않고 떨어뜨릴 수 있습니다.
자신의 프로젝트에서 시도할 준비가 됐나요? Finexly 무료 API 키 받기 — 신용카드 불필요. 월 1,000회 무료 요청으로 시작하고 트래픽이 무료 티어를 넘어설 때 업그레이드하세요.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →