[KCC] 환율 변환기 크롬 앱 만들기 - 1(기본기능)

 

시작은 쪼끄맣게

뭐든지 성취는 작은 것부터 하랬다. 당장에 결과를 볼 수 있는 세미 프로젝트부터 해봤다.

JS로 만드는 환율 계산기이며 성취할 목록은 아래와 같다.

 

- 크롬 앱을 만든다

- 환율 변환 OpenAPI를 이용한다

- 사용자는 A통화로부터 B통화로 변환시킬 수 있어야 한다

- 사용자는 최근 사용한 통화목록을 조회할 수 있어야 한다

 

 

로고부터 만들어보자

 

Microsoft Designer - Stunning designs in a flash

A graphic design app that helps you create professional quality social media posts, invitations, digital postcards, graphics, and more. Start with your idea and create something unique for you.

designer.microsoft.com

위 친구에게 적절히 귤이미지 들어간 환율변환기 로고 만들어 달랬다. 맘에 드는 게 나오면 포토샵으로 세미편집 후 크기(16, 48, 128 px)별로 만든다.

 

 

크롬 앱을 만들기 위한 프로젝트의 기본 구조

 

맞춤 Chrome 앱 및 확장 프로그램 만들기 및 게시하기 - Chrome Enterprise and Education 고객센터

도움이 되었나요? 어떻게 하면 개선할 수 있을까요? 예아니요

support.google.com

구글측에선 위와 같이 심플하게 설명했긴한데 따로 더 설명하자면 아래와 같은 시각화를 할 수 있다.

 

✔️ 심플 버전

extension/
├── manifest.json             // 필수: 익스텐션의 설정 파일
├── background.js             // 필수는 아님: 백그라운드 스크립트
├── content.js                // 선택: 웹 페이지와 상호작용하는 스크립트
├── popup.html                // 선택: 팝업 페이지의 UI
└── icon.png                  // 선택: 익스텐션 아이콘

가장 심플한 구조다.

- manifest.json은 확장 프로그램의 이름, 버전, 권한, 스크립트 실행 등에 대한 정보를 포함한다.

- background.js는 크롬 API를 사용하거나 이벤트를 처리하는 데 사용된다. 이름명에 보이듯이 백그라운드에서 특정 작업을 주기적으로 실행하거나 알림 표시하는 데에 쓸 수 있다.

- content.js는 웹페이지와 상호작용하는 곳이다. 사용자가 방문한 웹사이트 내용을 읽거나, 수정하거나, 기능을 추가하거나 한다.

- popup.html는 내 기준 필수적인 요소다. 크롬 내 상단바에 익스텐션 아이콘으로 클릭 시 나타나는 UI다.

 

 

✔️ 고도화 버전

extension/
├── manifest.json             // 필수: 익스텐션의 메타데이터 설정 파일
├── background/
│   └── background.js         // 백그라운드 스크립트 (서비스 워커)
├── content/
│   └── content.js            // 콘텐츠 스크립트 (웹페이지에 삽입되는 스크립트)
├── popup/
│   ├── popup.html            // 팝업 페이지 HTML
│   ├── popup.js              // 팝업 페이지 JS
│   └── popup.css             // 팝업 페이지 CSS
├── options/
│   ├── options.html          // 옵션 페이지 HTML
│   ├── options.js            // 옵션 페이지 JS
│   └── options.css           // 옵션 페이지 CSS
├── assets/
│   ├── icon128.png           // 아이콘 (128x128)
│   ├── icon48.png            // 아이콘 (48x48)
│   └── icon16.png            // 아이콘 (16x16)
├── _locales/
│   └── en/
│       └── messages.json     // 다국어 지원 메시지 (영문 예시)
└── styles/
    └── common.css            // 공통 스타일시트

만약 AdBlock 이라거나 Momentum 처럼 규모가 커지는 앱이라면 위와 같이 각 영역별로 세분화한 프로젝트 구조를 세울 수도 있다.

나는 초초미니이기에 심플 구조로 가져간다.

 

 

프로젝트 빌딩


✔️ manifest.json

{
    "manifest_version": 3,
    "name": "환율 계산기",
    "version": "1.0",
    "description": "현 시간의 최신 환율을 변환해줍니다.",
    "action": {
        "default_popup": "popup.html",
        "default_icon": {
            "16": "icon16.png",
            "48": "icon48.png",
            "128": "icon128.png"
        }
    },
    "icons": {
        "16": "icon16.png",
        "48": "icon48.png",
        "128": "icon128.png"
    },
    "permissions": [
        "storage"
    ]
}

- action.default_icon: 아까 만든 사이즈별 아이콘을 넣어준다. 툴바에 표시될 예정

- icons: 확장 프로그램 관리 페이지와 Chrome 웹 스토어에 표시될 이미지다

- permissions: 권한으로 storage을 넣어 최근사용기록, 추후 즐겨찾기 저장 기능을 구현토록 한다

 

✔️ popup.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>환율 계산기</title>
    <link rel="stylesheet" href="styles.css">
</head>

<body>
    <div class="converter">
        <h1>환율 계산기</h1>
        <label for="amount">금액:</label>
        <input type="number" id="amount" placeholder="Enter amount">

        <label for="from">현재 통화:</label>
        <select id="from"></select>

        <label for="to">변환 통화:</label>
        <select id="to"></select>

        <button id="convert">CONVERT!</button>
        <p id="result"></p>
    </div>
    <script src="popup.js"></script>
</body>

</html>

여기는 확장 프로그램의 UI를 구성하고 있다.

간단히 금액을 입력하고 통화를 선택하면 변환 결과가 표시되도록 했다. 더불어 이 화면 구성을 위한 css는 Cursor에게 맡겼다.

 

✔️popup.js

const apiBaseURL = `{API}`;

const amountInput = document.getElementById("amount");
const fromCurrency = document.getElementById("from");
const toCurrency = document.getElementById("to");
const convertButton = document.getElementById("convert");
const resultDisplay = document.getElementById("result");

async function fetchCurrencies() {
    try {
        const response = await fetch(`${apiBaseURL}/currencies.json`);
        const data = await response.json();

        // 주요 통화 목록
        const majorCurrencies = {
            usd: "US Dollar",
            eur: "Euro",
            jpy: "Japanese Yen",
            krw: "South Korean Won",
            gbp: "British Pound",
            cny: "Chinese Yuan",
            aud: "Australian Dollar",
            cad: "Canadian Dollar"
        };

        // 기타 통화 목록 (주요 통화 제외)
        const otherCurrencies = Object.entries(data)
            .filter(([code]) => !majorCurrencies[code] && !code.includes('crypto'))
            .reduce((acc, [code, name]) => ({...acc, [code]: name}), {});

        // Select 엘리먼트 업데이트
        [fromCurrency, toCurrency].forEach(select => {
            // 주요 통화 추가
            Object.entries(majorCurrencies).forEach(([code, name]) => {
                const option = document.createElement("option");
                option.value = code;
                option.textContent = `${code.toUpperCase()} - ${name}`;
                select.appendChild(option);
            });

            // 구분선 추가
            const separator = document.createElement("option");
            separator.disabled = true;
            separator.textContent = "──────────";
            select.appendChild(separator);

            // 기타 통화 추가
            Object.entries(otherCurrencies).forEach(([code, name]) => {
                const option = document.createElement("option");
                option.value = code;
                option.textContent = `${code.toUpperCase()} - ${name}`;
                select.appendChild(option);
            });
        });
    } catch (error) {
        console.error("Failed to fetch currencies:", error);
        resultDisplay.textContent = "Error loading currency list.";
    }
}

async function convertCurrency() {
    const amount = parseFloat(amountInput.value);
    const from = fromCurrency.value.toLowerCase();
    const to = toCurrency.value.toLowerCase();

    if (isNaN(amount) || !from || !to) {
        resultDisplay.textContent = "Please fill in all fields.";
        return;
    }

    const url = `${apiBaseURL}/currencies/${from}.json`;
    try {
        // 'from' 통화 데이터 가져오기
        const response = await fetch(url);

        if (!response.ok) {
            throw new Error(`Failed to fetch. Status: ${response.status}`);
        }
        const data = await response.json();

        // 'to' 통화에 대한 환율 찾기
        const rate = data[from][to];
        if (!rate) {
            resultDisplay.textContent = `Conversion rate from ${from.toUpperCase()} to ${to.toUpperCase()} not available.`;
            return;
        }

        // 변환 결과 계산
        const result = (amount * rate).toFixed(2);
        resultDisplay.textContent = `${amount} ${from.toUpperCase()} = ${result} ${to.toUpperCase()}`;
    } catch (error) {
        console.error("Conversion failed:", error);
        // resultDisplay.textContent = "Error fetching exchange rate.";
        // resultDisplay.textContent += error;
        resultDisplay.textContent = url;
    }
}

fetchCurrencies();
convertButton.addEventListener("click", convertCurrency);

 

우선 Github에 공개된 OpenAPI로 환율정보를 가져온다. 

 

GitHub - fawazahmed0/exchange-api: Free Currency Exchange Rates API with 200+ Currencies & No Rate Limits

Free Currency Exchange Rates API with 200+ Currencies & No Rate Limits - fawazahmed0/exchange-api

github.com

/currencies.json으로 가져오면 너무 많은 목록들을 한꺼번에 보이니, 목록 상단에 주요 통화 목록을 따로 하드코딩했다.

그럼 SelectBar에선 아래와 같이 구분선을 기준으로 볼 수 있게 된다.

 

 

 

개발자모드로 프로젝트 적용하기


크롬 Setting > extentions > Load unpacked > 경로 선택 

간단히 적용해볼 수 있다.

 

어마무시한 환율을 볼 수 있다

 

 

개선하기


✔️최근 사용한 통화 관리하기

최근 목록을 만들려면 chrome.storage.local (get/set)을 이용한다.

이는 크롬 확장에서 데이터를 저장하고 가져오는 데 사용되는 API이고, 로컬 혹은 세션 스토리지랑은 다른 것이다.

이 데이터는 사용자의 브라우저에만 저장되고 동기화 되진 않는다. 데이터를 명시적으로 삭제하지 않으면 브라우저를 닫아도 유지된다. 또한 약 5mb까지 데이터를 저장할 수 있는 것으로 알려진다.

 

async function getRecentCurrencies() {
    const result = await chrome.storage.local.get('recentCurrencies');
    return result.recentCurrencies || [];
}

 

우선 사용하고 있는 recentCurrencices라는 키에 저장된 데이터를 가져온다.

 

async function addToRecentCurrencies(fromCurrency, toCurrency) {
    const recentCurrencies = await getRecentCurrencies();
    const currencies = [fromCurrency, toCurrency];
    
    // 중복 제거 및 최신 순서로 정렬
    currencies.forEach(currency => {
        const index = recentCurrencies.indexOf(currency);
        if (index > -1) {
            recentCurrencies.splice(index, 1);
        }
        recentCurrencies.unshift(currency);
    });
    
    // 최대 5개까지만 유지
    const updatedRecent = recentCurrencies.slice(0, 5);
    await chrome.storage.local.set({ recentCurrencies: updatedRecent });
    
    // 통화 목록 새로고침
    await fetchCurrencies();
}

 

그다음 적절히 배열처리를 해준 뒤 chrome.storage.local.set으로 업데이트 해준다.

그러면 아래처럼 최근 사용을 같이 볼 수 있다 (최대 5개)

 

 

✔️ 즐겨찾기 기능 만들기

최근 통화말고 즐겨찾기 기능을 따로 만들어버리면 어떨까?

 

▪️ 영역추가

...
	<button class="favorite-btn" title="즐겨찾기에 추가">
        <i class="far fa-star"></i>
    </button>

..

<div class="favorites-container">
    <h3>즐겨찾기</h3>
    <div id="favorites"></div>
</div>

즐겨찾기를 보여줄 영역을 추가해본다

 

 

▪️ 즐겨찾기 데이터 관리

// 즐겨찾기 목록 가져오기
async function getFavorites() {
    const result = await chrome.storage.local.get('favoritePairs');
    return result.favoritePairs || [];
}

// 즐겨찾기 추가
async function addToFavorites(fromCurrency, toCurrency) {
    const favorites = await getFavorites();
    const pair = `${fromCurrency}-${toCurrency}`;
    
    if (!favorites.includes(pair)) {
        favorites.push(pair);
        await chrome.storage.local.set({ favoritePairs: favorites });
        await updateFavoritesDisplay();
        updateFavoriteButton();
    }
}

// 즐겨찾기 제거
async function removeFromFavorites(pair) {
    const favorites = await getFavorites();
    const index = favorites.indexOf(pair);
    
    if (index > -1) {
        favorites.splice(index, 1);
        await chrome.storage.local.set({ favoritePairs: favorites });
        await updateFavoritesDisplay();
        updateFavoriteButton();
    }
}

특별한 로직은 없다. 목록을 가져오고, 추가해주고, 제거해주면 된다.

 

▪️ 즐겨찾기 UI 업데이트하기

// 즐겨찾기 목록 표시 업데이트
async function updateFavoritesDisplay() {
    const favorites = await getFavorites();
    const favoritesContainer = document.getElementById('favorites');
    favoritesContainer.innerHTML = '';
    
    favorites.forEach(pair => {
        const [from, to] = pair.split('-');
        const element = document.createElement('div');
        element.className = 'favorite-pair';
        element.innerHTML = `
            ${from.toUpperCase()} ↔ ${to.toUpperCase()}
            <button class="remove-favorite" data-pair="${pair}">
                <i class="fas fa-times"></i>
            </button>
        `;
        
        // 즐겨찾기 쌍 클릭 시 자동 선택
        element.onclick = async (e) => {
            if (!e.target.closest('.remove-favorite')) {
                fromCurrency.value = from;
                toCurrency.value = to;
                await updateFavoriteButton(); // 별 상태 업데이트 추가
            }
        };
        
        favoritesContainer.appendChild(element);
    });
}

// 즐겨찾기 버튼 상태 업데이트
async function updateFavoriteButton() {
    const fromCurr = fromCurrency.value;
    const toCurr = toCurrency.value;
    const currentPair = `${fromCurr}-${toCurr}`;
    const favorites = await getFavorites();
    const isActive = favorites.includes(currentPair);
    
    favoriteBtn.classList.toggle('active', isActive);
    favoriteBtn.innerHTML = `<i class="${isActive ? 'fas' : 'far'} fa-star"></i>`;
}

 

데이터가 업데이트되면 목록도 표시해줘야한다.

현재환율(from)와 변환환율(to)을 연결해 준 뒤, 이 내용을 즐겨찾기의 항목으로써 보여준다. 그리고 클릭하면 삭제되게끔 이벤트를 만들어준다. 이후, 이미 선택된 환율 관계에 대해선 active 효과를 주도록 만든다.

 

▪️ 이벤트 처리

// 즐겨찾기 버튼 클릭 이벤트
const favoriteBtn = document.querySelector('.favorite-btn');
favoriteBtn.onclick = async () => {
    const fromCurr = fromCurrency.value;
    const toCurr = toCurrency.value;
    const pair = `${fromCurr}-${toCurr}`;
    const favorites = await getFavorites();
    
    if (favorites.includes(pair)) {
        await removeFromFavorites(pair);
    } else {
        await addToFavorites(fromCurr, toCurr);
    }
};

// 통화 선택 변경 시 즐겨찾기 버튼 상태 업데이트
[fromCurrency, toCurrency].forEach(select => {
    select.addEventListener('change', updateFavoriteButton);
});

최종적으로 onclick 이벤트를 달아주면 끝난다.

이제 여기서부터 시작하는 확장기능은 00,--,**가 있다.