만들 것 — 한 줄 정의
거실의 온습도와 모션을 1만 원짜리 보드 한 장으로 측정하고, 휴대폰 어디서든 웹 대시보드로 보고, 외출 중에도 조명을 켤 수 있는 시스템. 풀스택이라는 단어가 IoT에선 펌웨어부터 React까지를 의미합니다. 이 글은 그 전체 흐름을 한 번에 깔아주는 지도입니다.
부품 리스트
- ESP32 DevKit — Wi-Fi/BLE 내장, 약 7,000원
- DHT22 — 온습도 센서, ±0.5°C 정확도
- PIR 모션 센서 — HC-SR501
- 5V 릴레이 모듈 — 조명 제어용
- 점퍼 와이어, 브레드보드, USB-C 케이블
총 2만 원 안쪽에서 풀스택 IoT 학습 키트가 갖춰집니다.
1단계 — 펌웨어 (PlatformIO + Arduino)
VS Code의 PlatformIO 익스텐션을 설치하면 ESP32 개발 환경 셋업이 5분이면 끝납니다. platformio.ini만 한 번 설정해두면 됩니다.
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
knolleary/PubSubClient @ ^2.8
adafruit/DHT sensor library @ ^1.4
monitor_speed = 115200
펌웨어 핵심부:
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
DHT dht(15, DHT22);
WiFiClient wifi;
PubSubClient mqtt(wifi);
const char* SSID = "...";
const char* PASS = "...";
const char* BROKER = "broker.example.com";
void reconnect() {
while (!mqtt.connected()) {
if (mqtt.connect("livingroom-esp32", "user", "pw")) {
mqtt.subscribe("home/light/cmd");
} else delay(2000);
}
}
void onMessage(char* topic, byte* payload, unsigned len) {
if (strcmp(topic, "home/light/cmd") == 0) {
digitalWrite(13, payload[0] == ''1'');
}
}
void setup() {
pinMode(13, OUTPUT);
WiFi.begin(SSID, PASS);
while (WiFi.status() != WL_CONNECTED) delay(300);
mqtt.setServer(BROKER, 8883);
mqtt.setCallback(onMessage);
dht.begin();
}
void loop() {
if (!mqtt.connected()) reconnect();
mqtt.loop();
static unsigned long last = 0;
if (millis() - last > 5000) {
last = millis();
char buf[64];
snprintf(buf, sizeof(buf),
"{\"t\":%.1f,\"h\":%.1f}",
dht.readTemperature(), dht.readHumidity());
mqtt.publish("home/livingroom/temp", buf, true);
}
}
처음 동작 확인은 시리얼 모니터로. Wi-Fi 연결 후 5초마다 데이터를 발행하는지 보고, MQTT Explorer 같은 GUI 클라이언트로 토픽이 실제로 들어오는지 확인합니다.
2단계 — 브로커 (Docker로 5분)
# docker-compose.yml
services:
mosquitto:
image: eclipse-mosquitto:2
ports: ["1883:1883", "8083:8083", "8883:8883"]
volumes:
- ./mosquitto.conf:/mosquitto/config/mosquitto.conf
- ./passwd:/mosquitto/config/passwd
- ./certs:/mosquitto/certs
mosquitto.conf는 인증을 강제하고 평문 1883은 로컬에서만 허용, 외부는 8883 TLS만 노출하는 게 정석입니다. mosquitto_passwd 명령으로 사용자 추가 후 passwd 파일을 마운트합니다.
3단계 — 백엔드 (Node.js)
ESP32가 발행하는 메시지를 받아 시계열 DB에 적재하고, 프론트엔드에 실시간 push하는 역할입니다.
import mqtt from "mqtt";
import { Server } from "socket.io";
import { prisma } from "./db";
const broker = mqtt.connect("mqtts://broker.example.com:8883", {
username: "backend", password: process.env.MQTT_PW,
});
const io = new Server(httpServer, { cors: { origin: "*" } });
broker.on("connect", () => broker.subscribe("home/+/+", { qos: 1 }));
broker.on("message", async (topic, payload) => {
const data = JSON.parse(payload.toString());
const [, room, sensor] = topic.split("/");
await prisma.reading.create({
data: { room, sensor, value: data.t ?? data.v, ts: new Date() },
});
io.emit("reading", { room, sensor, value: data.t ?? data.v });
});
// 프론트에서 조명 제어 명령 받기
io.on("connection", (socket) => {
socket.on("light:set", (on: boolean) => {
broker.publish("home/light/cmd", on ? "1" : "0");
});
});
TimescaleDB(Postgres 확장)를 쓰면 시계열 쿼리가 매우 빨라집니다. time_bucket(''5 minutes'', ts)로 5분 평균을 즉시 뽑을 수 있습니다.
4단계 — 프론트엔드 (React)
import { useEffect, useState } from "react";
import { io } from "socket.io-client";
import { LineChart, Line, XAxis, YAxis, Tooltip } from "recharts";
const socket = io("https://api.example.com");
export default function Dashboard() {
const [readings, setReadings] = useState<Reading[]>([]);
const [light, setLight] = useState(false);
useEffect(() => {
fetch("/api/readings/recent").then(r => r.json()).then(setReadings);
socket.on("reading", (r) => setReadings(prev => [...prev.slice(-100), r]));
return () => { socket.off("reading"); };
}, []);
const toggle = () => {
socket.emit("light:set", !light);
setLight(!light);
};
return (
<div>
<LineChart width={600} height={300} data={readings}>
<XAxis dataKey="ts"/><YAxis/><Tooltip/>
<Line dataKey="value" stroke="#7c3aed"/>
</LineChart>
<button onClick={toggle}>{light ? "끄기" : "켜기"}</button>
</div>
);
}
운영에서 챙길 포인트
- OTA 업데이트 — ESP32는 펌웨어를 무선 업데이트할 수 있습니다(
ArduinoOTA). 보드를 들고 와 USB 꽂는 일을 줄여줍니다. - Watchdog — 펌웨어가 행에 걸렸을 때 자동 재부팅하도록
esp_task_wdt를 사용합니다. - 시계열 보존 정책 — 1초 단위 원본은 일주일, 5분 평균은 1년 보관처럼 다운샘플링 정책을 미리 정합니다.
- 디바이스 인증서 — 운영 단계에서는 디바이스마다 X.509 인증서를 발급하면 토큰 도난 시 개별 폐기 가능.
다음 단계
이 한 세트를 만들고 나면 Home Assistant 통합(MQTT 자동 발견 규약), Matter 표준으로의 마이그레이션, 에너지 모니터링(스마트 플러그)으로 자연스럽게 확장됩니다. 한 번 흐름을 잡아두면 새 디바이스 추가는 펌웨어 변경 없이 토픽만 추가해서 끝납니다. 이게 IoT 시스템의 가장 큰 보상입니다.