[Firebase] Gemini API를 활용한 웹 서비스 배포하기
[ 목표 ]
1. firebase로 프론트엔드, 백엔드 배포하기
2. Gemini API 사용하기
[ 단계 ]
1. 기본 설정
1-1. firebase project 생성
1-2. firebase CLI설치
1-3. firebase Login
2. 프로젝트 구성
2-1. 클라이언트 사이드(프론트엔드)
hosting 초기화 및 배포
2-2. 서버 사이드(백엔드)
functions 초기화 및 배포
3. 프로젝트 완성
3-1. 코드 수정
3-2. 재배포
[ 결과물 ]
Gemini API를 사용하여 꿈을 해석해주는 웹 서비스를 만들고 firebase로 배포
1. 기본 설정
1. Firebase 프로젝트 생성
Firebase 프로젝트 시작하기 선택
프로젝트 이름 지정하기
Google 애널리틱스 설정
프로젝트 생성 완료!!
* Blaze 요금제로 변경
추후 functions를 사용하기 위해 요금제 변경이 필요하다. Spark 요금제에서 Blaze 요즘제로 변경한다.
결제 설정이 되어 있지 않다면 구글 결제 정보 페이지로 넘어가진다.
주소 및 결제 카드 정보를 입력하며 모두 한글로 작성하면 된다.
정보를 모두 입력 하면 Firebase 화면으로 돌아온다. 구매 버튼을 눌러 Blaze 요금제로 변경한다.
2. Firebase CLI 설치
npm install -g firebase-tools
3. Firebase Login
firebase login
로그인 완료!!!
만약 로그아웃하고 싶다면 firebase logout 입력하면 된다.
2. 프로젝트 기본 구조
2-1. 클라이언트 사이드 (프론트엔드)
웹 브라우저에서 실행할 수 있는 클라이언트 사이드 웹을 구축한다.
사용자가 텍스트를 입력하면 입력값을 출력하는 간단한 서비스를 구축한다.
파일 구조: index.html / index.js
2-1-1. 코드 작성하기
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>텍스트 출력 서비스</title>
</head>
<body>
<h1>텍스트 출력 서비스</h1>
<input type="text" id="inputText" placeholder="텍스트를 입력하세요">
<button onclick="printText()">확인</button>
<p id="outputText"></p>
<script src="index.js"></script>
</body>
</html>
index.js
function printText() {
const inputText = document.getElementById('inputText').value;
const outputText = document.getElementById('outputText');
outputText.textContent = inputText;
}
이렇게 사용자가 텍스트를 입력하면 그대로 출력한다.
2-1-2. 호스팅 초기화
내가 작성한 프로젝트 파일을 Firebase 프로젝트에 연결하기 위한 기본 설정이다.
Project Setup과 Hosting Setup의 설정값은 아래 이미지를 참고하기 바란다.
firebase init hosting
호스팅 초기화가 완료되면 내가 지정한 public폴더와 index.html이 생성되며, firebase 관련 파일도 확인할 수 있다.
public말고 다른 폴더로도 사용 가능하다.
2-1-3. 호스팅 배포하기
firebase deploy --only hosting
호스팅 배포가 완료된 후 Hosting URL로 접속한다.
호스팅 설정이 완료 되었다고 나오지만 내가 작성한 코드가 반영이 되어 있지 않다.
새로고침을 눌러도 해당 화면만 계속 나온다.... 여기서 시간을 많이 잡아 먹었다....
알아보니 Firebase Hosting은 초기화하면서 설정한 public폴더를 공개형 루트 디렉토리로 사용하고 public폴더 안의 index.html을 entry point(기본 진입점)으로 사용한다.
*호스팅 참고 : https://firebase.google.com/docs/hosting/quickstart?hl=ko
나는 기본 호스팅 URL 설정을 사용하였기 때문에 프로젝트의 호스팅 URL로 접근하면, public/index.html이 제공되는 것이었다. 다른 폴더를 사용하고 싶으면 firebase.json파일에서 hosting 섹션을 수정하여 다른 폴더를 지정하면 된다.
나는 경로를 설정하는 방법 대신 index.html코드를 직접 수정하는 방식을 택하였다.
따라서 public 폴더의 index.html 코드를 삭제한 후 내가 작성한 index.html 코드로 붙여넣는다.
또한, 앞서 작성한 index.js 파일도 public 폴더로 이동한다.
수정이 완료 되었다면 호스팅을 재배포한다.
firebase deploy --only hosting
Hosting URL로 접속해보면 웹이 잘 동작하는 것을 확인할 수 있다.
* 만약 화면이 여전히 "Firebase Hosting Setup Complete"가 뜬다면 새로고침을 눌러보거나 쿠키를 삭제해본다.
크롬 설정> 개인 정보 보호 및 보안> 인터넷 사용 기록 삭제를 눌러 쿠키를 삭제한 후 다시 테스트 진행하기
이렇게 firebase로 웹 배포까지 완료하여 가장 기본 구조를 완성하였다.
다음으로는 서버에서 실행 가능한 서버 사이드 서비스를 구축하고 배포한다.
2-2. 서버 사이드 (백엔드)
최종 목표는 Gemini API를 이용하여 사용자가 입력한 질문에 대한 답변을 출력하는 것이다.
2-2-1. Gemini API Key발급하기
Get API key > API 키 만들기
Firebase에서 만든 프로젝트 선택 후 생성된 API Key를 복사한다.
API Key 발급이 완료되었다면 이제 서버 사이드 구축을 위해 먼저 functions를 초기화한다.
2-2-2. functions 초기화
firebase init functions
초기화가 완료되면 functions 폴더가 생성된다.
2-2-3. functions폴더 안의 index.js 수정하기
public의 index.html과 마찬가지로 functions의 index.js도 다음과 같이 수정한다.
const { onRequest } = require("firebase-functions/v2/https");
const { GoogleGenerativeAI } = require("@google/generative-ai");
const cors = require('cors')({ origin: true });
const API_KEY = "AIzaSyCPrvsR4F-ZOXbaIrO720WzwAWLAzcaYEM";
const genAI = new GoogleGenerativeAI(API_KEY);
exports.answerQuestion = onRequest(
{ cors: true },
async (req, res) => {
// Use CORS middleware
cors(req, res, async () => {
// 입력된 질문 받기
const prompt = req.query.question || "너에 대해 설명해줘";
try {
// Gemini 모델 설정 및 질문 전송
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
const result = await model.generateContent(prompt);
const response = await result.response;
const text = await response.text();
// 응답 전송
res.status(200).send(text);
} catch (error) {
console.error("Error generating response:", error);
res.status(500).send("Error generating response");
}
});
}
);
index.js 파일에서 API_KEY에 앞서 발급받은 Gemini API Key 입력한다.
2-2-4. 패키지 설치 및 package.json / package-lock.json 확인
1. Gemini API를 사용하기 위해 Node.js용 GoogleGenerativeAI 패키지를 설치
npm install @google/generative-ai
2. cors 설치
npm install cors
3. functions 폴더의 package.json와 package-lock.json에 설치된 패키지 확인
2-2-5. functions 배포 (시간 좀 걸림)
firebase deploy — only functions
Funtion URL로 접근하면 다음과 같이 gemini api가 잘 작동하는 것을 확인할 수 있다.
firebase Functions에서도 확인가능
Google Cloud에서도 확인 가능하다.
도커 이미지로 배포되어 있으며 Cloud Functions의 URL을 사용하여 호출할 수 있다.
2-2-6. 프론트엔드 수정 후 hosting 재배포
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>텍스트 출력 서비스</title>
<script>
async function displayQuestion() {
var inputText = document.getElementById("inputText").value;
// Firebase function 호출
try {
const response = await fetch(`https://us-central1-test-project-be45c.cloudfunctions.net/answerQuestion?question=${encodeURIComponent(inputText)}`, {
method: 'GET'
});
const result = await response.text();
document.getElementById("display").innerText = result;
document.getElementById("display").classList.remove("error");
} catch (error) {
console.error("Error calling Firebase Function:", error);
document.getElementById("display").innerText = "Error calling function";
document.getElementById("display").classList.add("error");
}
}
</script>
</head>
<body>
<div class="container">
<h1>텍스트 출력 서비스</h1>
<input type="text" id="inputText" placeholder="텍스트를 입력하세요">
<button onclick="displayQuestion()">확인</button>
<div id="display" class="result"></div>
</div>
</body>
</html>
hosting 재배포
firebase deploy — only hosting
hosting, functions 모두 재배포 완료 후 Hosting URL로 접속한다.
다음과 같이 잘 작동하는 것을 볼 수 있다.
만약 또 수정한 웹 페이지가 적용되지 않는다면 앞에서 한 것처럼 쿠키를 삭제하고 다시 접속하기 바란다.
3. 프로젝트 완성
앞서 진행한 프로젝트를 보완해서 완성해보겠다.
사용자가 꿈에 대한 내용을 입력하면 해석해주는 서비스를 제작한다.
3-1. index.html 코드 수정
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>해몽 서비스</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f0f2f5;
height: 100vh; /* Viewport height */
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 80%;
max-width: 700px;
padding: 20px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
box-sizing: border-box; /* Ensure padding is included in width calculation */
}
h1 {
text-align: center;
color: #333;
font-size: 24px;
margin-bottom: 20px;
}
textarea {
width: 100%; /* Ensure textarea takes up full width of container */
height: 120px; /* Default height of approximately 4 lines */
padding: 12px;
margin-bottom: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box; /* Include padding in width calculation */
overflow-y: auto; /* Add vertical scrollbar if content exceeds */
}
button {
width: 100%; /* Ensure button takes up full width of container */
padding: 12px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: #ffffff;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
box-sizing: border-box; /* Include padding in width */
}
button:hover {
background-color: #0056b3;
}
.result {
margin-top: 20px;
padding: 15px;
background-color: #e9ecef;
border-radius: 4px;
border: 1px solid #ddd;
min-height: 100px;
line-height: 1.5;
box-sizing: border-box; /* Include padding in width calculation */
}
.error {
color: #dc3545;
}
</style>
<script>
async function displayQuestion() {
var inputText = document.getElementById("inputText").value;
// Firebase function 호출
try {
const response = await fetch(`https://us-central1-test-project-be45c.cloudfunctions.net/answerQuestion?question=${encodeURIComponent(inputText)}`, {
method: 'GET'
});
const result = await response.text();
document.getElementById("display").innerText = result;
document.getElementById("display").classList.remove("error");
} catch (error) {
console.error("Error calling Firebase Function:", error);
document.getElementById("display").innerText = "Error calling function";
document.getElementById("display").classList.add("error");
}
}
</script>
</head>
<body>
<div class="container">
<h1>꿈 풀이</h1>
<textarea id="inputText" placeholder="꿈에 대한 내용을 자세히 작성하세요"></textarea>
<button onclick="displayQuestion()">분석</button>
<div id="display" class="result"></div>
</div>
</body>
</html>
3-2. index.js 코드 수정
const { onRequest } = require("firebase-functions/v2/https");
const { GoogleGenerativeAI } = require("@google/generative-ai");
const cors = require('cors')({ origin: true });
const API_KEY = "AIzaSyCPrvsR4F-ZOXbaIrO720WzwAWLAzcaYEM";
const genAI = new GoogleGenerativeAI(API_KEY);
exports.answerQuestion = onRequest(
{ cors: true },
async (req, res) => {
// Use CORS middleware
cors(req, res, async () => {
// 입력된 질문 받기
const userinput = req.query.question || "꿈에 대해 설명해줘";
const prompt = `Be sure to answer in Korean. Do not exceed 5 sentences. You are an expert on dreams. Analyze the following dream and tell me what it means: "${userinput}"`;
try {
// Gemini 모델 설정 및 질문 전송
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
const result = await model.generateContent(prompt);
const response = await result.response;
const text = await response.text();
// 응답 전송
res.status(200).send(text);
} catch (error) {
console.error("Error generating response:", error);
res.status(500).send("Error generating response");
}
});
}
);
3-3. 프론트엔드, 백엔드 재배포
firebase deploy — only hosting
firebase deploy — only functions
최종 프로젝트 완성!!!
재배포 완료 후 Hosting URL로 접속하면 잘 작동한다.

firebase에서 Hosting과 Functions 상태도 확인 가능하다.
추가 1) CORS(Cross-Origin Resource Sharing) 정책과 관련된 에러가 발생하였을 경우
요청된 리소스(Cloud Function)가 'Access-Control-Allow-Origin' 헤더를 포함하고 있지 않아 브라우저가 요청을 차단하여 발생한 문제라고 한다. 코드를 열심히 수정해보았지만.... 해결이 되지 않았다....
Google Cloud의 Cloud Run의 보안 탭에서 "인증되지 않은 호출 허용"으로 변경하였더니 잘 작동한다.