목차(클릭하세요)
아두이노가 측정한 온습도를 인터넷 너머 구글 시트에 자동으로 쌓는 실습
일반적으로 엑셀에 저장하는 방법, 프로그램도 있지만, 보다 진일보한 형태로 구글 appas script를 활용하는 방식
[참고자료] 엑셀에 저장하는 방법은 상당히 많이 존재함
0. 아두이노 우노 R4에서 측정값을 구글 시트로 저장하기 위한 사전지식
0-1. 2.4GH만 대역만 지원
[주의점] WiFi - 5GHz 사용 불가)
0-2.관련 라이브러리
•
WiFi는 사용을 위한 "WiFiS3.h" 라이브러리가 이미 존재함(기본 라이브러리)
0-3. 유의사항
실습에서 비밀번호 3종 세트가 사용됨
혼동 주의•
#주의사항: 구글 계정 비밀번호는 코드 어디에도 입력하지 않음
•
아두이노 코드의 PASSWORD는 구글 계정 비밀번호가 아니라 와이파이 비밀번호임
•
CLASS_KEY는 사용자가 임의로 정하는 수업용 키임
◦
영문과 숫자를 섞어 각자 다르게 정하기
값 | 복사할 위치 | 입력할 부분 |
시트 ID | 시트 주소의 /d/ 다음 | Apps Script의 SHEET_ID |
배포 ID | 배포된 구글 웹 앱 주소의 /s/와 /exec 사이 | 아두이노의 DEPLOY_ID |
CLASS_KEY (수업용 키) | 각자 정한 영문·숫자 조합(각자 임의대로) | 두 코드의 CLASS_KEY |
0-4. 전체 흐름
•
DHT11 센서가 온도와 습도를 측정함
•
아두이노 UNO R4 WiFi가 측정값을 수신해 학교 와이파이를 경유하여 구글 서버로 전송함
•
구글 앱스 스크립트로 구현한 웹 앱이 이 요청을 수신함
•
앱스 스크립트가 구글 시트에 1행을 추가함
Ⅰ. 구글 시트·앱스 스크립트 수신부 구성
구글 시트는 데이터를 보관하는 저장소이고, 앱스 스크립트는 요청을 수신해 그 저장소에 값을 기록하는 프로그램임.
아두이노 작업 전에 이 수신부부터 구성함.
1-1. 구글 시트 생성과 시트 ID 확인
•
본인 구글 학교 계정으로 로그인하기
•
구글 드라이브에서 새 구글 스프레드시트 1개 생성하기
•
파일 이름은 날짜와 이름을 포함해 지정하기
◦
예시: 26년 9월온습도센서값기록
•
주소창에서 시트 ID 복사해 두기
◦
주소 형식은 https://docs.google.com/spreadsheets/d/시트_ID/edit임
◦
/d/ 다음부터 그다음 / 앞까지가 시트 ID임
[예시]https://docs.google.com/spreadsheets/d/1Rexckjdcmkldcmlk여기가시트IDq-QyiJn5TiYhm9oAWPTs/edit?gid=1461417854#gid=1461417854
1-2. 앱스 스크립트 추가와 setupSheet() 실행
•
시트 메뉴에서 확장 프로그램을 누르고 Apps Script 열기
•
새 프로젝트가 출력되면 기존 코드를 전부 삭제하고 아래 코드 붙여 넣기
const SHEET_ID = "YOUR_SPREADSHEET_ID";
const CLASS_KEY = "YOUR_OWN_LETTERS_AND_NUMBERS";
// 최초 한 번 편집기에서 실행하여 권한을 승인하고 기록 탭을 준비함
function setupSheet() {
const book = SpreadsheetApp.openById(SHEET_ID);
let sheet = book.getSheetByName("기록");
if (!sheet) {
sheet = book.insertSheet("기록");
}
if (sheet.getLastRow() === 0) {
sheet.appendRow(["서버 수신 시각(KST)", "온도(℃)", "습도(%)"]);
}
}
// 주소로 받은 값을 검사하고, 한국 시간의 수신 시각과 함께 기록함
function doGet(e) {
const p = (e && e.parameter) || {};
if (p.key !== CLASS_KEY) {
return ContentService.createTextOutput("DENIED: check key");
}
const integer = /^\d{1,3}$/;
if (!integer.test(p.temp || "") || !integer.test(p.humi || "")) {
return ContentService.createTextOutput("INVALID: use integer values");
}
const temp = Number(p.temp);
const humi = Number(p.humi);
if (temp < 0 || temp > 50 || humi < 0 || humi > 100) {
return ContentService.createTextOutput("INVALID: out of range");
}
const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName("기록");
if (!sheet) {
return ContentService.createTextOutput("SETUP_REQUIRED: run setupSheet");
}
// 시간대를 명시하여 계정 설정과 관계없이 한국 시간으로 기록함
const receivedAt = Utilities.formatDate(
new Date(), "Asia/Seoul", "yyyy-MM-dd HH:mm:ss"
);
sheet.appendRow([receivedAt, temp, humi]);
SpreadsheetApp.flush();
return ContentService.createTextOutput("SAVED: " + temp + "," + humi);
}
JavaScript
복사
•
맨 위의 SHEET_ID와 CLASS_KEY를 본인 값으로 수정하고 저장하기
◦
이 2줄이 각자 수정해야 하는 부분임
•
편집기 위쪽 함수 목록에서 setupSheet()를 선택하고 실행 누르기
•
본인 계정과 프로젝트를 확인하는 창이 출력되면 시트 접근 권한 승인하기
•
시트로 돌아가 결과 확인하기
•
제목 행에 3칸이 입력되었는지 확인하기
•
기록이라는 이름의 탭이 새로 생성되었는지 확인하기
1-3. 웹 앱 배포와 배포 ID 확인
•
앱스 스크립트에서 배포를 누르고 새 배포를 선택한 다음 종류를 웹 앱으로 지정하기
•
실행 사용자는 나로 설정하기
•
액세스 권한은 모든 사용자로 설정하기
◦
중간에 영어로 출력되는 실행 여부 확인 창도 계속 승인하기
•
배포가 끝나면 /exec로 끝나는 웹 앱 URL 복사하기
◦
주소 형식은 https://script.google.com/macros/s/배포_ID/exec임
◦
s 다음부터 exec 앞까지가 배포 ID임
1-4. 브라우저 주소창을 이용한 시험값 전송
•
아두이노 없이 브라우저만으로 구글 측 수신부가 정상 실행되는지 먼저 확인할 수 있음
•
복사해 둔 웹 앱 URL 뒤에 아래 내용을 추가해 다른 브라우저 창이나 탭의 주소창에 입력하기
?key=나의키&temp=25&humi=60
Plain Text
복사
•
나의키 자리에는 앞에서 정한 CLASS_KEY 값 입력하기
◦
키를 ysjtttysj로 지정했다면 ?key=ysjtttysj&temp=25&humi=60이 됨
•
정상 결과 확인하기
◦
브라우저 화면에 SAVED: 25,60이 출력됨
◦
시트에 한국 시간과 함께 25와 60이 1행 기록됨
여기까지 확인되면 이 실습의 50% 이상이 완료된 상태임. 구글 측 수신부가 정상 동작한다는 뜻이므로, 남은 작업은 아두이노에서 동일한 주소를 호출하도록 구현하는 것임.Ⅱ. 아두이노 수동 입력 전송
센서를 곧바로 연결하지 않는 이유가 있음. 센서까지 한꺼번에 연결하면 값이 수신되지 않을 때 통신 문제인지 센서 문제인지 구분할 수 없음. 따라서 먼저 사람이 직접 숫자를 입력해 통신 경로부터 검증하는 과정을 거침
2-1. 보드 설정과 값 4개 수정
•
기본 보드 설정을 마치고 아래 코드 붙여 넣기
#include <WiFiS3.h>
#include <WiFiSSLClient.h>
#include <stdio.h>
// 구글 계정 비밀번호가 아니라 Wi-Fi 비밀번호를 입력함
const char SSID[] = "YOUR_WIFI_NAME";
const char PASSWORD[] = "YOUR_WIFI_PASSWORD";
const char DEPLOY_ID[] = "YOUR_DEPLOYMENT_ID";
const char CLASS_KEY[] = "YOUR_OWN_LETTERS_AND_NUMBERS";
WiFiSSLClient client;
void setup() {
Serial.begin(9600);
Serial.setTimeout(1000);
client.setTimeout(15000);
// 시리얼 모니터를 기다리되, 열지 않아도 무한정 멈추지 않음
unsigned long started = millis();
while (!Serial && millis() - started < 10000) {
delay(10);
}
Serial.println("WiFi connecting...");
WiFi.begin(SSID, PASSWORD);
delay(10000);
if (WiFi.status() == WL_CONNECTED) {
Serial.print("WiFi connected. IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("WiFi failed. Check settings, then press RESET.");
}
Serial.println("Enter integers as temperature,humidity. Example: 25,60");
}
void loop() {
if (!Serial.available()) {
return;
}
String input = Serial.readStringUntil('\n');
input.trim();
if (input.length() == 0) {
return;
}
int temp;
int humi;
char extra;
// 숫자가 아닌 입력과 남는 문자를 거부함 쉼표 앞 공백은 넣지 않음
if (input.length() > 12 ||
sscanf(input.c_str(), "%3d,%3d%c", &temp, &humi, &extra) != 2 ||
temp < 0 || temp > 50 || humi < 0 || humi > 100) {
Serial.println("Invalid input. Use integers: 25,60 (T: 0-50, H: 0-100).");
return;
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected. Press RESET, then enter again.");
return;
}
Serial.print("Sending: ");
Serial.println(input);
if (!client.connect("script.google.com", 443)) {
Serial.println("HTTPS connection failed. Check WiFi and firmware.");
client.stop();
return;
}
// 두 값은 숫자이고 키는 영문·숫자이므로 주소 인코딩을 별도로 하지 않음
String path = String("/macros/s/") + DEPLOY_ID + "/exec";
path += String("?key=") + CLASS_KEY;
path += String("&temp=") + temp;
path += String("&humi=") + humi;
client.print(String("GET ") + path + " HTTP/1.1\r\n");
client.println("Host: script.google.com");
client.println("Connection: close");
client.println();
// 첫 응답만 표시함 302도 저장 성공을 증명하지 않음
String status = client.readStringUntil('\n');
status.trim();
Serial.print("HTTP: ");
Serial.println(status.length() > 0 ? status : "No status received");
client.stop();
Serial.println("Check your sheet for the new row before sending again.");
}
C++
복사
•
SSID, PASSWORD, DEPLOY_ID, CLASS_KEY 4개 수정하기
◦
SSID: 학교 와이파이이므로 OOOO입력하기
◦
PASSWORD: OOOO
◦
DEPLOY_ID: 1-3에서 확인한 배포 ID (웹 앱 주소에서 s 다음부터 exec 앞까지)
◦
CLASS_KEY: 각자 정한 임의 번호를 그대로 입력하기
•
수정을 마쳤으면 컴파일 후 업로드하기
2-2. 시리얼 모니터 입력과 결과 확인
•
시리얼 모니터를 열고 9600bps와 새 줄로 설정하기
•
RESET 버튼을 누른 뒤 연결 메시지가 출력될 때까지 대기하기
◦
WiFi connected와 IP가 출력되면 준비가 완료된 상태임
•
시리얼 모니터 창에 ‘26, 67’처럼 온도와 습도를 쉼표로 연결해 숫자로 입력하기
◦
쉼표 앞에 공백을 넣지 않기
•
아래 3가지 시험 수행하기
입력 | 기대 결과 | 관찰 기록 |
26,61 | 해당 값 한 행 저장 | □ 성공 □ 실패 |
27,62 | 다음 행에 해당 값 저장 | □ 성공 □ 실패 |
abc,60 | Invalid input, 저장 없음 | □ 성공 □ 실패 |
•
3번째 시험이 통과해야 코드가 유효성 검사로 잘못된 값을 차단하고 있음을 확인한 것임
[참고]doGet()의 값 검증 순서
•
수신 측 코드는 값을 무조건 저장하지 않음.
◦
키가 일치하는지 확인하고, 숫자 형식이 맞는지 정규식으로 확인하고, 온도 0에서 50, 습도 0에서 100 범위 안인지 확인함.
•
수신측에서 3개 검사를 모두 통과해야 시트에 행이 추가되고 SAVED 응답이 반환됨.
◦
중간에 차단되면 각각 DENIED, INVALID, 범위 초과에 해당하는 다른 문자열이 반환됨
Ⅲ. 센서 자동 기록 구성
사람이 수동으로 하던 입력을 DHT11 센서로 바꾸는 단계임.
구글 시트 측 앱스 스크립트는 Ⅰ단계에서 검증한 코드를 그대로 재사용하며, 기록 탭도 그대로 사용함. 3-1. DHT11 온습도 센서연결
•
전원을 분리한 상태에서 연결하기
◦
S → D4
◦
+ → 5V
◦
− → GND
•
접촉이 헐거우면 값이 읽히지 않으므로 꽂은 뒤에 한 번씩 확인하기
3-2. 자동 전송 코드 업로드
•
아래 코드를 붙여 넣고 Ⅱ단계에서 검증한 설정 4개를 그대로 복사해 입력하기
#include <DHT11.h>
#include <WiFiS3.h>
#include <WiFiSSLClient.h>
// 2단계에서 성공한 설정 네 개를 그대로 복사함
const char SSID[] = "YOUR_WIFI_NAME";
const char PASSWORD[] = "YOUR_WIFI_PASSWORD";
const char DEPLOY_ID[] = "YOUR_DEPLOYMENT_ID";
const char CLASS_KEY[] = "YOUR_OWN_LETTERS_AND_NUMBERS";
const unsigned long INTERVAL = 5000;
unsigned long lastAttempt = 0;
DHT11 dht11(4);
WiFiSSLClient client;
void setup() {
Serial.begin(9600);
client.setTimeout(15000);
// 모니터를 열지 않아도 자동 측정이 시작되도록 대기 시간을 제한함
unsigned long started = millis();
while (!Serial && millis() - started < 10000) {
delay(10);
}
Serial.println("WiFi connecting...");
WiFi.begin(SSID, PASSWORD);
delay(10000);
if (WiFi.status() == WL_CONNECTED) {
Serial.print("WiFi connected. IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("WiFi failed. Check settings, then press RESET.");
}
lastAttempt = millis();
Serial.println("DHT11 automatic logging started. Interval: 5 seconds.");
}
void loop() {
// 전송 시작을 기준으로 5초를 계산함 통신 중에는 요청이 겹치지 않음
if (millis() - lastAttempt < INTERVAL) {
return;
}
lastAttempt = millis();
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected. Press RESET to reconnect.");
return;
}
int temp = 0;
int humi = 0;
int err = dht11.readTemperatureHumidity(temp, humi);
// 읽기에 실패한 값은 시트에 전송하지 않음
if (err != 0) {
Serial.print("Sensor error: ");
Serial.println(err);
return;
}
Serial.print("Temperature: ");
Serial.print(temp);
Serial.print(" Humidity: ");
Serial.println(humi);
if (!client.connect("script.google.com", 443)) {
Serial.println("HTTPS connection failed.");
client.stop();
return;
}
// 1단계와 같은 주소 형식으로 실제 측정값을 전달함
String path = String("/macros/s/") + DEPLOY_ID + "/exec";
path += String("?key=") + CLASS_KEY;
path += String("&temp=") + temp;
path += String("&humi=") + humi;
client.print(String("GET ") + path + " HTTP/1.1\r\n");
client.println("Host: script.google.com");
client.println("Connection: close");
client.println();
// HTTP 상태만 표시하며, 저장 성공은 시트의 새 행으로 확인함
String status = client.readStringUntil('\n');
status.trim();
Serial.print("HTTP: ");
Serial.println(status.length() > 0 ? status : "No status received");
client.stop();
}
C++
복사
•
코드의 동작 조건 확인하기
◦
전송 간격은 5초로 설정되어 있음
◦
읽기에 실패한 값은 시트로 전송하지 않고 건너뜀
◦
통신에 실패한 값도 자동으로 재전송하지 않음
•
HTTP 200이나 302가 출력됐는데 시트에 행이 없는 경우가 생길 수 있음
◦
통신에 실패한 값은 재전송하지 않으므로 해당 시각의 데이터는 소실될 수 있다는 이야기임
•
기록 간격이 5초보다 길어지는 것도 코드 문제가 아니라 통신 소요 시간이 추가되기 때문
3-3. 성공 기준 확인
•
시리얼 모니터에 측정값이 출력되는지 확인하기
•
시트에 실제 측정값이 3회 이상 저장되면 성공임
•
HTTP 상태만으로 성공을 판단하지 않기
◦
시리얼 모니터에 출력되는 HTTP 상태는 참고 사항임
◦
200이나 302가 출력되어도 시트에 행이 생성되지 않았으면 저장된 것이 아님
◦
성공 여부는 언제나 시트의 새 행으로 판단함
단계 | 입력과 성공 기준 |
Ⅱ단계 | 시리얼 모니터에 26,61을 직접 입력 → 시트에 같은 값 한 행 저장 |
Ⅲ단계 | DHT11 측정 → 5초 간격 전송 시도 → 실제 측정값 3회 이상 저장 |
부록. 증상별 점검 항목
오류 메시지를 그대로 읽고 해당하는 줄을 찾기. 구글 측 증상과 아두이노 측 증상으로 구분해 정리함.부록-1. 구글 측 증상과 조치
•
먼저 ?key=나의키&temp=25&humi=60 시험 주소를 시크릿 창에서 열어 구글 측만 단독으로 확인하기
증상 | 확인할 사항과 조치 |
setupSheet 오류 | SHEET_ID가 시트 주소의 값인지 확인함. 해당 계정이 시트를 열 수 있는지 확인함. |
로그인 화면 표시 | 웹 앱의 모든 사용자 접근 설정을 확인함. /dev 대신 /exec 사용 여부를 확인함. |
DENIED | Apps Script·아두이노·시험 주소의 CLASS_KEY가 같은지 확인함. 대소문자도 구분함. |
INVALID | 온도와 습도가 정수인지 확인함. 온도 0~50, 습도 0~100 범위를 확인함. |
SETUP_REQUIRED | setupSheet를 실행하고 기록 탭이 있는지 확인함. |
수정 전 결과가 계속 표시 | 저장 후 배포 관리에서 새 버전으로 갱신했는지 확인함. |
HTTP 302 또는 200, 그런데 기록 없음 | 이 상태만으로 성공을 판단하지 않음. 동일 주소·키로 브라우저 시험 후 Apps Script 실행 기록의 실패 원인을 확인함. |
HTTP 403 또는 404 | 403은 접근 권한·네트워크 제한 가능성을, 404는 배포 ID·배포 상태를 우선 확인함. 코드만으로 원인을 단정하지 않음. |
응답 없음 또는 중복 행 | 시트부터 확인함. 저장 후 응답만 못 받았을 수 있음. 새로고침·재전송 횟수를 확인함. |
부록-2. 아두이노 측 증상과 조치
증상 | 확인할 사항과 조치 |
업로드 또는 포트 오류 | UNO R4 WiFi 선택, USB 데이터 케이블, 포트 번호를 확인함. 포트를 사용하는 다른 프로그램을 닫음. |
WiFiS3.h 파일 없음 | Arduino UNO R4 Boards 설치와 보드 선택을 확인함. ESP32용 WiFi.h로 임의 교체하지 않음. |
DHT11.h 파일 없음 또는 함수 없음 | 기초 코드에서 사용한 라이브러리와 비교함. 이름·제작자·버전 및 readTemperatureHumidity 지원 여부를 확인함. |
WiFi failed 또는 disconnected | SSID·비밀번호·2.4GHz 접속 가능 여부를 확인함. 설정 수정 후 업로드하거나 RESET으로 다시 연결함. |
HTTPS connection failed | Wi-Fi 연결 다음에 인터넷 접속·학교망 제한이므로 수업용으로 보안이 적게 셋팅된 Wifi연결 필요 |
Sensor error 숫자 표시 | 전원 분리 후 S→D4, +→5V, −→GND와 접촉 상태를 확인함. 기초 센서 코드로 따로 시험함. 오류 번호의 의미는 설치한 라이브러리 문서로 확인함. |
시리얼 입력 반응 없음 | 9600bps와 새 줄을 확인함. 통신 응답을 기다린 뒤 다시 입력함. |
기록 간격이 5초보다 김 | 현재 코드는 통신 중 다음 요청을 실행하지 않음. 학교망 지연·서버 처리시간을 함께 확인함. INTERVAL만 줄여 해결하려 하지 않음. |





















