AI로 나만의 무료 앱, 10분 안에 만들기 (Claude + 구글시트)
AI 무료 앱 만들기 목차 바로가기
영상 강의
예제파일 다운로드
오빠두엑셀의 강의 예제파일은 여러분을 위해 자유롭게 제공하고 있습니다.
- [업무생산성] AI로 나만의 무료 앱 10분 안에 만들기보충파일
라이브 강의 전체영상도 함께 확인해보세요!
위캔두 회원이 되시면 매주 오빠두엑셀에서 진행하는 라이브강의 풀영상을 확인하실 수 있습니다.
AI 초보자도 10분이면 됩니다! 나만의 앱을 무료로 개발해보세요
이제 코딩은 더 이상 전문가만의 영역이 아닐 수도 있을 것 같습니다. 오늘 소개해드리는 구글 시트의 앱스 스크립트와 AI 툴을 도구를 활용하면, 누구나 단 10분 만에 나만의 맞춤형 앱을 직접 개발하고 배포까지 할 수 있게 되었습니다. 게다가 서버나 복잡한 설정도 전혀 필요 없이, 구글에서 제공하는 무료 서비스만 활용하여 간단히 개발할 수 있습니다.

이제 아이디어만 있으면 간단하게 나만의 앱을 만들 수 있습니다! 이번 강의에서는 현존 코딩에서 가장 탁월한 능력을 보여주는 Claude와 Gemini API, 그리고 구글 시트의 Apps Script를 활용해서 나만의 맞춤형 앱을 처음부터 끝까지 직접 만들어 봅니다. 여행 가이드, 음식 분석, 식단 관리, 그림 그리기 앱 등등.. 이번 강의를 통해 그 동안 꿈꿔왔던 나만의 앱을 직접 만들어보시길 바랍니다.
구글 AI 스튜디오에서 API키 발급받기
먼저 앱에 AI 기능을 연동하기 위해 구글 Gemini 의 API키를 발급 받겠습니다. Gemini API는 일정 한도를 무료로 제공하기 때문에, 호출이 적은 개인 사용자 (분당 호출 15회, 일 호출 500회 미만)의 경우 부담없이 사용할 수 있습니다.

Gemini 무료 API의 경우 분당/일일 사용량 한도가 있습니다. 구글 Gemini API키의 전체 목록과 요금제별 한도의 자세한 설명은 아래 링크를 확인해보시길 바랍니다.
- 아래 링크를 클릭해서 구글 AI 스튜디오로 이동한 후, API키를 발급 받을 계정으로 로그인합니다. 이후 오른쪽 상단의 [Get API Key](또는 API키 발급 받기) 버튼을 클릭해서 API 키 발급 페이지로 이동합니다.

구글 AI 스튜디오로 이동한 후, API 키 발급받기 버튼을 클릭합니다. - API키를 처음 발급 받을 경우, 오른쪽 상단의 [API키 만들기] 버튼을 클릭하면 API키가 발급됩니다. 만약 기존에 발급받은 이력이 있을 경우, 아래 표에서 기존에 발급받은 API키를 확인할 수 있습니다.

API키 만들기 버튼을 클릭해서 API키를 생성합니다. - API키를 복사해서 워드나 메모장에 보관합니다.

발급받은 API키를 워드나 메모장에 보관합니다.
앱에 활용할 데이터를 구글시트로 구축하기
이제 본격적으로 앱에 사용할 데이터와 인터페이스를 설계해보겠습니다. 예제파일로 준비해드린 프롬프트 가이드를 실행한 후, 만들고자 하는 앱의 설명을 작성합니다. 이번에는 "해외 여행 가이드 앱"을 만든다고 가정하고, 아래 미리 준비해한 프롬프트를 사용해보겠습니다.
자, 너는 지금부터 지상 최고의 구글 Apps Sciprt 웹 앱 개발자야.
이번에 새로운 프로젝트로 "여행 스마트 여행 가이드 앱"을 만들려고 해.
앱 주요 기능과 사용자 흐름은 다음과 같아.== [앱 주요 기능] 시작 ==
1. 사용자가 여행할 나라나 지역 또는 도시를 입력하고, 인원수(혼자 가는지, 여러명이서 가는지), 성별,나이, 여행 스타일, 여행 일 수, 예산 규모 등을 선택하면 구글 Gemini API를 사용해서 여행 경로를 추천해주는 웹 앱을 만들고 싶어.
2. 이 앱은 1회성으로 쓰는 앱이야. 웹 앱 화면에 결과 히스토리를 표시할 필요는 없어.
3. 다만 내부 관리용으로, 출력 결과는 구글 시트에 자동으로 기록.
== [앱 주요 기능] 끝 ==== [사용자 흐름] 시작 ==
1. 사용자가 여행할 나라, 지역 또는 도시를 선택. 이거는 일반 텍스트 상자로 자유롭게 입력하도록 하자.
2. 인원수, 성별, 나이, 여행 스타일(휴양지, 커플여행, 가족여행), 여행 일수(1일~7일까지), 예산 규모(백만원/인당, 2백만원/인당) 등등을 선택.
3. [여행 경로 추천] 버튼을 클릭하면 각 일자별 추천 경로를 출력
== [사용자 흐름] 끝 ==데이터는 구글 시트에서 "기록" 시트 하나로 관리할 거야.
위와 같이 만들려면, 구글 시트의 필드와 화면 인터페이스는 어떻게 구성하면 좋을까?
머리글 순번으로 간략하게 알려줘.
- 이번 강의에서는 클로드(Claude)를 사용합니다. 아래 링크를 클릭해서 클로드로 이동한 후 로그인합니다. (ChatGPT, Gemini 등 사용 중인 AI 모델이 있다면 다른 플랫폼에서 진행합니다.)

클로드에 로그인합니다. - 프롬프트를 실행하면, Claude가 아래 그림과 같이 앱에 사용할 데이터 구조와 앱 인터페이스를 설계합니다. (필드 목록과 인터페이스 구성은 응답마다 조금씩 다를 수 있습니다.)

프롬프트를 실행하면 앱 구동에 필요한 데이터 구조와 인터페이스를 확인할 수 있습니다. - 이제 구글 시트에서 비어있는 시트를 만든 후 통합 문서의 이름은 "여행 가이드 앱", 시트 이름은 "기록" 으로 변경합니다.

구글 시트의 파일명과 시트명을 변경합니다. - Claude의 제안을 참고해서 데이터의 머리글을 입력하면 앱에 사용할 데이터 준비가 끝납니다.

클로드가 제한한 필드를 참고해서 구글 시트 데이터를 구축합니다.
앱에 사용할 코드 작성하기
이제 '코딩'을 시작합니다. 코딩을 전혀 몰라도 걱정하지 마세요! 우리는 단 한 줄의 코드도 직접 작성하지 않을겁니다. 이전 단계에서 Claude가 제안해 준 데이터 구조와 화면 인터페이스를 그대로 참고해서 "이 설계도 대로 앱을 만들어줘!"라고 요청하면 코드를 알아서 작성해줍니다. 이전 응답을 참고해서, 아래와 같이 두번째 프롬프트를 완성합니다. (프롬프트에서 파란색 글씨는 수정해서 사용합니다.)
좋아. 너가 알려준 대로 아래와 같이 구성했어.
웹 앱 개발을 시작해보자. code.gs 와 index.html 코드를 각각 작성해줘.== [백엔드 구성] 시작 ===
1. 사용할 외부 API
- Gemini API : API키
- 모델은 Gemini 2.5 Flash 를 사용할거야.2. 구글 시트 구조
- 구글시트 ID : 구글시트url
- 시트명 : 기록
- 필드 구조 : "머리글 목록"
== [백엔드 구성] 끝 ===== [화면 인터페이스 구성] 시작 ==
- 인터페이스 구조
== [화면 인터페이스 구성] 끝 ==== [추가 요청] 시작 ==
- 모든 개발은 구글 Apps Script + HTML/CSS/JS 로 진행할거야.
- 최신 트렌드에 맞춰서 세련되고 시원한 여름느낌이 물씬 나는 상큼하고 딱 접속했을 때 행복한 감정이 드는 디자인으로 꾸며줘.
- 모바일에 대응할 수 있도록 반응형 CSS로 작성해.
== [추가 요청] 끝 ==
- 코드 작성에 사용할 프롬프트를 완성한 후, 이전 대화 내역에 이어서 프롬프트를 붙여넣고 실행합니다.

이전 대화에 이어서, 코드 작성을 위한 프롬프트를 실행합니다. - 프롬프트를 실행하고 잠시만 기다리면 Claude 가 Apps Script에 사용할 code.gs와 index.html 코드를 작성합니다.
· code.gs: 앱의 동작을 처리하는 코드로, 앱 실행에 필요한 기능과 로직을 포함합니다.
· index.html: 사용자에게 보여지는 앱 화면(인터페이스)를 구성하는 코드입니다.
- Claude 무료 버전의 경우 출력 응답 길이에 제한이 있어 [계속] 버튼이 나올 수 있습니다. 그럴 경우 [계속] 버튼을 클릭해 이어서 코드를 작성합니다.
- 잠시만 기다리면 앱이 완성됩니다. 이제 완성된 코드를 구글시트에 붙여넣으면 나만의 앱을 바로 사용할 수 있습니다.

Apps Script로 앱 완성하기
이제 AI가 작성한 코드를 구글 시트에 붙여넣고 배포하면 나만의 앱이 완성됩니다. 이제 마지막 단계로 구글 시트의 보석 같은 기능인 앱스 스크립트(Apps Script)에 완성된 코드를 옮기는 방법을 알아보겠습니다. 실습 과정에 편의를 위해 code.gs와 index.html 예제 코드를 함께 공유해드리니 확인해보시길 바랍니다.
① code.gs (구글시트ID와 API키는 수정 후 사용하세요!)
// 구글 시트 ID 및 API 키 설정 const SHEET_ID = '구글시트ID'; const GEMINI_API_KEY = 'API키'; const SHEET_NAME = '기록'; /** * 웹 앱 진입점 */ function doGet() { return HtmlService.createHtmlOutputFromFile('index') .setSandboxMode(HtmlService.SandboxMode.IFRAME) .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); } /** * 여행 경로 추천 메인 함수 */ function getTravelRecommendation(formData) { try { // 요청 ID 생성 const requestId = Utilities.getUuid(); // 구글 시트에 데이터 저장 saveToSheet(formData, requestId); // Gemini API로 여행 경로 추천 받기 const recommendation = callGeminiAPI(formData); // 성공 응답 return { success: true, data: recommendation, requestId: requestId }; } catch (error) { console.error('오류 발생:', error); return { success: false, error: error.toString() }; } } /** * Gemini API 호출 함수 */ function callGeminiAPI(formData) { const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${GEMINI_API_KEY}`; // 프롬프트 생성 const prompt = createTravelPrompt(formData); const payload = { contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: 0.7, topK: 40, topP: 0.95, maxOutputTokens: 8192, } }; const options = { method: 'POST', headers: { 'Content-Type': 'application/json', }, payload: JSON.stringify(payload) }; const response = UrlFetchApp.fetch(url, options); const responseData = JSON.parse(response.getContentText()); if (responseData.candidates && responseData.candidates[0]) { const content = responseData.candidates[0].content.parts[0].text; return parseGeminiResponse(content, formData.travelDays); } else { throw new Error('Gemini API 응답을 처리할 수 없습니다.'); } } /** * 여행 추천 프롬프트 생성 */ function createTravelPrompt(formData) { return ` 당신은 전문 여행 가이드입니다. 다음 조건에 맞는 ${formData.travelDays}일간의 상세한 여행 일정을 추천해주세요. [여행 정보] - 여행지: ${formData.destination} - 인원수: ${formData.groupSize} - 성별: ${formData.gender} - 나이대: ${formData.ageGroup} - 여행 스타일: ${formData.travelStyle} - 여행 일수: ${formData.travelDays}일 - 예산: ${formData.budget} [응답 형식] 각 일자별로 다음과 같은 형식으로 응답해주세요: **DAY1** 🌅 오전 (09:00-12:00) - 장소명: [구체적인 장소] - 활동: [상세한 활동 내용] - 예상 비용: [금액] - 팁: [실용적인 조언] 🌞 오후 (12:00-18:00) - 장소명: [구체적인 장소] - 활동: [상세한 활동 내용] - 예상 비용: [금액] - 팁: [실용적인 조언] 🌙 저녁 (18:00-22:00) - 장소명: [구체적인 장소] - 활동: [상세한 활동 내용] - 예상 비용: [금액] - 팁: [실용적인 조언] [주의사항] 1. 실제 존재하는 장소와 정확한 정보만 제공해주세요 2. 예산 범위 내에서 현실적인 비용을 제시해주세요 3. 이동 시간과 거리를 고려한 효율적인 동선을 제안해주세요 4. 현지 문화와 날씨를 고려한 팁을 포함해주세요 5. 각 일자는 **DAY1**, **DAY2** 형식으로 구분해주세요 `; } /** * Gemini 응답 파싱 */ function parseGeminiResponse(content, travelDays) { const days = []; const dayPattern = /\*\*DAY(\d+)\*\*([\s\S]*?)(?=\*\*DAY\d+\*\*|$)/g; let match; while ((match = dayPattern.exec(content)) !== null) { const dayNumber = parseInt(match[1]); const dayContent = match[2].trim(); days.push({ day: dayNumber, content: dayContent }); } // 일수에 맞게 정렬 및 필터링 return days .filter(day => day.day <= travelDays) .sort((a, b) => a.day - b.day); } /** * 구글 시트에 데이터 저장 */ function saveToSheet(formData, requestId) { try { const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME); // 헤더가 없으면 추가 if (sheet.getLastRow() === 0) { const headers = [ '생성일시', '여행지', '인원수', '성별', '나이대', '여행스타일', '여행일수', '예산규모', '요청ID' ]; sheet.getRange(1, 1, 1, headers.length).setValues([headers]); } // 데이터 추가 const row = [ new Date(), formData.destination, formData.groupSize, formData.gender, formData.ageGroup, formData.travelStyle, formData.travelDays, formData.budget, requestId ]; sheet.appendRow(row); } catch (error) { console.error('시트 저장 오류:', error); throw new Error('데이터 저장 중 오류가 발생했습니다.'); } } /** * 테스트용 함수 */ function testAPI() { const testData = { destination: '제주도', groupSize: '2명', gender: '혼성', ageGroup: '30대', travelStyle: '커플여행', travelDays: 3, budget: '200만원/인당' }; const result = getTravelRecommendation(testData); console.log(result); }② index.html
<html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>🌺 스마트 여행 가이드</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; line-height: 1.6; } .container { max-width: 800px; margin: 0 auto; } .header { text-align: center; margin-bottom: 30px; color: white; } .header h1 { font-size: 2.5rem; margin-bottom: 10px; text-shadow: 0 2px 4px rgba(0,0,0,0.3); } .header p { font-size: 1.2rem; opacity: 0.9; } .main-card { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 20px; padding: 30px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); margin-bottom: 20px; } .form-section { margin-bottom: 30px; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 8px; font-weight: 600; color: #2c3e50; font-size: 1rem; } .form-control { width: 100%; padding: 12px 15px; border: 2px solid #e0e6ed; border-radius: 10px; font-size: 1rem; transition: all 0.3s ease; background: rgba(255,255,255,0.8); } .form-control:focus { outline: none; border-color: #667eea; background: white; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } .radio-group { display: flex; flex-wrap: wrap; gap: 15px; margin-top: 10px; } .radio-item { position: relative; } .radio-item input[type="radio"] { position: absolute; opacity: 0; } .radio-item label { display: flex; align-items: center; padding: 10px 20px; background: rgba(102, 126, 234, 0.1); border: 2px solid transparent; border-radius: 25px; cursor: pointer; transition: all 0.3s ease; font-weight: 500; margin-bottom: 0; } .radio-item input[type="radio"]:checked + label { background: linear-gradient(135deg, #667eea, #764ba2); color: white; border-color: #667eea; transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); } .btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 15px 40px; border-radius: 50px; font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; width: 100%; margin-top: 20px; box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); } .btn-primary:hover { transform: translateY(-3px); box-shadow: 0 15px 30px rgba(102, 126, 234, 0.4); } .btn-primary:disabled { opacity: 0.7; cursor: not-allowed; transform: none; } .btn-secondary { background: linear-gradient(135deg, #ff6b6b, #ee5a24); color: white; border: none; padding: 12px 30px; border-radius: 50px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; margin-top: 20px; box-shadow: 0 8px 16px rgba(255, 107, 107, 0.3); } .btn-secondary:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(255, 107, 107, 0.4); } .loading { display: none; text-align: center; padding: 40px; } .spinner { width: 50px; height: 50px; border: 4px solid #e0e6ed; border-left: 4px solid #667eea; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 20px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .results { display: none; } .day-card { background: rgba(255,255,255,0.9); border-radius: 15px; margin-bottom: 15px; overflow: hidden; box-shadow: 0 5px 15px rgba(0,0,0,0.1); transition: all 0.3s ease; } .day-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.15); } .day-header { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 15px 20px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font-weight: 600; } .day-content { padding: 20px; display: none; white-space: pre-line; line-height: 1.8; } .day-content.show { display: block; animation: slideDown 0.3s ease; } @keyframes slideDown { from { opacity: 0; max-height: 0; } to { opacity: 1; max-height: 1000px; } } .chevron { transition: transform 0.3s ease; } .chevron.rotate { transform: rotate(180deg); } .footer { text-align: center; color: rgba(255,255,255,0.8); margin-top: 30px; font-size: 0.9rem; } /* 반응형 디자인 */ @media (max-width: 768px) { .container { padding: 0 10px; } .header h1 { font-size: 2rem; } .header p { font-size: 1rem; } .main-card { padding: 20px; border-radius: 15px; } .radio-group { justify-content: center; } .radio-item label { padding: 8px 16px; font-size: 0.9rem; } } @media (max-width: 480px) { body { padding: 10px; } .header h1 { font-size: 1.8rem; } .main-card { padding: 15px; } .radio-group { flex-direction: column; align-items: center; } .radio-item { width: 100%; } .radio-item label { justify-content: center; width: 100%; } } .error-message { background: #ff6b6b; color: white; padding: 15px; border-radius: 10px; margin: 20px 0; display: none; } .success-message { background: #51cf66; color: white; padding: 15px; border-radius: 10px; margin: 20px 0; display: none; } </style> </head> <body> <div class="container"> <!-- 헤더 --> <div class="header"> <h1>🌺 스마트 여행 가이드</h1> <p>AI가 추천하는 맞춤형 여행 일정을 만나보세요!</p> </div> <!-- 메인 카드 --> <div class="main-card"> <!-- 입력 폼 섹션 --> <div class="form-section" id="formSection"> <form id="travelForm"> <!-- 여행지 입력 --> <div class="form-group"> <label for="destination">🗺️ 여행할 곳을 알려주세요</label> <input type="text" class="form-control" id="destination" placeholder="예: 제주도, 부산, 일본 도쿄..." required> </div> <!-- 인원수 선택 --> <div class="form-group"> <label>👥 몇 명이서 여행하시나요?</label> <div class="radio-group"> <div class="radio-item"> <input type="radio" id="group1" name="groupSize" value="1명" required> <label for="group1">혼자</label> </div> <div class="radio-item"> <input type="radio" id="group2" name="groupSize" value="2명"> <label for="group2">2명</label> </div> <div class="radio-item"> <input type="radio" id="group3" name="groupSize" value="3명"> <label for="group3">3명</label> </div> <div class="radio-item"> <input type="radio" id="group4" name="groupSize" value="4명 이상"> <label for="group4">4명 이상</label> </div> </div> </div> <!-- 성별 선택 --> <div class="form-group"> <label>👫 성별을 선택해주세요</label> <div class="radio-group"> <div class="radio-item"> <input type="radio" id="male" name="gender" value="남성" required> <label for="male">남성</label> </div> <div class="radio-item"> <input type="radio" id="female" name="gender" value="여성"> <label for="female">여성</label> </div> <div class="radio-item"> <input type="radio" id="mixed" name="gender" value="혼성"> <label for="mixed">혼성</label> </div> </div> </div> <!-- 나이대 선택 --> <div class="form-group"> <label for="ageGroup">🎂 나이대를 선택해주세요</label> <select class="form-control" id="ageGroup" required> <option value="">나이대를 선택하세요</option> <option value="10대">10대</option> <option value="20대">20대</option> <option value="30대">30대</option> <option value="40대">40대</option> <option value="50대">50대</option> <option value="60대 이상">60대 이상</option> </select> </div> <!-- 여행 스타일 선택 --> <div class="form-group"> <label for="travelStyle">✨ 어떤 여행을 원하시나요?</label> <select class="form-control" id="travelStyle" required> <option value="">여행 스타일을 선택하세요</option> <option value="휴양지">🏖️ 휴양지 (바다, 리조트)</option> <option value="커플여행">💕 커플여행 (로맨틱)</option> <option value="가족여행">👨👩👧👦 가족여행 (온 가족)</option> <option value="액티비티">🎢 액티비티 (모험, 스포츠)</option> <option value="문화탐방">🏛️ 문화탐방 (역사, 예술)</option> <option value="음식여행">🍜 음식여행 (맛집 투어)</option> </select> </div> <!-- 여행 일수 선택 --> <div class="form-group"> <label for="travelDays">📅 며칠 동안 여행하시나요?</label> <select class="form-control" id="travelDays" required> <option value="">여행 일수를 선택하세요</option> <option value="1">당일치기</option> <option value="2">1박 2일</option> <option value="3">2박 3일</option> <option value="4">3박 4일</option> <option value="5">4박 5일</option> <option value="6">5박 6일</option> <option value="7">6박 7일</option> </select> </div> <!-- 예산 규모 선택 --> <div class="form-group"> <label>💰 예산은 어느 정도 생각하고 계시나요?</label> <div class="radio-group"> <div class="radio-item"> <input type="radio" id="budget1" name="budget" value="100만원/인당" required> <label for="budget1">100만원/인당</label> </div> <div class="radio-item"> <input type="radio" id="budget2" name="budget" value="200만원/인당"> <label for="budget2">200만원/인당</label> </div> <div class="radio-item"> <input type="radio" id="budget3" name="budget" value="300만원/인당"> <label for="budget3">300만원/인당</label> </div> <div class="radio-item"> <input type="radio" id="budget4" name="budget" value="500만원/인당"> <label for="budget4">500만원/인당</label> </div> </div> </div> <!-- 추천 버튼 --> <button type="submit" class="btn-primary" id="submitBtn"> ✈️ 여행 경로 추천받기 </button> </form> </div> <!-- 로딩 섹션 --> <div class="loading" id="loadingSection"> <div class="spinner"></div> <h3>🤖 AI가 맞춤형 여행 일정을 만들고 있어요...</h3> <p>잠시만 기다려주세요! (약 10-20초 소요)</p> </div> <!-- 결과 출력 섹션 --> <div class="results" id="resultsSection"> <div style="text-align: center; margin-bottom: 30px;"> <h2>🎉 맞춤형 여행 일정이 완성되었어요!</h2> <p>각 일차를 클릭해서 상세 일정을 확인해보세요</p> </div> <div id="dayCards"></div> <div style="text-align: center;"> <button class="btn-secondary" onclick="resetForm()"> 🔄 새로운 여행 계획하기 </button> </div> </div> <!-- 에러/성공 메시지 --> <div class="error-message" id="errorMessage"></div> <div class="success-message" id="successMessage"></div> </div> <!-- 푸터 --> <div class="footer"> <p>💝 AI와 함께하는 특별한 여행을 만들어보세요!</p> </div> </div> <script> // 폼 제출 처리 document.getElementById('travelForm').addEventListener('submit', function(e) { e.preventDefault(); submitForm(); }); // 폼 데이터 수집 및 제출 function submitForm() { // 폼 데이터 수집 const formData = { destination: document.getElementById('destination').value.trim(), groupSize: getRadioValue('groupSize'), gender: getRadioValue('gender'), ageGroup: document.getElementById('ageGroup').value, travelStyle: document.getElementById('travelStyle').value, travelDays: parseInt(document.getElementById('travelDays').value), budget: getRadioValue('budget') }; // 유효성 검사 if (!validateForm(formData)) { return; } // 로딩 상태로 전환 showLoading(); // Google Apps Script 함수 호출 google.script.run .withSuccessHandler(handleSuccess) .withFailureHandler(handleError) .getTravelRecommendation(formData); } // 라디오 버튼 값 가져오기 function getRadioValue(name) { const radios = document.getElementsByName(name); for (let radio of radios) { if (radio.checked) { return radio.value; } } return ''; } // 폼 유효성 검사 function validateForm(formData) { const required = ['destination', 'groupSize', 'gender', 'ageGroup', 'travelStyle', 'travelDays', 'budget']; for (let field of required) { if (!formData[field]) { showError(`모든 항목을 입력해주세요. (누락: ${getFieldName(field)})`); return false; } } if (formData.destination.length < 2) { showError('여행지는 최소 2글자 이상 입력해주세요.'); return false; } return true; } // 필드명 변환 function getFieldName(field) { const names = { destination: '여행지', groupSize: '인원수', gender: '성별', ageGroup: '나이대', travelStyle: '여행스타일', travelDays: '여행일수', budget: '예산' }; return names[field] || field; } // 로딩 화면 표시 function showLoading() { document.getElementById('formSection').style.display = 'none'; document.getElementById('resultsSection').style.display = 'none'; document.getElementById('loadingSection').style.display = 'block'; hideMessages(); } // 성공 처리 function handleSuccess(response) { hideLoading(); if (response.success) { displayResults(response.data); showSuccess('여행 일정이 성공적으로 생성되었습니다!'); } else { showError(response.error || '알 수 없는 오류가 발생했습니다.'); } } // 오류 처리 function handleError(error) { hideLoading(); showError('서버 오류가 발생했습니다: ' + error.message); } // 결과 출력 function displayResults(days) { const dayCards = document.getElementById('dayCards'); dayCards.innerHTML = ''; days.forEach((day, index) => { const card = document.createElement('div'); card.className = 'day-card'; card.innerHTML = ` <div class="day-header" onclick="toggleDay(${index})"> <span>📅 ${day.day}일차</span> <span class="chevron" id="chevron-${index}">▼</span> </div> <div class="day-content" id="content-${index}"> ${day.content} </div> `; dayCards.appendChild(card); }); document.getElementById('resultsSection').style.display = 'block'; } // 일차별 콘텐츠 토글 function toggleDay(index) { const content = document.getElementById(`content-${index}`); const chevron = document.getElementById(`chevron-${index}`); if (content.classList.contains('show')) { content.classList.remove('show'); chevron.classList.remove('rotate'); } else { content.classList.add('show'); chevron.classList.add('rotate'); } } // 폼 리셋 function resetForm() { document.getElementById('travelForm').reset(); document.getElementById('formSection').style.display = 'block'; document.getElementById('resultsSection').style.display = 'none'; hideMessages(); } // 로딩 숨기기 function hideLoading() { document.getElementById('loadingSection').style.display = 'none'; document.getElementById('formSection').style.display = 'block'; } // 에러 메시지 표시 function showError(message) { const errorDiv = document.getElementById('errorMessage'); errorDiv.textContent = message; errorDiv.style.display = 'block'; setTimeout(() => { errorDiv.style.display = 'none'; }, 5000); } // 성공 메시지 표시 function showSuccess(message) { const successDiv = document.getElementById('successMessage'); successDiv.textContent = message; successDiv.style.display = 'block'; setTimeout(() => { successDiv.style.display = 'none'; }, 3000); } // 메시지 숨기기 function hideMessages() { document.getElementById('errorMessage').style.display = 'none'; document.getElementById('successMessage').style.display = 'none'; } // 페이지 로드 시 첫 번째 일차 열기 function openFirstDay() { setTimeout(() => { const firstContent = document.getElementById('content-0'); if (firstContent) { toggleDay(0); } }, 500); } </script> </body> </html>- 구글 시트에서 [확장 프로그램] → [Apps Script]를 클릭해서 앱스 스크립트를 실행합니다.

확장프로그램 → Apps Script 를 실행합니다. - 만약 구글에 여러 계정이 로그인 되어 있을 경우, Apps Script를 실행하면 아래 그림과 같이 '여러번 리디렉션 되었습니다' 라는 오류가 발생할 수 있습니다. 그럴 경우 로그인 된 모든 계정을 로그아웃 한 후, 하나의 계정만 로그인해서 앱스 스크립트를 다시 실행합니다.

리디렉션 오류가 발생할 경우, 구글 계정을 모두 로그아웃하고 본 계정 하나만 로그인합니다. - 앱스 스크립트 편집기가 실행되면 code.gs 파일에 작성되어 있던 기존코드를 지우고 AI가 작성한 code.gs 코드를 붙여넣습니다.

기존 코드를 지우고 ai가 작성한 코드를 붙여넣습니다. - 이후 [+] 버튼을 클릭해서 'index.html' 이라는 이름의 새로운 HTML 페이지를 추가합니다.

index.html 이라는 새로운 페이지를 추가합니다. - AI가 작성한 index.html 코드를 안에 붙여 넣으면 코드를 옮기는 과정이 모두 끝납니다.

index.html 에도 ai가 작성한 코드를 붙여넣습니다. - 오른쪽 상단의 [배포] → [새 배포]를 클릭한 후, 유형 선택에서 '웹 앱'을 선택합니다.

우측 상단의 [배포] → [새 배포] 에서 종류로 웹 앱을 선택합니다. - 설명으로는 '여행 가이드 앱 V1.0'으로 입력한 후, "인증 정보를 실행할 계정"과 "엑세스 권한이 있는 계정"을 선택합니다. 이번 예제로는 본인 계정만 접근 가능하도록 선택하겠습니다.

앱 설명과 엑세스 권한 정보를 설정합니다. - 모두 선택한 후, [배포] 버튼을 클릭하면 아래 그림과 같이 '엑세스 승인' 버튼이 나옵니다. 버튼을 클릭합니다.

배포 과정 중 엑세스 승인 버튼이 나오면 버튼을 클릭합니다. - 이후 계정을 선택하고 [고급] → '제목없는 프로젝트(안전하지 않음)' 으로 이동한 후, [허용] 버튼을 클릭해서 접근 권한을 승인합니다.

[고급] 을 클릭한 후, 접근 권한을 승인합니다. - 잠시만 기다리면 웹 앱 URL이 생성됩니다. 이제 웹 앱 URL로 이동하면 나만의 앱을 사용할 수 있습니다.

웹 앱 URL이 생성되고, 이제 URL에서 앱을 바로 실행할 수 있습니다.
부족한 부분 및 오류 개선 후 새 버전으로 배포하기
축하합니다! 이제 세상에 단 하나뿐인 나만의 앱이 완성되었습니다!👏 하지만 모든 과정이 그렇듯, 첫 번째 결과가 항상 완벽할 수는 없습니다. 직접 사용해보면서 버튼 크기를 키우거나, 생각지 못한 오류가 발생할 수 있는데요. 그럴 때에도 AI를 활용해서 원하는 부분을 직접 수정해서 사용하면 됩니다.
- 이번에는 간단한 예제로 앱의 머리글인 "스마트 여행 가이드" 라는 제목이 파도 치듯이 움직이는 것처럼 보이도록 애니메이션을 추가해달라고 요청해보겠습니다.
좋아, 아주 훌륭해. 그런데 앱 화면이 조금 심심한 것 같아서 "스마트 여행 가이드" 라는 제목이 파도치듯이 움직이는 듯한 애니메이션을 추가하고 싶어.

앱 수정이나 개선이 필요할 때에도 AI로 편리하게 코드를 수정할 수 있습니다. - 잠시만 기다리면 AI가 기존 코드에서 수정이 필요한 부분을 찾아 코드 작성을 시작합니다.

AI가 수정이 필요한 부분을 찾아 수정합니다. - 아래 그림과 같이 애니메이션이 들어간 앱이 완성되었습니다.

앱이 수정되었습니다. - 이제 수정된 코드를 복사해서 기존 code.gs 와 index.html 에 새롭게 붙여넣고 [저장] 버튼을 클릭하거나 Ctrl + S로 저장합니다.

수정된 코드를 붙여넣고 저장합니다. - 이후 [배포] → [테스트 배포]를 클릭하면 수정한 코드가 잘 동작하는지 확인하는 URL이 생성됩니다. 테스트 URL에서 수정한 앱이 잘 동작하는지 확인합니다.

[배포] → [테스트 배포]에서 테스트용 URL을 생성한 후, 앱이 잘 동작하는지 확인합니다. - 앱이 잘 동작하는 것을 확인했으면 [배포] → [배포 관리] 에서 편집 버튼을 클릭하고 새 버전을 추가합니다.

[배포] → [배포 관리] 에서 새 버전의 앱을 배포합니다. - 설명에 새로운 버전의 앱 설명을 입력하고 [배포] 버튼을 클릭하면 새 버전으로 배포가 끝납니다.

앱 설명을 입력한 후, [배포] 버튼을 클릭하면 앱이 배포됩니다.
휴대폰 홈 화면에 앱 아이콘 추가하기
이제 완성된 웹 앱을 휴대폰 홈 화면의 앱 아이콘으로 추가해보겠습니다. 아이폰 사용자(사파리)와 갤럭시 사용자(크롬) 브라우저별 홈 화면 등록 방법을 알아보겠습니다.
① 아이폰 사용자 (사파리 브라우저 기준)
브라우저에서 웹 앱 페이지로 이동 → 중앙 하단 공유버튼 → [홈 화면에 추가] → [추가] 버튼 클릭!
아이폰 사파리 브라우저 홈 화면에 아이콘 추가하기 ② 갤럭시 사용자 (크롬 브라우저 기준)
브라우저에서 웹 앱 페이지로 이동 → 우측 상단 공유버튼 → [홈 화면에 추가] → [추가] 버튼 클릭!
갤럭시 크롬 브라우저 홈 화면에 아이콘 추가하기 - 아래 링크를 클릭해서 구글 AI 스튜디오로 이동한 후, API키를 발급 받을 계정으로 로그인합니다. 이후 오른쪽 상단의 [Get API Key](또는 API키 발급 받기) 버튼을 클릭해서 API 키 발급 페이지로 이동합니다.

