시작은 쪼끄맣게
뭐든지 성취는 작은 것부터 하랬다. 당장에 결과를 볼 수 있는 세미 프로젝트부터 해봤다.
JS로 만드는 환율 계산기이며 성취할 목록은 아래와 같다.
- 크롬 앱을 만든다
- 환율 변환 OpenAPI를 이용한다
- 사용자는 A통화로부터 B통화로 변환시킬 수 있어야 한다
- 사용자는 최근 사용한 통화목록을 조회할 수 있어야 한다
로고부터 만들어보자
위 친구에게 적절히 귤이미지 들어간 환율변환기 로고 만들어 달랬다. 맘에 드는 게 나오면 포토샵으로 세미편집 후 크기(16, 48, 128 px)별로 만든다.
크롬 앱을 만들기 위한 프로젝트의 기본 구조
구글측에선 위와 같이 심플하게 설명했긴한데 따로 더 설명하자면 아래와 같은 시각화를 할 수 있다.
✔️ 심플 버전
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로 환율정보를 가져온다.
/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,--,**가 있다.