무엇을 만들고, 무엇을 배우나코드부터 치기 전에 이 3분을 먼저 읽는다
이 수업이 끝나면 휴대폰에서 열리는 나만의 웹앱 하나가 생긴다. 화면에서 입력한 내용이 구글 스프레드시트에 실제로 저장되고, 저장된 데이터가 다시 그래프로 나타난다. 서버를 빌리지도, 프로그램을 설치하지도 않는다. 구글 계정만 있으면 된다.
앱스 스크립트 문법을 외우는 것이 아니다. 「문제 → 요구사항 → 설계 → 구현 → 테스트 → 배포 → 회고」라는 소프트웨어 개발 과정을 한 바퀴 직접 돌아 보는 것이다. AI에게 코드를 받아 쓰는 시대에 더 중요해진 능력은 무엇을 만들지 정하는 힘과 받은 코드를 판단하는 힘이다.
개발 프로세스 7단계 — 그리고 AI가 끼어드는 자리
| 단계 | 하는 일 | 산출물 | AI에게 맡길 수 있나 |
|---|---|---|---|
| ① 문제 정의 | 무엇이 불편한지 한 문장으로 | 주제 한 문장 | ❌ 내가 정해야 한다 |
| ② 요구사항 | 누가 무엇을 할 수 있어야 하는가 | 요구사항 정의서 양식 1 | △ 초안은 도움받되 판단은 내가 |
| ③ 설계 | 데이터를 어떤 표로 저장할지, 화면을 어떻게 나눌지 | 시트 구조 · 화면 스케치 양식 2 | △ 대안을 물어보기 좋다 |
| ④ 구현 | 코드 작성 (AI와 협업) | .gs / .html / .json + 프롬프트 설계서 양식 4 | ⭕ AI가 가장 잘하는 구간 |
| ⑤ 테스트 | 일부러 틀린 값을 넣어 본다 | 테스트 체크리스트 양식 3 | △ 항목 뽑기는 도움, 실행은 내가 |
| ⑥ 배포 | 친구가 쓸 수 있는 주소 만들기 | 웹앱 URL | ❌ 권한 결정은 내 책임 |
| ⑦ 회고 | 무엇이 잘/안 됐나, 다음엔 무엇을 | 회고 기록 | ❌ 내 경험이다 |
'바이브 코딩'은 AI와 대화하며 만드는 방식이지, 생각을 멈추고 붙여넣는 것이 아니다. ②③⑤를 건너뛰고 ④만 AI에게 던지면 "돌아가긴 하는데 내가 왜 이걸 만들었는지 설명할 수 없는 앱"이 남는다. 이 수업은 ②③⑤를 내 손으로 쓰게 만든다. 그게 AI를 잘 쓰는 사람과 못 쓰는 사람의 차이다.
기술부채와 이해부채 — 바이브 코딩이 남기는 두 가지 빚
개발에서 부채(빚)는 "지금 편하려고 미뤄 둔 일이 나중에 이자까지 붙어 돌아오는 것"을 뜻한다. 빚에는 두 종류가 있는데, 바이브 코딩은 둘째 쪽을 훨씬 빠르게 쌓는다.
| 견주어 보기 | 기술부채 코드에 쌓인다 | 이해부채 내 머릿속에 쌓인다 |
|---|---|---|
| 무엇을 미룬 것인가 | 코드를 제대로 정리하는 일 중복 지우기, 이름 다시 붙이기, 예외 처리 |
이 코드가 왜 그렇게 동작하는지 이해하는 일 |
| 어디에 남는가 | 파일 안에. 열어 보면 보인다 | 아무 데도 남지 않는다. 코드는 깔끔한데 설명할 사람이 없다 |
| 겉으로 드러나나 | △ 코드를 읽으면 티가 난다 | ❌ 안 난다. 앱이 돌아가는 동안은 아무 문제가 없어 보인다 |
| 청구서가 언제 오나 | 기능을 하나 더 붙일 때 | 고장 났을 때 · 질문받았을 때 · 발표할 때 |
| 이자가 어떻게 붙나 | 고쳐야 할 곳이 점점 늘어난다 | 모르는 코드 위에 또 모르는 코드가 얹힌다 |
| 누가 갚을 수 있나 | 나중에 남이 대신 정리해 줄 수도 있다 | 나밖에 갚을 수 없다 |
| AI를 쓰면 | 줄어들 수도 있다 — AI는 정리도 잘한다 | 거의 항상 늘어난다 |
기술부채는 코드가 지저분해서 내가 불편을 느낀다. 그래서 갚아야 한다는 것을 안다. 이해부채는 반대다. AI가 준 코드는 대개 깔끔하고 잘 돌아가고, 진도도 빠르다. 「나는 지금 잘하고 있다」는 느낌이 들기 때문에 빚이 쌓이는 줄을 알아채지 못한다.
청구서는 꼭 이럴 때 온다 — ① 에러가 났는데 어디를 봐야 할지 모른다 ② "여기는 왜 이렇게 했어?"라는 질문에 답을 못 한다 ③ 기능 하나를 더 붙이려는데 어느 줄을 고쳐야 할지 짚을 수 없다. 셋 다 코드의 문제가 아니라 내 이해의 문제여서, AI에게 다시 물어도 잘 풀리지 않는다. 질문 자체를 만들 수 없기 때문이다.
AI가 준 코드를 편집기에 넣기 전에 스스로 확인한다. 하나라도 "아니오"면 그 자리에서 AI에게 되물어야 한다. 나중에 몰아서 갚으려 하면 이자가 붙어 있다.
모르는 코드는 넣지 않는 것이 원칙이지만, 수업 시간은 짧다. 그럴 때는 넣되 그 위에 「여기는 아직 이해 못 함」이라고 주석으로 적어 둔다. 적어 둔 빚은 갚을 수 있고, 적지 않은 빚은 잊힌다.
그래서 이 수업은 AI에게 코드를 그냥 받지 않게 짜여 있다. STEP 11에서 프롬프트를 직접 설계하고, STEP 12에서 받은 코드에 내 손으로 한국어 주석을 달고 한 군데를 고쳐 넣는다. 그 두 가지가 이해부채를 그날 안에 갚는 방법이다.
차시 계획
| 차시 | 단계 | 끝났을 때 손에 남는 것 |
|---|---|---|
| 1차시 기획·데이터 |
STEP 1 → 5 | 요구사항 정의서, 데이터 계획서, 시트에 코드로 값을 넣고 읽어 본 경험 |
| 2차시 웹앱·배포 |
STEP 6 → 9 | 내 주소로 열리는 웹앱 하나 (휴대폰에서 열어 친구에게 보여 줄 수 있다) |
| 3차시 테스트·AI 협업 |
STEP 10 → 12 | 테스트 체크리스트, 프롬프트 설계서, AI가 준 코드를 고쳐 넣은 기능 1개 |
준비물
아래 양식에 적은 내용은 탭을 닫으면 사라진다. 수업이 끝날 때 오른쪽 위 ⬇ 내 작업 버튼을 눌러 파일로 내려받고, 그 파일을 제출한다.
① 문제 정의 — 무엇을 만들지 정한다여기서 고른 주제에 맞춰 이 페이지의 예시 코드가 전부 바뀐다
네 가지 중 하나를 고른다. ①번이 기준 주제이고, 나머지는 만들고 싶은 것이 확실한 학생용이다. 고르면 위쪽 선택 상자와 STEP 12의 코드가 그 주제로 바뀐다.
- 이 앱이 없으면 지금 누가 어떻게 불편한가?
- 이 앱을 나 말고 실제로 쓸 사람이 있는가? 몇 명쯤인가?
- 데이터가 100건 모였다고 하자. 그걸로 무슨 말을 할 수 있는가?
세 번째 질문에서 막히면 주제를 바꾸는 편이 낫다. 많은 프로젝트가 "모으는 것"을 목표로 삼고 무엇을 알아낼지는 정하지 않은 채 시작해서 실패한다.
② 요구사항 — "무엇이 되면 완성인가"를 미리 적는다이 문장이 없으면 언제 끝난 건지 알 수 없다
요구사항은 기능 설명이 아니라 확인 가능한 문장으로 쓴다. 확인할 수 없는 문장은 요구사항이 아니다.
'편리하게'를 어떻게 확인하나? 다 만든 뒤에도 됐는지 안 됐는지 판단할 수 없다.
직접 해 보면 됐는지 안 됐는지 바로 판정된다. 이것이 나중에 테스트 항목이 된다.
「(누가)는 (무엇)을 (어떤 조건에서) 할 수 있다」
예) 관리자는 새 책을 등록할 수 있고, 등록하면 목록에 즉시 나타난다.
③ 설계 (1) — 어떤 데이터를 모을지 정한다잘못 모은 데이터는 나중에 고칠 수 없다. 코드보다 먼저 온다
스프레드시트를 쓰는 앱은 결국 표를 설계하는 일이다. 그런데 그 표에 사람에 관한 정보가 들어가는 순간, 이건 프로그래밍 문제가 아니라 판단의 문제가 된다.
설문을 이미 돌린 뒤에 "이름은 받지 말았어야 했다"는 걸 깨달으면 처음부터 다시 모아야 한다. 그때쯤이면 남은 차시가 없다. 그래서 이 점검이 코드보다 먼저 온다.
내 데이터는 세 경로 중 어디인가
| 경로 | 정체 | 핵심 쟁점 |
|---|---|---|
| A. 공개 데이터 | 기관이 쓰라고 공개한 것 (공공데이터포털, 기상자료, 학교알리미) | 출처 표기, 이용 조건 |
| B. 직접 수집 | 사람에게 직접 받는 것 (설문·관찰·측정) | 동의 · 최소 수집 · 익명 |
| C. 웹에 있는 남의 것 | 다른 목적으로 올라온 것 (게시글, 댓글, 리뷰) | 이용약관, 저작권 |
판별이 어려우면 이렇게 묻는다 — "이 데이터는 누가, 왜 인터넷에 올려 둔 것인가?"
권장 순서는 A → B → C다. 같은 질문에 답할 수 있다면 공개 데이터가 가장 낫고, 크롤링은 마지막에 고려한다.
이 수업의 네 주제는 모두 B(내가 만드는 데이터) 또는 내 기록이다.
최소 수집 — 모든 항목에 이 질문을 적용한다
"이 항목이 없으면 내 질문에 답할 수 없는가?"
답이 "없어도 답할 수 있다"이면 뺀다. "혹시 몰라서", "나중에 쓸 수도 있어서", "다들 물어보니까"는 이유가 아니다.
| 항목 | 학생이 넣는 이유 | 실제 판단 |
|---|---|---|
| 이름 | "누가 안 냈는지 알려고" | 분석에 쓰이지 않는다 → 뺀다 |
| 학번 | "중복 방지" | 다른 방법이 있다 → 뺀다 |
| 반 | "반별로 비교하려고" | 정말 반별 비교를 발표할 거면 남긴다. 아니면 뺀다 |
| 성별 | "기본이니까" | 결과에서 성별로 무엇을 비교할지 말할 수 있어야 남긴다 |
| 연락처 | "추가 질문하려고" | → 뺀다 |
이름을 안 받아도 사람이 특정된다 — 재식별
학년: 2학년
반: 7반
성별: 남
동아리: 방송부
이름이 없어도 조합하면 누구인지 알 수 있다. 응답 결과를 친구에게 보여줬을 때 "이거 ○○이 쓴 거네"라고 알아챌 수 있다면 그 설문은 익명이 아니다.
대응 방법 세 가지 — ① 조합 항목 수를 줄인다(반을 빼고 학년만) ② 범위로 묶는다 ③ 인원이 적은 선택지는 '기타'로 합친다
절대 수집하지 않는 것
| 금지 | 대신 이렇게 |
|---|---|
| 주민등록번호 · 전화번호 · 집 주소 · 계좌 | 임의 번호(응답1, 응답2)나 별칭 |
| 본인이 아닌 사람의 성적 · 석차 | 공부 시간, 스스로 매긴 만족도, 공개된 학교 통계 |
| 건강 상태 · 병력 · 정신건강 | 수면 시간, 운동 횟수 같은 일반적 생활 습관 |
| 종교 · 정치 성향 · 가정 형편 · 가족 구성 | 주제 자체를 다른 축으로 바꾼다 |
| 몰래 찍은 사진 · 영상 · 녹음 | 사전 동의 후 촬영, 또는 공개 데이터셋 |
| 단톡방 · SNS 대화 내용 | 본인이 직접 쓴 기록만 |
| 로그인해야 보이는 남의 게시물 | 공개 API 또는 공공데이터 |
교내에서 설문을 돌리기 전에 담당 선생님의 사전 승인을 받는다. 학교마다 절차가 다르므로 AI나 이 페이지가 "괜찮다"고 판단해 주지 않는다. 응답자가 만 14세 미만(중학생 이하)이면 법정대리인 동의가 필요하다 — 이 경우 여기서 멈추고 선생님과 상의한다.
동의 안내문에 반드시 들어갈 여섯 가지
⑥이 특히 중요하다. 교실에서 친구가 돌리는 설문은 거절하기 어렵다. 그 사실을 아는 것 자체가 배울 내용이다. 그리고 신뢰를 주는 안내문이 더 솔직한 응답을 만든다 — 윤리는 규제가 아니라 데이터 품질의 문제이기도 하다.
④ 구현 준비 — 스프레드시트와 편집기를 연다여기서 파일 5개의 정체를 처음 만난다
경고를 넘기는 방법만 알려주면 학생은 앞으로 모든 경고를 습관적으로 넘긴다. "이 화면은 내가 만든 코드가 내 데이터에 접근해도 되는지 묻는 것이고, 남이 만든 링크에서 같은 화면이 뜨면 멈춰야 한다"까지 함께 짚는다.
파일 5개, 각각 무슨 일을 하나
<style>을 넣는다.앱스 스크립트는 .gs와 .html 두 가지 확장자만 만들 수 있다. 그래서 CSS와 JavaScript도
.html 파일 안에 <style>, <script> 태그로 넣고 index.html에서 불러온다.
파일을 나누지 않고 index.html 하나에 다 넣어도 돌아가긴 한다. 그래도 나누는 이유는
고칠 곳을 빨리 찾기 위해서다. 색을 바꾸려면 css만 열면 된다.
스프레드시트를 코드로 조작한다이 다섯 가지만 알면 어떤 앱이든 만들 수 있다
먼저, .gs는 무슨 파일인가
Code.gs의 .gs는 Google Script의 줄임이다.
구글 스프레드시트·문서·슬라이드·폼 같은 구글 문서에 붙어서 그것을 조작하는 프로그램을
담아 두는 파일이고, 이 프로그램을 부르는 이름이 구글 앱 스크립트(Google Apps Script, 줄여서 GAS)다.
기반 언어는 자바스크립트(JavaScript)다. 즉 .gs 안에 적는 문법은
웹 페이지에서 쓰는 그 자바스크립트와 같다 — function, let/const,
if, for, 배열, 객체가 모두 그대로다.
| 견주어 보기 | 브라우저의 자바스크립트 | 앱스 스크립트(.gs) |
|---|---|---|
| 문법 | 자바스크립트 | 같은 자바스크립트 |
| 어디서 실행되나 | 내 브라우저(내 컴퓨터) | 구글 서버 — 내 컴퓨터를 꺼도 돌아간다 |
| 화면을 만질 수 있나 | ⭕ document로 화면을 고친다 | ❌ document·alert·window가 없다 |
| 대신 무엇이 있나 | — | SpreadsheetApp, DriveApp, GmailApp 같은 구글 전용 객체 |
| 값 확인 | console.log() | Logger.log() → 편집기의 실행 기록 |
.gs = 구글 서버에서 도는 자바스크립트(시트를 읽고 쓴다) ·
.html = 학생 휴대폰 화면(STEP 6~8) ·
appsscript.json = 설정.
편집기에서 .gs 파일을 여러 개로 나눠도 서로 함수를 그냥 부를 수 있다
(import가 필요 없다). 그래서 Code.gs 하나로 시작해도 괜찮다.
AI에게 물을 때는 꼭 "Google Apps Script"라고 적는다.
그냥 "자바스크립트로 시트에 저장하는 코드"라고 물으면 브라우저용 코드나
Node.js·구글 API 키를 쓰는 코드가 와서 여기서는 돌지 않는다.
앱스 스크립트에서 시트를 다루는 일은 항상 같은 순서다.
SpreadsheetAppgetSheetByName('응답')getRange · getValues · setValues · appendRow① 시트 열기
② 읽기 — getValues는 항상 2차원 배열을 준다
가장 많이 헷갈리는 부분이다. 셀 하나를 읽어도 표 모양(배열의 배열)으로 돌아온다.
셀을 하나씩 getValue()로 100번 읽으면 100번 구글 서버에 왕복한다.
30초면 끝날 일이 몇 분이 걸린다. getValues()로 한 번에 다 읽고, 계산은 자바스크립트에서 한다.
이것이 앱스 스크립트 성능의 90%를 결정한다.
③ 추가 — appendRow
④ 수정 — setValue / setValues
⑤ 삭제 — deleteRow
자주 쓰는 명령 정리
| 하고 싶은 일 | 코드 | 기억할 점 |
|---|---|---|
| 붙어 있는 시트 열기 | SpreadsheetApp.getActive() | 확장 프로그램에서 만든 스크립트만 가능 |
| 다른 시트 열기 | SpreadsheetApp.openById('...') | URL의 /d/ 다음 부분이 ID |
| 시트(탭) 고르기 | ss.getSheetByName('응답') | 이름이 틀리면 null이 돌아온다 |
| 전체 데이터 읽기 | sheet.getDataRange().getValues() | 머리글까지 포함 → .slice(1) |
| 범위 지정 | getRange(행, 열, 행수, 열수) | 행이 먼저, 1부터 시작 |
| 한 줄 추가 | sheet.appendRow([...]) | 열 순서를 머리글과 맞춘다 |
| 셀 하나 쓰기 | range.setValue(값) | s가 없다 |
| 여러 셀 쓰기 | range.setValues([[...],[...]]) | s가 있다 + 2차원 배열 |
| 줄 지우기 | sheet.deleteRow(행) | 아래에서 위로 지운다 |
| 마지막 행 번호 | sheet.getLastRow() | 머리글 포함이므로 -1 |
| 값 확인(디버깅) | Logger.log(값) | 실행 기록에서 볼 수 있다 |
편집기에 아래 함수를 붙여넣고 ▶ 실행 → 시트가 바뀌는지 확인 → 왼쪽 ▷실행 메뉴에서 로그를 확인한다.
sheet.getRange(2, 3)은 어느 셀인가?rows는 머리글을 자른 배열)파일 3개가 화면 하나로 합쳐진다"어디를 고쳐야 하지?"를 스스로 판단할 수 있게 된다
웹앱 주소를 열면 세 가지 일이 차례로 일어난다. 지금은 이것만 알면 된다.
doGet 함수를 실행한다include 다. 아직 사용자에게 보내지 않았다.include — 파일을 끼워 넣는 함수
파이썬에서 함수를 쓰려면 먼저 def 로 만들어야 하는 것처럼,
include 도 내가 Code.gs 에 만들어 두는 함수다. 앱스 스크립트에 원래 있는 기능이 아니다.
이 세 줄을 안 넣으면 화면이 열리지 않는다.
그리고 index.html 에서 끼워 넣고 싶은 자리에 이렇게 적는다.
<?!= include('css') ?> 는 "이 자리에 css.html 의 내용을 그대로 넣어라"는 표시다.
서버가 화면을 보내기 전에 이 자리를 실제 내용으로 바꿔 준다. 그래서 사용자가 소스 보기를 해도
include 흔적은 없고, 이미 합쳐진 화면 하나만 보인다.
!)를 빼먹으면 바로 티가 난다<?= include('css') ?> 라고 적으면 화면에
<style>body{…} 이 글자로 그대로 보인다. 증상이 특징적이라 한 번 보면 기억한다.
색·글씨 → css.html · 화면에 무엇이 있는지 → index.html · 버튼을 눌렀을 때의 동작 → js.html · 시트를 읽고 쓰는 일 → Code.gs. 파일을 나누는 이유는 딱 하나, 고칠 곳을 빨리 찾기 위해서다.
🔬 더 알고 싶은 사람만 — <? ?> 의 세 가지 모양과 흔한 사고 두 가지
<? ?> 안에 적는 코드를 스크립틀릿이라고 부른다. 모양이 세 가지다.
| 모양 | 뜻 | 쓰는 곳 |
|---|---|---|
<? … ?> | 코드만 실행하고 화면에는 아무것도 안 남긴다 | 반복문, 조건문 |
<?= … ?> | 값을 출력한다 (특수문자를 안전하게 바꿔서) | 이름, 숫자 같은 값 |
<?!= … ?> | 값을 그대로 출력한다 (안 바꾼다) | include 처럼 HTML 조각을 끼울 때 |
앱스 스크립트는 HTML 주석 안까지 실행 대상으로 본다. 설명하려고 주석에
<?= 값을 출력 ?> 같은 예시를 적어 두면 그것까지 실행하려 하다가
웹앱 전체가 열리지 않는다. (css.html·js.html 은 실행 대상이 아니라 주석이 안전하다 — index.html 만 다르다)
</script> 라는 글자를 쓰면 안 된다주석이든 문자열이든 상관없다. 브라우저는 그 글자를 보면 거기서 스크립트가 끝났다고 판단해서,
아래에 있던 함수가 모두 사라지고 코드가 화면에 글자로 쏟아진다.
꼭 써야 하면 <\/script> 처럼 역슬래시를 끼워 넣는다.
둘 다 문법 오류가 아니라, 파일이 서버 → 브라우저 순서로 두 번 읽힌다는 사실에서 생기는 문제다.
웹앱의 시작점, doGet함수 하나로 스프레드시트가 웹사이트가 된다
웹앱이 되는 조건은 하나다. doGet 이라는 이름의 함수가 있고, 그 함수가 화면을 돌려주는 것.
파이썬 프로그램의 시작점을 main() 으로 정해 두는 것과 비슷한데,
이름을 내가 고를 수 없다는 점만 다르다. 구글이 doGet 을 찾아서 실행하기 때문이다.
우리 앱은 화면을 파일에서 읽어 온다. 실제로 쓰는 형태가 아래이고, 한 줄이 한 가지 일을 한다.
createTemplateFromFile('index')— index.html 을 읽어 온다.evaluate()—include자리를 실제 내용으로 바꿔 화면을 완성한다 (빼면 화면이 안 만들어진다).setTitle('…')— 브라우저 탭 제목. 없어도 동작한다.addMetaTag('viewport', …)— 휴대폰 대응. 빼면 글씨가 아주 작게 보인다
appsscript.json — 코드가 아니라 설정
여기서 지금 신경 쓸 것은 두 줄뿐이다.
"timeZone": "Asia/Seoul"— 안 맞춰 두면 시트에 저장되는 시각이 9시간 어긋난다."executeAs": "USER_DEPLOYING"— 내 권한으로 실행한다는 뜻이다. 그래서 친구들은 내 스프레드시트에 권한이 없어도 설문에 답할 수 있다.USER_ACCESSING으로 두면 접속한 사람마다 시트 권한이 필요해져서 설문이 사실상 불가능해진다.
누가 열 수 있는지를 정하는 "access" 는 STEP 9에서 다룬다.
🔬 더 알고 싶은 사람만 — doGet(e) 의 e, runtimeVersion, 그리고 "누구 이름으로 저장되나"
e 는 무엇인가doGet(e) 의 e 에는 주소에 붙은 값이 담겨 온다.
내주소?name=수민 으로 열면 e.parameter.name 이 '수민' 이 된다.
이걸 이용해 화면 대신 데이터를 돌려주는 통로를 만들 수 있다 (STEP 8의 심화 부분).
"runtimeVersion": "V8" 은 자바스크립트 엔진 이름이다. V8 이어야
const 와 화살표 함수를 쓸 수 있다. 요즘 만든 프로젝트는 기본값이 V8 이라 손댈 일이 거의 없다.
executeAs 는 편의 문제가 아니라 책임 문제다.
USER_DEPLOYING 이면 시트에 줄을 쓰는 것은 배포한 내 계정이다.
즉 친구가 남긴 응답도 기록상으로는 내 권한으로 저장된 것이 된다.
누구의 이름으로 데이터가 저장되는지 알고 고르는 것과, 되니까 쓰는 것은 다르다.
화면과 서버는 어떻게 이야기하나이 개념 하나가 웹 개발 전체의 핵심이다
브라우저는 스프레드시트를 만질 수 없다. 아무리 코드를 잘 써도 안 된다.
브라우저에서 도는 js.html 은 시트가 어디 있는지도 모른다. 할 수 있는 일은 서버(Code.gs)에게
"이 값 좀 저장해 줘"라고 부탁하는 것뿐이고, 그 부탁 창구가 google.script.run 이다.
식당에 비유하면 화면은 손님, Code.gs 는 주방이다. 손님은 주방에 들어갈 수 없고 주문만 넣는다. 주문서에 적을 것은 세 가지 — 어떤 함수에게, 무슨 값을, 답이 오면 무엇을 할지.
딱 하나만 조심 — 답은 나중에 온다
파이썬에서는 함수를 부르면 답이 나올 때까지 기다렸다가 다음 줄로 간다.
google.script.run 은 기다리지 않는다. 주문만 넣고 바로 다음 줄로 넘어가고,
서버의 답은 조금 뒤에 withSuccessHandler 에 적어 둔 함수로 배달된다.
서버가 답하기 전에 아래 줄이 실행된다.
total 은 undefined 다.
결과를 쓰는 코드를 핸들러 안에 넣는다. 서버가 답하면 그때 실행된다.
규칙은 한 줄이다 — 서버가 준 값을 쓰는 코드는 반드시 핸들러 안에 넣는다. 「저장은 되는데 화면이 안 바뀐다」는 문제는 거의 다 여기서 온다.
JSON — 객체를 글자로 바꾼 것
화면과 서버는 데이터를 결국 글자로 주고받는다. 그 약속된 형식이 JSON이다.
파이썬의 딕셔너리와 거의 같은 모양이라 읽기 어렵지 않다. appsscript.json 도 이 형식이다.
규칙 세 가지만 기억한다 — 키는 겹따옴표, 마지막 항목 뒤 쉼표 금지, 주석 금지.
하나라도 어기면 appsscript.json 이 저장되지 않는다.
🔬 더 알고 싶은 사람만 — 보낼 수 없는 값, 동시 호출 10개, 내 앱을 데이터 제공소로 쓰기
숫자, 문자열, true/false, null, 객체, 배열, <form> 요소
Date 객체, 함수, 화면 요소(form 제외), 서로를 참조하는 객체
날짜는 문자열로 주고받는다. '2026-08-19' 처럼 보내고,
시트에 저장할 때 서버에서 new Date() 를 만든다. 이걸 모르면
Invalid argument 오류에서 한참 헤맨다.
동시 호출은 10개까지다. 넘으면 줄을 서서 기다린다.
버튼을 연달아 누르면 느려지는 이유가 이것이어서, 제출 버튼은 누른 순간 disabled 로 잠근다.
주소 뒤에 ?api= 를 붙이면 같은 웹앱이 화면도 되고 데이터 제공소(API)도 된다.
실습 코드에 이미 들어 있다.
배포한 주소 뒤에 ?api=(주제별 값)을 붙여 열면 화면이 아니라 글자 덩어리가 나온다.
다른 프로그램이 내 앱의 데이터를 가져다 쓰는 방식이고, 내가 만든 것이 서비스가 되는 순간이다.
⑥ 배포 — 친구가 쓸 수 있는 주소 만들기여기서 처음으로 "내 것"이 아니라 "서비스"가 된다
- 오른쪽 위 배포 → 새 배포
- 톱니바퀴 ⚙ → 웹 앱 선택
- 다음 사용자로 실행:
나 - 액세스 권한이 있는 사용자: 아래 표에서 고른다 가장 중요한 선택
- 배포 → 나오는 웹 앱 URL이 완성된 앱의 주소다
| 액세스 권한 | 누가 쓸 수 있나 | 언제 고르나 |
|---|---|---|
| 나만 | 나 혼자 | 주제 ④ 개인용, 개발 중 |
| (학교) 도메인 내 사용자 | 같은 학교 계정으로 로그인한 사람 | 교내 설문·학급 관리에 가장 적절 |
| 모든 사용자 | 구글 로그인한 누구나 | 학교 밖 응답이 필요할 때 |
| 모든 사용자(익명 포함) | 링크만 있으면 누구나 | 로그인 없이 받아야 할 때. 가장 조심해서 쓴다 |
'익명 포함'으로 열면 링크가 단톡방을 타고 다른 학교까지 갈 수 있다. 그러면 내 데이터에 우리 학교 학생이 아닌 응답이 섞이고, 발표에서 "우리 학교는 ~하다"고 말할 근거가 사라진다. 권한 설정은 보안 문제이면서 동시에 데이터 품질 문제다.
배포는 사진을 찍는 것과 같다. 코드를 고쳐도 이전에 찍은 사진이 계속 서비스된다.
배포 → 배포 관리 → ✏ 편집 → 버전: 새 버전 → 배포를 해야 반영된다.
개발 중에는 배포 → 배포 테스트의 /dev 주소를 쓰면 저장할 때마다 바로 반영된다.
단, /dev 주소는 편집 권한이 있는 사람만 열 수 있어서 친구에게 보낼 수는 없다.
⑤ 테스트 — "잘 되는지"가 아니라 "언제 깨지는지"를 찾는다내 앱을 내가 부수는 시간. 발표 중에 부서지는 것보다 낫다
학생들은 보통 자기가 의도한 대로만 눌러 보고 "된다"고 말한다. 그건 테스트가 아니다. 테스트는 일부러 이상하게 써 보는 것이다.
| 넣어 볼 값 | 왜 | 기대되는 올바른 반응 |
|---|---|---|
| 아무것도 안 넣고 제출 | 필수값 검사가 있나 | "…를 입력해 주세요" 안내, 시트에는 빈 줄이 안 생김 |
숫자칸에 abc | 형 변환이 안전한가 | 거부 또는 안내. NaN이 시트에 저장되면 안 된다 |
| 아주 긴 글 (1000자) | 길이 제한이 있나 | 길이 제한 안내 |
| 제출 버튼 빠르게 5번 클릭 | 중복 저장이 되나 | 한 줄만 저장 (버튼이 잠긴다) |
<b>굵게</b> 입력 | 남의 코드가 실행되나 (XSS) | 글자 그대로 보임. 굵어지면 안 된다 |
| 이모지 · 특수문자 | 인코딩 문제 | 깨지지 않고 저장 |
| 데이터 0건 상태에서 결과 보기 | 빈 데이터 처리 | "아직 없습니다" 안내. 오류가 나면 안 된다 |
| 다른 계정 / 시크릿 창 | 권한 설정이 맞나 | 의도한 사람만 열림 |
디버깅 — 추측하지 말고 확인한다
오류가 났을 때 코드를 이리저리 바꿔 보는 것은 가장 느린 방법이다. 순서를 지킨다.
Logger.log('여기까지 옴')을 넣는다. 찍히면 그 앞은 정상이다.Logger.log(typeof x, x) — 비어 있는지, 문자인지 숫자인지를 확인한다. 대부분 여기서 원인이 나온다.js.html의 오류는 실행 기록에 안 남는다. 브라우저에서 F12 → Console 탭을 본다.
withFailureHandler를 안 붙이면 서버 오류도 조용히 콘솔로만 간다.
그래서 google.script.run 호출에 실패 핸들러(failure handler)를 붙여 주는 것이 바람직하다.
증상별 원인 표
| 증상 | 거의 항상 이 원인 |
|---|---|
Script function not found: doGet | 함수 이름 오타. 대소문자까지 정확히 doGet |
화면에 <?!= include('css') ?> 가 글자로 보인다 | createHtmlOutputFromFile을 썼다 → createTemplateFromFile().evaluate() |
화면에 <style> 코드가 그대로 보인다 | <?= 를 썼다 → <?!= 로 |
… 시트가 없습니다 | setupSheet를 실행하지 않았거나 시트 이름 오타 |
| 버튼을 눌러도 아무 반응이 없다 | 서버 함수 이름 불일치, 또는 _로 끝나는 함수를 불렀다 |
| 저장은 되는데 화면이 안 변한다 | 결과 처리 코드가 withSuccessHandler 밖에 있다 |
| 고친 코드가 반영 안 된다 | 새 버전으로 재배포하지 않았다 |
Invalid argument · 날짜 관련 오류 | Date 객체를 화면↔서버로 넘겼다 → 문자열로 |
| 타임스탬프가 9시간 어긋난다 | appsscript.json의 timeZone |
| 실행이 아주 느리다 | 반복문 안에서 getValue()/setValue()를 호출한다 |
AI와 함께 만들기 — 프롬프트도 설계다AI가 코드를 잘 쓰는 시대에 남는 실력은 '판단'과 '질문'이다
AI에게 요청하기 전에 이 세 가지가 내 손에 있어야 한다.
① STEP 2의 요구사항 — 무엇이 되면 완성인지
② STEP 3의 데이터 구조 — 어떤 시트에 어떤 열이 있는지
③ 실패 조건 — 어떤 값이 들어오면 거부해야 하는지
이게 없으면 AI는 그럴듯한 아무 앱을 만들어 준다. 그리고 그건 내 프로젝트가 아니다.
좋은 프롬프트의 6요소
| 요소 | 쓸 내용 | 빠뜨리면 |
|---|---|---|
| ① 환경 | "구글 앱스 스크립트(V8), 스프레드시트 바인딩, HtmlService 웹앱" | Node.js·React 코드를 준다 |
| ② 데이터 구조 | 시트 이름과 열 이름을 그대로 적는다 | 열 이름을 지어내서 안 돌아간다 |
| ③ 하나의 목표 | "기능 하나"만 요청 | 전부 다 새로 써서 준다 → 검증 불가 |
| ④ 제약 | "라이브러리 추가 없이", "함수 이름은 유지", "주석은 한국어로" | 외부 라이브러리를 쓰거나 기존 코드를 다 바꾼다 |
| ⑤ 형식 | "Code.gs에 추가할 함수만", "고친 줄만 표시" | 어디를 바꿔야 할지 모르게 된다 |
| ⑥ 검증 요청 | "이 코드가 실패하는 경우 2가지도 알려 줘" | 실패 조건을 내가 다 찾아야 한다 |
비교해 보기
→ 어떤 환경인지 모르니 일반 웹 코드가 나온다.
fetch, express, localStorage가 섞여 있고 앱스 스크립트에서는 안 돌아간다.
어디에 붙여야 할지도 알 수 없다.
→ 붙여넣을 수 있는 코드가 나오고, 한계까지 알려 준다.
받은 코드를 검증하는 5단계
붙여넣기 전에 한다. 순서를 건너뛰면 나중에 원인을 못 찾는다.
AI가 앱스 스크립트에서 자주 틀리는 것
| 이런 코드가 오면 | 왜 안 되나 | 맞는 방법 |
|---|---|---|
document.getElementById가 .gs 안에 있다 | 서버에는 화면이 없다 | 화면 조작은 js.html에서 |
SpreadsheetApp이 js.html 안에 있다 | 브라우저에는 시트가 없다 | google.script.run으로 서버에 부탁 |
fetch('/api/…') | 우리 앱에 그런 서버 경로가 없다 | google.script.run 또는 ?api= 방식 |
localStorage에 저장 | 브라우저에만 남아 다른 사람은 못 본다 | 데이터는 시트에 저장 |
new Date()를 화면에서 서버로 넘김 | Date는 전달 불가 | 문자열로 넘기고 서버에서 변환 |
반복문 안의 getValue() | 매번 서버 왕복 → 매우 느리다 | getValues()로 한 번에 |
sheet.addRow(...) 같은 없는 함수 | 그럴듯하지만 존재하지 않는다 | appendRow. 의심되면 공식 문서 확인 |
async/await로 google.script.run 대기 | 이 API는 Promise가 아니다 | withSuccessHandler 사용 |
🐞 버그 찾기 훈련
아래는 AI가 실제로 줄 만한 코드다. 무엇이 잘못됐는지 먼저 찾아보고 답을 열어 본다.
내 주제의 전체 코드파일 탭을 눌러 5개 파일을 각각 복사한다
appsscript.json → Code.gs → index → css → js
순서로 넣고 저장한 다음, setupSheet를 실행한다.
파일 이름에 .html은 쓰지 않는다 (앱스 스크립트가 자동으로 붙인다).
⑦ 회고 — 그리고 제출여기까지 쓴 내용을 파일로 내려받는다
아래 버튼을 누르면 지금까지 입력한 양식 1~5 전체가 하나의 텍스트 파일로 내려받아진다. 이 파일과 웹앱 주소를 함께 제출한다.
더 해 볼 것
편집기 왼쪽 시계 아이콘에서 매일 오후 6시에 함수를 자동 실행할 수 있다. 하루 응답 수를 정리하거나 메일로 요약을 보내 보자.
MailApp.sendEmail()로 알림을 보낼 수 있다.
단, 응답자의 개인정보는 메일에 담지 않는다.
스프레드시트에서 만든 차트를 이미지로 뽑아 화면에 넣을 수도 있다.
직접 계산하는 것과 시트 함수(SUMIF) 중 어느 쪽이 나을지 비교해 보자.
설문 결과를 대시보드 형태로 보여 주거나, 대출 기록으로 인기 도서 순위를 만들어 보자. 같은 기술로 전혀 다른 앱이 된다.
교사용 — 평가 관점
| 영역 | 무엇을 보나 | 근거 자료 |
|---|---|---|
| 문제 정의 | 주제가 확인 가능한 문장으로 좁혀졌는가 | 양식 1-A · 1-B |
| 정보윤리 | 수집 항목을 스스로 뺀 근거를 말할 수 있는가 | 양식 2, 회고 1번 |
| 구현 | 시트 조작 · 화면·서버 분리를 설명할 수 있는가 | 동작하는 웹앱 URL |
| 테스트 | 정상 경로 외의 시도를 실제로 했는가 | 양식 3 (실패 기록 포함) |
| AI 활용 | 받은 코드를 검증·수정한 흔적이 있는가 | 양식 4, 회고 3번 |
| 성찰 | 기술이 아니라 판단을 되돌아보는가 | 회고 4번 |
가장 뚜렷한 신호는 "이 항목은 선생님이 하지 말래서 뺐다"가 아니라 "내 질문에 필요 없어서 뺐다"고 말하는 학생이다. 그리고 실패 기록이 있는 테스트 체크리스트가 전부 통과인 체크리스트보다 낫다.