본문으로 바로가기
기술

ESP32 + 웹 대시보드 — 실전 홈 IoT 풀스택 프로젝트

A
AlwaysCorp 기술팀· 기술·개발 콘텐츠 전문
||12분 읽기
#ESP32#IoT#Arduino#PlatformIO#MQTT#Mosquitto#React#대시보드#홈오토메이션#펌웨어

만들 것 — 한 줄 정의

거실의 온습도와 모션을 1만 원짜리 보드 한 장으로 측정하고, 휴대폰 어디서든 웹 대시보드로 보고, 외출 중에도 조명을 켤 수 있는 시스템. 풀스택이라는 단어가 IoT에선 펌웨어부터 React까지를 의미합니다. 이 글은 그 전체 흐름을 한 번에 깔아주는 지도입니다.

ESP32 풀스택 IoT 구성

부품 리스트

  • 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 시스템의 가장 큰 보상입니다.

A

AlwaysCorp 기술팀

기술·개발 콘텐츠 전문

얼웨이즈 블로그에서 유용한 정보와 인사이트를 공유합니다.