“이번 글은 ESP8266 스마트팜 온습도 모니터링 시스템을 어떻게 구축했는지 기록한 내용입니다.”
실내 분무수경 재배기를 운영하면서 가장 크게 느낀 것은 환경 데이터 관리의 절대적 중요성이다. 이전 글에서 설명한 것처럼, 식물 생육을 좌우하는 핵심 환경 지표 중 하나는 VPD(증기압 부족)이며, VPD는 온도와 습도 조합이 조금만 틀어져도 급격하게 변화한다.
예를 들어, 온도와 습도가 가 조금만 맞지 않아도 버터헤드는 바로 팁번이 발생하고 실내 재배는 더 이상 감(感)으로 관리할 수 없고, 데이터 기반 환경 관리가 필수가 되었다. 그래서 이번에는 ESP8266 + DHT22 센서를 활용해 재배기 환경 데이터를 자동으로 수집하고 Firestore → BigQuery → Looker Studio로 이어지는 자동 환경 모니터링 파이프라인을 구축했다.
🧩 전체 아키텍처(Architecture) 흐름
ESP8266 → Firestore(실시간 저장) → BigQuery(Export & Transform) → Looker Studio(대시보드 시각화)
- ESP8266 : DHT22 센서 값 측정 → JSON으로 Firestore API에 전송
- Firestore : 원본 시계열 데이터 저장
- BigQuery : 정규화/가공 테이블 생성
- Looker Studio : 온·습도 실시간 모니터링 대시보드 구성
1️⃣ ESP8266 + DHT22 센서 연결
아래는 DHT22와 ESP8266(NodeMCU 기준)의 기본 배선이다.
| DHT22 | ESP8266(예: NodeMCU) |
|---|---|
| VCC (+) | 3.3V |
| GND | GND |
| DATA | D4(GPIO2) 또는 사용 가능한 핀 |
처음에 “VCC가 + 맞나?”라는 의문이 있었지만, 맞다. VCC = 센서 전원(+) 이다. 센서는 재배기 내부에서도 사용 가능하지만 직접적으로 물이 닿지 않는 위치에 설치하고 공기 순환이 자연스럽게 이루어지도록 배치해야 한다.
또한 전체 재배기에 도입하기 전에 테스트용으로 1개씩 총 3개를 먼저 제작해 안정성을 검증했다. 필요한 부품과 금액은 아래와 같다.
- ESP8266 : 3,490원/개 (3개)
- DHT22 센서: 1,790원/개 (3개)
- USB 2.0 to Micro 5핀 케이블 : 1,200원/개 (3개)
- 4구 USB 확장허브 : 4,740원/개 (1개)
- USB 1구 충전기 : 3,020원/개 (1개)
- 케이스 : 집에 있는 케이블 정리 케이스를 사용

2️⃣ Wi-Fi 연결 시 가장 주의할 점
ESP8266은 2.4GHz Wi-Fi만 지원한다. 처음에 농장 Wi-Fi가 “2G/5G” 두 가지로 나뉘어 있는데 2G에만 연결 해야 한다는 사실을 몰랐어서 5G SSID에 연결했다가 연결 장애를 겪었다.
- Wi-Fi 이름이 2G/5G 분리되어 있다면 → 2G 선택
- 통합 SSID라면 → 공유기 설정에서 2.4GHz 우선으로 설정
Wi-Fi 문제만 해결되면 네트워크 연결은 매우 안정적이다.
3️⃣ ESP8266 → Firestore로 데이터 전송
ESP8266에서는 REST API 방식으로 HTTP POST 요청을 보내 Firestore 문서를 생성하게 구성했다.
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include "DHT.h"
#include "time.h"
#define DHTPIN 2
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// 🔹 Wi-Fi 정보
const char* ssid_farm = "wifi_name";
const char* pass_farm = "wifi_pw";
const char* ssid_home = "wifi_name";
const char* pass_home = "wifi_pw";
// 🔹 Cloud Run 엔드포인트
const char* host = "cloud_run_endpoint_name"; //Cloud Run에서 만든 엔트포인트 주소
const int httpsPort = 443;
// 🔹 장비 ID
String deviceId = "device_name"; // 여러 재배기에 설치한다면 재배기마다 이름
// 🔹 NTP 서버 (한국시간)
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 9 * 3600; // GMT+9
const int daylightOffset_sec = 0;
void setupWiFi() {
Serial.println("📡 Wi-Fi 연결 시도 중...");
// 1️⃣ 집 Wi-Fi 시도
WiFi.begin(ssid_home, pass_home);
for (int i = 0; i < 15 && WiFi.status() != WL_CONNECTED; i++) {
delay(1000);
Serial.print(".");
}
// 2️⃣ 실패 시 농장 Wi-Fi 시도
if (WiFi.status() != WL_CONNECTED) {
Serial.println("\n❌ 집 Wi-Fi 연결 실패 → 농장 Wi-Fi 시도");
WiFi.begin(ssid_farm, pass_farm);
for (int i = 0; i < 15 && WiFi.status() != WL_CONNECTED; i++) {
delay(1000);
Serial.print(".");
}
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n✅ Wi-Fi 연결 성공");
Serial.print("IP 주소: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\n❌ Wi-Fi 연결 실패. 1분 후 재시도 예정");
}
}
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("🔌 NodeMCU 부팅 중...");
setupWiFi();
dht.begin();
// 시간 동기화
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("⏰ 한국시간 동기화 중...");
delay(2000);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
setupWiFi();
}
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
if (isnan(temperature) || isnan(humidity)) {
Serial.println("⚠️ DHT22 읽기 실패, 30초 후 재시도");
delay(30000);
return;
}
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("⏱ NTP 시간 불러오기 실패");
delay(30000);
return;
}
char timeString[30];
strftime(timeString, sizeof(timeString), "%Y-%m-%d %H:%M:%S", &timeinfo);
String localTime = String(timeString);
// HTTPS 연결
WiFiClientSecure client;
client.setInsecure(); // SSL 검증 비활성화
Serial.print("☁️ Cloud Run 연결 중...");
if (!client.connect(host, httpsPort)) {
Serial.println("❌ 연결 실패");
delay(60000);
return;
}
Serial.println("✅ 연결 성공!");
// JSON 데이터 생성
String jsonData = String("{\"deviceId\":\"") + deviceId +
"\",\"temperature\":" + temperature +
",\"humidity\":" + humidity +
",\"createdAt\":\"" + localTime + "\"}";
// HTTP POST 요청
client.println("POST / HTTP/1.1");
client.println("Host: " + String(host));
client.println("Content-Type: application/json");
client.print("Content-Length: ");
client.println(jsonData.length());
client.println();
client.println(jsonData);
// 응답 처리
while (client.connected()) {
String line = client.readStringUntil('\n');
if (line == "\r") break;
}
String response = client.readString();
Serial.println("서버 응답:");
Serial.println(response);
client.stop();
// 시리얼 출력
Serial.println("🌡 데이터 전송 완료");
Serial.print("시간: "); Serial.println(localTime);
Serial.print("온도: "); Serial.print(temperature);
Serial.print(" °C, 습도: "); Serial.print(humidity); Serial.println(" %");
// 20분 대기 (1200초)
Serial.println("⏳ 다음 측정까지 20분 대기...");
delay(1200 * 1000);
}
전송된 데이터가 Firestore 컬렉션에 정상적으로 쌓이기 시작하면 환경 데이터 수집 파이프라인의 1단계가 완료된다. Firestore에는 아래와 같은 형태로 저장된다.
{
"deviceId": "aeroplenty-A1",
"temperature": 24.2,
"humidity": 63.5,
"createdAt": "2025-11-07T17:05:00Z"
}
4️⃣ BigQuery로 자동 Export 후 가공
Firestore에 저장된 문서는 설정된 규칙에 따라 1시간마다 BigQuery Raw 테이블로 자동 Export되도록 구성했다.
▶ BigQuery 가공용 테이블 생성 SQL
아래 SQL을 실행해 JSON 구조를 분석용 테이블로 정제한다.
CREATE OR REPLACE TABLE `table_name` AS
SELECT
FORMAT_DATETIME(
'%Y-%m-%d %H:%M:%S',
DATETIME(timestamp, "Asia/Seoul")
) AS createdAt_kst,
SAFE_CAST(JSON_VALUE(data, '$.temperature') AS FLOAT64) AS temperature,
SAFE_CAST(JSON_VALUE(data, '$.humidity') AS FLOAT64) AS humidity,
JSON_VALUE(data, '$.deviceId') AS deviceId,
JSON_VALUE(data, '$.createdAt') AS createdAt_raw,
operation,
timestamp
FROM
`rawdata_table_name`
WHERE
operation IN ('CREATE', 'UPDATE') -- 삭제 이벤트 제외
AND JSON_VALUE(data, '$.deviceId') IS NOT NULL;
이 과정을 통해 데이터가 시계열 분석 및 대시보드 구성에 적합한 형태로 깔끔하게 정규화된다.
5️⃣ Looker Studio로 온습도 대시보드 만들기
BigQuery의 플랫 테이블을 Looker Studio 데이터 소스로 연결하면 즉시 시각화를 시작할 수 있다.
구성 예시
- 📈 온도 시계열 그래프
- 💧 습도 시계열 그래프
- 🎛 deviceId(재배기)별 필터
- 📊 평균/최대/최소 요약 지표
스마트폰에서도 쉽게 확인할 수 있어 재배 환경 변화를 쉽게 알 수 있었다.
구성 예시
처음에는 습도 기준값을 ±5% 범위로 설정해두었는데 데이터를 확인해보니 편차가 너무 크게 나와 지금은 ±2%로 조정, 추후에는 ±1%로 더 세밀하게 조정할 예정이다. 이러한 미세 조정은 데이터 기반 의사결정 없이는 불가능하다.

✨ 마무리: “데이터가 들어오면 농장은 달라진다”
스마트팜 운영은 결국 데이터 기반 환경 제어에서 시작하고 완성된다. 온도·습도는 작은 변화에도 작물 생육에 치명적인 영향을 주기 때문에 이를 실시간으로 모니터링하는 것은 선택이 아니라 필수다.
ESP8266 + Firestore + BigQuery + Looker Studio 기반 파이프라인은 아직 설비업체 수준의 자동 환경 제어 시스템까지는 아니지만, 내가 운영하는 실내 재배기의 환경을 좀 더 정량적이고 객관적이며, 실시간에 가깝게 관리할 수 있게 해 줄 것이다.🚀