# ESP32 Web Flasher

브라우저 기반 ESP32 펌웨어 플래시 시스템
Docker 컨테이너 아키텍처 기술 분석 보고서

작성일
2025년 5월
버전
v1.0.0
분류
기술 분석
저장소
gitea.byunc.com/byun/webflash
1

## 프로젝트 개요

ESP32 Web Flasher는 ESP32 계열 마이크로컨트롤러에 펌웨어를 플래시하는 작업을 웹 브라우저에서 수행할 수 있게 해주는 Docker 기반 웹 애플리케이션입니다. Arduino IDE, esptool.py 등 별도 소프트웨어 설치 없이 Chrome/Edge 브라우저와 USB 케이블만으로 동작합니다.

목적
펌웨어 배포·업데이트 과정을 웹 기반으로 단순화하여 설치 장벽 제거
핵심 기술
Web Serial API + Espressif esp-web-tools + Docker 멀티컨테이너
접근 URL
`http://localhost:3100`
브라우저 Chrome/Edge 89+ 필요

### 지원 칩 패밀리

칩 | USB 방식 | 비고
--- | --- | ---
ESP32-S3 | 내장 USB-OTG (드라이버 불필요) | 권장
ESP32-S2 | 내장 USB-OTG | 지원
ESP32-C3 | USB-CDC | 지원
ESP32 | USB-UART 변환기 (CP2102 등) | 변환칩 필요

2

## 시스템 아키텍처

두 개의 Docker 컨테이너가 Docker Compose로 오케스트레이션됩니다. Nginx가 리버스 프록시 역할을 하여 정적 파일 서빙과 API 라우팅을 모두 담당합니다.

컨테이너 구성

컨테이너 | 이미지 | 내부 포트 | 외부 포트 | 역할
--- | --- | --- | --- | ---
`esp32-flasher-frontend` | nginx:alpine | 80 | 3100 | 정적 파일 서빙 + 리버스 프록시
`esp32-flasher-backend` | node:20-alpine | 3000 | 비공개 (내부망) | REST API + 파일 관리

### 통신 흐름

사용자 브라우저
Chrome/Edge
→
Frontend
Nginx :3100
→
Backend
Express :3000
→
스토리지
Docker Volume
사용자 브라우저
Web Serial API
→
USB 연결
CDC/OTG
→
ESP32
ROM Bootloader

### Nginx 라우팅 규칙

    location /api/      →  proxy_pass http://backend:3000/api/
    location /firmware/ →  proxy_pass http://backend:3000/firmware/
    location /          →  try_files $uri /index.html  (SPA 라우팅)

백엔드는 외부에 포트를 노출하지 않으며, Docker 내부 DNS(`backend:3000`)를 통해서만 접근됩니다.

3

## 파일 구조 및 구성요소 분석

webflash/ ├── backend/ │ ├── Dockerfile # Node.js 20 Alpine 이미지 │ ├── package.json # Express, Multer, UUID, CORS │ └── server.js # REST API 서버 (188줄) ├── frontend/ │ ├── Dockerfile # Nginx Alpine 이미지 │ ├── nginx.conf # 리버스 프록시 설정 │ ├── index.html # SPA (356줄, 한국어 UI) │ ├── css/ │ │ └── style.css # 다크 테마 (388줄) │ └── js/ │ └── app.js # Web Serial 로직 (396줄) ├── docker-compose.yml # 멀티컨테이너 오케스트레이션 └── README.md # 프로젝트 문서
backend/server.js

Express.js 기반 REST API 서버. Multer로 멀티파트 업로드 처리, UUID로 펌웨어 식별자 생성, 파일시스템 기반 JSON 메타데이터 관리. esp-web-tools가 요구하는 manifest.json을 동적으로 생성하여 플래싱 파라미터를 제공합니다.

frontend/js/app.js

Vanilla JavaScript로 작성된 SPA 로직. Web Serial API로 ESP32 연결을 확인하고, XHR 기반 업로드 진행률 추적, 드래그&드롭 파일 선택, esp-web-install-button 웹 컴포넌트 manifest 동적 주입을 담당합니다.

frontend/nginx.conf

SPA 라우팅(try_files), API 역방향 프록시, 펌웨어 파일 서빙을 하나의 Nginx 설정으로 처리합니다. 백엔드는 Docker DNS를 통해 참조됩니다.

docker-compose.yml

backend/frontend 두 서비스를 정의합니다. backend 헬스체크(30초 간격), frontend는 backend depends_on, 펌웨어는 Named Volume으로 영속 저장됩니다.

4

## 기술 스택 상세

백엔드
🟩
Node.js 20 (Alpine)
LTS 버전, 경량 Alpine 이미지로 컨테이너 크기 최소화
⚡
Express 4.18.2
REST API 라우팅, 미들웨어 파이프라인
📤
Multer 1.4.5
multipart/form-data 파일 업로드, 8MB 제한, .bin 필터
🔑
UUID v9
펌웨어 고유 식별자 생성 (RFC 4122 v4)
🌐
CORS 2.8.5
크로스 오리진 요청 허용, ALLOWED_ORIGIN 환경변수로 제어
프론트엔드
🌍
Vanilla JavaScript (ES2020+)
프레임워크 없음, 번들러 없음 — 경량 직접 실행
🔌
Web Serial API
브라우저 네이티브 시리얼 통신, Chrome/Edge 89+ 지원
⚙️
esp-web-tools v10
Espressif 공식 웹 컴포넌트, manifest.json 기반 플래싱
🎨
CSS Custom Properties
다크 테마, GitHub Dark 팔레트, 반응형 그리드
🔀
Nginx Alpine
정적 서빙 + 리버스 프록시 (이미지 ~40 MB)

### 인프라

구성요소 | 기술 | 버전 | 용도
--- | --- | --- | ---
컨테이너 런타임 | Docker Engine | 20.10+ | 컨테이너 빌드·실행
오케스트레이션 | Docker Compose | v2 | 멀티서비스 관리
스토리지 | Docker Named Volume | — | 펌웨어 바이너리 영속 저장
메타데이터 DB | JSON 파일 (_metadata.json) | — | 펌웨어 정보 레지스트리

5

## 핵심 기능 분석

### ① 펌웨어 업로드 관리

multipart/form-data 방식으로 최대 3개의 .bin 파일(firmware, bootloader, partitions)을 동시 업로드합니다. 각 파일은 `타임스탬프-랜덤4바이트.bin` 형태로 저장되고, UUID 기반 메타데이터가 JSON 파일에 기록됩니다.

    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "My Product v1.0",
      "version": "1.0.0",
      "chipFamily": "ESP32-S3",
      "createdAt": "2025-05-17T10:00:00.000Z",
      "parts": [
        { "file": "1747123456-ab12.bin", "offset": "0x0000",  "label": "Bootloader" },
        { "file": "1747123456-cd34.bin", "offset": "0x8000",  "label": "Partition Table" },
        { "file": "1747123456-ef56.bin", "offset": "0x10000", "label": "Application" }
      ]
    }

### ② Manifest 동적 생성

백엔드가 저장된 메타데이터를 esp-web-tools 표준 manifest 형식으로 변환합니다. offset은 16진수 문자열에서 정수로 자동 변환됩니다.

    GET /api/firmware/:id/manifest

    {
      "name": "My Product v1.0",
      "version": "1.0.0",
      "new_install_prompt_erase": true,
      "builds": [{
        "chipFamily": "ESP32-S3",
        "parts": [
          { "path": "/firmware/files/xxxx.bin", "offset": 0 },
          { "path": "/firmware/files/yyyy.bin", "offset": 32768 },
          { "path": "/firmware/files/zzzz.bin", "offset": 65536 }
        ]
      }]
    }

### ③ Web Serial API 연결 확인

플래시 전에 ESP32가 올바르게 연결되었는지 VID/PID로 확인합니다. Espressif의 USB Vendor ID는 `0x303A`로, 이 값을 감지하면 ESP32 장치로 판단합니다.

장치 확인 로직

VID | 장치 | 비고
--- | --- | ---
`0x303A` | Espressif ESP32 (내장 USB) | Espressif 확인
`0x10C4` | Silicon Labs CP210x | USB-UART 변환기
`0x1A86` | CH340/CH341 | USB-UART 변환기
기타 | 알 수 없는 장치 | 경고 표시

### ④ 드래그&드롭 파일 업로드

DataTransfer API를 이용한 드래그&드롭 인터페이스와 XHR 기반 업로드 진행률 추적을 구현합니다. `fetch()`는 업로드 진행률을 지원하지 않으므로 XMLHttpRequest를 직접 사용합니다.

### ⑤ 시리얼 모니터

로그 출력 유형

타입 | CSS 클래스 | 색상 | 용도
--- | --- | --- | ---
ok | `line-ok` | teal (#00c896) | 성공 메시지
err | `line-err` | red (#f85149) | 오류 메시지
warn | `line-warn` | yellow (#e3b341) | 경고 메시지
info | `line-info` | muted (#8b949e) | 정보 메시지

6

## API 명세

메서드 | 경로 | 설명 | 응답
--- | --- | --- | ---
GET | `/api/health` | 헬스 체크 (Docker 프로브용) | `{ status: "ok", timestamp }`
GET | `/api/firmware` | 펌웨어 목록 조회 | 메타데이터 배열 JSON
POST | `/api/firmware/upload` | 펌웨어 업로드 (multipart) | `{ success, id, firmware }`
GET | `/api/firmware/:id/manifest` | esp-web-tools manifest 생성 | manifest JSON
DELETE | `/api/firmware/:id` | 펌웨어 삭제 (파일 + 메타데이터) | `{ success: true }`
GET | `/firmware/files/:filename` | 바이너리 파일 정적 서빙 | application/octet-stream

### 업로드 필드 상세

필드명 | 타입 | 필수 | 기본값 | 설명
--- | --- | --- | --- | ---
`firmware` | file (.bin) | 필수 | — | 애플리케이션 바이너리
`bootloader` | file (.bin) | 선택 | — | 부트로더 (offset 0x0000)
`partitions` | file (.bin) | 선택 | — | 파티션 테이블 (offset 0x8000)
`name` | string | 선택 | 파일명 | 펌웨어 이름
`version` | string | 선택 | 1.0.0 | 버전 문자열
`chipFamily` | string | 선택 | ESP32-S3 | 칩 패밀리
`flashAddress` | string (hex) | 선택 | 0x10000 | 앱 플래시 주소

7

## 데이터 흐름

### 펌웨어 업로드 흐름

①
브라우저 FormData
→
②
Nginx 프록시
→
③
Multer 파싱
→
④
Disk 저장
→
⑤
JSON 메타 기록

### 플래싱 흐름

①
펌웨어 선택
→
②
manifest URL 주입
→
③
esp-web-tools 기동
→
④
manifest fetch
⑤
.bin 파일 fetch
→
⑥
Web Serial 연결
→
⑦
ESP32 ROM 부트로더
→
⑧
플래시 완료

플래싱 단계에서 .bin 파일은 서버에서 브라우저로 다운로드된 후, 브라우저가 직접 Web Serial API를 통해 ESP32에 씁니다. 서버는 플래시 과정에 관여하지 않으므로 네트워크 지연의 영향이 최소화됩니다.

8

## 보안 분석

### 현재 구현된 보안 조치

항목 | 구현 여부 | 방법
--- | --- | ---
파일 유형 제한 | ✓ 구현 | Multer fileFilter — .bin 확장자만 허용
파일 크기 제한 | ✓ 구현 | 8 MB 상한 (limits.fileSize)
XSS 방지 | ✓ 구현 | escHtml() 함수로 사용자 입력 이스케이프
CORS 제어 | ✓ 구현 | ALLOWED_ORIGIN 환경변수로 설정 가능
안전한 파일명 | ✓ 구현 | 원본 파일명 미사용, 타임스탬프+랜덤 생성
인증/인가 | 미구현 | 현재 MVP 단계, 토큰 인증 추가 필요
Rate Limiting | 미구현 | 무차별 업로드 공격 방지 필요
HTTPS | 미구현 | 외부 배포 시 필수 (현재 HTTP)

### ESP32 하드웨어 보안 옵션

1
Flash Encryption
AES-256으로 플래시 메모리 암호화 — 덤프 시 암호화된 데이터만 읽힘
2
Secure Boot V2
부팅 시 서명 검증 — 미서명 펌웨어 실행 차단
3
NVS Encryption
비휘발성 저장소(설정 데이터) 암호화
4
JTAG 비활성화
eFuse로 디버그 인터페이스 영구 차단

### 추천 보안 강화 방향

    # HTTPS (Nginx SSL 설정 예시)
    server {
        listen 443 ssl;
        ssl_certificate     /etc/ssl/cert.pem;
        ssl_certificate_key /etc/ssl/key.pem;
        ...
    }

    # Flash Encryption 활성화 (esptool)
    espefuse.py --chip esp32s3 burn_efuse DIS_DOWNLOAD_PLAIN_DECRYPT

9

## 배포 및 운영

### Docker 구성 상세

항목 | 백엔드 | 프론트엔드
--- | --- | ---
베이스 이미지 | node:20-alpine | nginx:alpine
예상 이미지 크기 | ~400 MB | ~40 MB
노출 포트 | 없음 (내부 전용) | 3100:80
재시작 정책 | unless-stopped | unless-stopped
헬스 체크 | 30초 간격 (3회 재시도) | 없음
볼륨 | firmware_uploads:/app/uploads | 없음

### 환경 변수

변수명 | 기본값 | 설명
--- | --- | ---
`PORT` | 3000 | 백엔드 리스닝 포트
`NODE_ENV` | production | Node.js 실행 환경
`ALLOWED_ORIGIN` | * (전체 허용) | CORS 허용 오리진

### 운영 명령

    # 빌드 및 시작
    docker compose up -d --build

    # 로그 확인
    docker compose logs -f

    # 재시작
    docker compose restart

    # 중지 (데이터 유지)
    docker compose down

    # 완전 초기화 (데이터 삭제)
    docker compose down -v

10

## 결론 및 확장 방향

### 현재 구현 평가

✅
기술적 완성도
Web Serial API + esp-web-tools 연동, 멀티파트 업로드, 동적 manifest 생성이 모두 구현되어 있음
✅
Docker 아키텍처
백엔드/프론트엔드 분리, 헬스 체크, Named Volume, 내부 네트워크 격리 적용
✅
사용자 경험
한국어 UI, 다크 테마, 드래그&드롭, 실시간 진행률, 시리얼 모니터 제공
⚠️
브라우저 제한
Web Serial API는 Chrome/Edge 89+만 지원 — Firefox, Safari 미지원
⚠️
인증 미구현
현재 MVP 수준으로 인증 없이 누구나 업로드/삭제 가능

### 확장 로드맵

단계 | 기능 | 우선순위
--- | --- | ---
v1.1 | HTTPS 지원 (Nginx SSL/TLS), 업로드 인증 토큰 | 높음
v1.2 | Rate Limiting, 플래시 이력 로깅, MAC 주소 기반 기기 인증 | 중간
v2.0 | 결제 연동 (Stripe), 1회용 플래시 토큰, 라이선스 서버 | 계획
v2.1 | S3 호환 스토리지 연동, CDN 배포, 다국어 UI | 계획
v3.0 | OTA 업데이트 통합, 구독 모델, 관리자 대시보드 | 계획

### 상업화 가능성

Espressif 공식 esp-web-tools를 기반으로 하므로 기술 안정성이 높습니다. ESP32-S3의 내장 USB-OTG 지원으로 별도 USB-UART 변환 칩 없이 플래싱이 가능하고, 이는 제품 원가 절감으로 이어집니다. MAC 주소 기반 라이선스 바인딩(ESP32의 eFuse MAC은 위변조 난이도 높음)과 결합하면 펌웨어 유료 판매 플랫폼으로 확장할 수 있습니다.

현재 MVP 아키텍처(파일 기반 JSON 메타데이터)는 소규모 운영에 적합하며, 수백~수천 건 이상의 트래픽이 예상되면 PostgreSQL 등 관계형 DB와 S3 호환 스토리지로 마이그레이션이 권장됩니다.

ESP32 Web Flasher 기술 분석 보고서 — 2025년 5월 작성