웹 대시보드는 라즈베리 파이의 창문이다
센서값을 터미널에만 출력하면 라즈베리 파이 앞에 앉아 있어야 확인할 수 있습니다. 웹 대시보드는 이 값을 브라우저로 보여 주는 창문입니다. 같은 Wi-Fi에 연결된 노트북, 태블릿, 스마트폰에서 라즈베리 파이의 상태를 볼 수 있습니다.
이번 장에서는 작은 웹 서버를 만들고, 측정값을 데이터베이스에 저장한 뒤, 시간순으로 화면에 표시합니다. 실제 센서가 아직 없어도 괜찮습니다. 먼저 가짜 온습도 값을 저장해 구조를 익히고, 나중에 read_sensor() 함수 안쪽만 실제 센서 코드로 바꾸면 됩니다.
이번 프로젝트의 구조는 다음과 같습니다.
센서 측정 코드 → SQLite에 저장 → Flask 웹 서버 → 브라우저 대시보드여기서 Flask는 파이썬으로 웹 페이지를 만드는 가벼운 도구입니다. SQLite는 파일 하나로 동작하는 작은 데이터베이스입니다. 별도 서버를 설치하지 않아도 되므로 라즈베리 파이 실습에 잘 맞습니다.
주의: 이번 장의 Flask 서버는 학습용 개발 서버입니다. 같은 집이나 교실 안의 내부망에서만 사용하세요. 공유기 포트 포워딩으로 인터넷에 바로 공개하지 마세요.
AI 생성 이미지
프로젝트 폴더 만들기
먼저 SSH나 터미널로 라즈베리 파이에 접속합니다. 작업은 홈 디렉터리 아래에서 진행합니다.
cd ~
mkdir -p pi-dashboard
cd pi-dashboard
sudo apt update
sudo apt install -y python3-venv
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install flask프롬프트 앞에 (.venv)가 보이면 가상환경이 활성화된 상태입니다.
(.venv) pi@raspberrypi:~/pi-dashboard $라즈베리 파이에서는 보통 시스템 파이썬을 부를 때 python3를 사용합니다. 다만 가상환경을 활성화한 뒤에는 그 안의 python 명령이 Python 3를 가리키므로, 이후 예제에서는 가상환경 안에서 python을 사용하겠습니다.
앞으로 만들 파일의 최종 구조는 다음과 같습니다. readings.db는 init_db.py를 실행한 뒤에 생깁니다.
pi-dashboard/
├── app.py
├── collector.py
├── init_db.py
├── readings.db
└── templates/
└── index.html역할은 분명히 나눕니다.
collector.py: 센서값을 읽고 저장합니다.app.py: 저장된 값을 웹 페이지로 보여 줍니다.templates/index.html: 브라우저에 보일 화면 모양입니다.readings.db: 측정값이 쌓이는 SQLite 데이터베이스 파일입니다.
측정과 표시를 한 파일에 모두 넣을 수도 있지만, 처음부터 분리하는 습관을 들이면 나중에 프로젝트가 커져도 정리하기 쉽습니다.
SQLite 데이터베이스 준비하기
데이터를 오래 보관하려면 줄줄이 출력하는 것보다 파일에 저장해야 합니다. CSV는 표처럼 단순히 저장할 때 편하고, SQLite는 원하는 조건으로 조회하기 좋습니다. 이번에는 시간순 조회가 쉬운 SQLite를 사용합니다.
init_db.py 파일을 만듭니다.
nano init_db.py다음 코드를 입력합니다.
import sqlite3
DB = 'readings.db'
with sqlite3.connect(DB) as con:
con.execute('''
create table if not exists readings (
id integer primary key autoincrement,
measured_at text not null,
temperature real not null,
humidity real not null
)
''')
print('database ready:', DB)저장한 뒤 실행합니다.
python init_db.py다음과 비슷한 결과가 나오면 준비가 끝났습니다.
database ready: readings.dbls로 확인해 봅니다.
lsreadings.db 파일이 보이면 데이터베이스 파일이 만들어진 것입니다. 이 파일 안에 앞으로 측정값이 한 줄씩 쌓입니다.
측정 코드를 따로 만들기
이제 일정 시간마다 값을 저장하는 코드를 만듭니다.
nano collector.py먼저 실제 센서 대신 임의의 온습도 값을 만들겠습니다.
import random
import sqlite3
import time
from datetime import datetime
DB = 'readings.db'
INTERVAL = 10
def read_sensor():
temperature = 24 + random.uniform(-2, 2)
humidity = 50 + random.uniform(-5, 5)
return round(temperature, 1), round(humidity, 1)
def save_reading(temperature, humidity):
measured_at = datetime.now().isoformat(timespec='seconds')
with sqlite3.connect(DB) as con:
con.execute(
'insert into readings (measured_at, temperature, humidity) values (?, ?, ?)',
(measured_at, temperature, humidity)
)
print(measured_at, temperature, humidity)
if __name__ == '__main__':
print('collector started. press Ctrl+C to stop.')
while True:
temp, hum = read_sensor()
save_reading(temp, hum)
time.sleep(INTERVAL)실행합니다.
python collector.py10초마다 다음처럼 값이 출력됩니다.
collector started. press Ctrl+C to stop.
2026-07-15T14:20:10 24.7 51.2
2026-07-15T14:20:20 23.9 48.8중지하려면 Ctrl+C를 누릅니다.
실제 DHT22 같은 센서를 연결했다면 read_sensor() 함수 안쪽만 바꾸면 됩니다. 중요한 점은 함수가 항상 다음 형태로 값을 돌려주는 것입니다.
return temperature, humidity여기서 temperature와 humidity는 문자열이 아니라 숫자여야 합니다. 웹 서버는 값이 실제 센서에서 왔는지, 임의로 만든 값인지 신경 쓰지 않습니다. 데이터베이스에 같은 모양으로 저장되기만 하면 됩니다.
저장된 데이터 확인하기
웹 페이지를 만들기 전에 데이터가 제대로 들어갔는지 확인합니다.
python - <<'PY'
import sqlite3
with sqlite3.connect('readings.db') as con:
rows = con.execute(
'select measured_at, temperature, humidity from readings order by id desc limit 5'
).fetchall()
for row in rows:
print(row)
PY다음처럼 최근 데이터가 보이면 정상입니다.
('2026-07-15T14:20:20', 23.9, 48.8)
('2026-07-15T14:20:10', 24.7, 51.2)아무것도 나오지 않는다면 collector.py를 먼저 실행했는지 확인하세요. no such table: readings 같은 오류가 나온다면 python init_db.py를 먼저 실행해야 합니다. 또 app.py, collector.py, init_db.py, readings.db가 같은 폴더에 있어야 합니다.
CSV를 쓰고 싶다면 구조는 더 단순합니다. measured_at,temperature,humidity 같은 제목 줄을 만들고, 측정할 때마다 한 줄씩 추가하면 됩니다. 다만 최근 30개 조회, 조건 검색, 정렬이 많아질수록 SQLite가 더 편합니다.
Flask 웹 서버 만들기
이제 저장된 값을 웹으로 보여 줄 차례입니다.
nano app.py다음 코드를 입력합니다.
import sqlite3
from flask import Flask, render_template
DB = 'readings.db'
app = Flask(__name__)
def get_rows(limit=30):
try:
with sqlite3.connect(DB) as con:
con.row_factory = sqlite3.Row
rows = con.execute(
'select measured_at, temperature, humidity from readings order by id desc limit ?',
(limit,)
).fetchall()
except sqlite3.OperationalError:
return []
return list(reversed(rows))
def make_graph_points(rows):
temperatures = [row['temperature'] for row in rows]
if not temperatures:
return ''
low = min(temperatures)
high = max(temperatures)
span = high - low or 1
count = max(len(temperatures) - 1, 1)
points = []
for index, temp in enumerate(temperatures):
x = 20 + index * (360 / count)
y = 160 - ((temp - low) / span) * 120
points.append(f'{x:.1f},{y:.1f}')
return ' '.join(points)
@app.route('/')
def index():
rows = get_rows()
latest = rows[-1] if rows else None
graph_points = make_graph_points(rows)
return render_template(
'index.html',
rows=rows,
latest=latest,
graph_points=graph_points
)@app.route('/')는 브라우저가 첫 화면에 접속했을 때 실행할 함수를 지정합니다. 여기서는 데이터베이스에서 최근 30개 값을 가져오고, HTML 템플릿에 넘깁니다.
get_rows()는 데이터베이스에서 최신 데이터 30개를 가져온 뒤, 그래프를 그리기 좋게 오래된 값부터 최신 값 순서로 다시 정렬합니다. 아직 데이터베이스가 준비되지 않은 상태라면 빈 목록을 돌려주어 웹 화면이 갑자기 멈추지 않게 했습니다.
make_graph_points()는 온도값을 간단한 선 그래프로 그리기 위해 좌표로 바꾸는 함수입니다. 전문 그래프 라이브러리를 쓰지 않고, HTML의 SVG 기능만 사용합니다.
HTML 템플릿 만들기
Flask는 templates 폴더 안의 HTML 파일을 찾아 화면을 만듭니다.
mkdir -p templates
nano templates/index.html다음 내용을 입력합니다.
<!doctype html>
<html lang='ko'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>라즈베리 파이 대시보드</title>
<style>
body { font-family: sans-serif; margin: 24px; background: #f6f7f9; color: #222; }
.card { background: white; padding: 18px; border-radius: 12px; margin-bottom: 16px; box-shadow: 0 2px 8px #0001; }
.latest { font-size: 2rem; font-weight: bold; }
table { width: 100%; border-collapse: collapse; background: white; }
th, td { padding: 10px; border-bottom: 1px solid #ddd; text-align: left; }
svg { width: 100%; max-width: 700px; background: #fff; border: 1px solid #ddd; }
.muted { color: #666; }
</style>
</head>
<body>
<h1>라즈베리 파이 온습도 대시보드</h1>
{% if latest %}
<div class='card'>
<div class='muted'>최근 측정</div>
<div class='latest'>{{ latest['temperature'] }} ℃ / {{ latest['humidity'] }} %</div>
<div class='muted'>{{ latest['measured_at'] }}</div>
</div>
<div class='card'>
<h2>최근 온도 변화</h2>
<svg viewBox='0 0 400 180' role='img' aria-label='최근 온도 변화 선 그래프'>
<line x1='20' y1='160' x2='380' y2='160' stroke='#ccc'/>
<line x1='20' y1='20' x2='20' y2='160' stroke='#ccc'/>
<polyline points='{{ graph_points }}' fill='none' stroke='#e05050' stroke-width='3'/>
</svg>
</div>
<div class='card'>
<h2>측정 기록</h2>
<table>
<tr>
<th>시간</th>
<th>온도</th>
<th>습도</th>
</tr>
{% for row in rows|reverse %}
<tr>
<td>{{ row['measured_at'] }}</td>
<td>{{ row['temperature'] }} ℃</td>
<td>{{ row['humidity'] }} %</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class='card'>
아직 저장된 데이터가 없습니다. 먼저 init_db.py와 collector.py를 실행하세요.
</div>
{% endif %}
</body>
</html>여기서 {{ latest['temperature'] }}처럼 생긴 부분은 Flask가 실제 값으로 바꿔 넣는 자리입니다. {% for row in rows|reverse %}는 여러 개의 측정값을 반복해서 표로 만드는 부분입니다. rows는 그래프를 위해 오래된 값부터 최신 값 순서로 넘어오지만, 표에서는 최근 기록이 위에 보이는 편이 보기 좋아서 reverse를 붙였습니다.
브라우저에서 접속하기
터미널을 두 개 열어 두면 가장 이해하기 쉽습니다.
첫 번째 터미널에서는 측정 코드를 실행합니다.
cd ~/pi-dashboard
source .venv/bin/activate
python collector.py두 번째 터미널에서는 웹 서버를 실행합니다.
cd ~/pi-dashboard
source .venv/bin/activate
python -m flask --app app run --host=0.0.0.0 --port=5000--host=0.0.0.0은 라즈베리 파이 자기 자신뿐 아니라 같은 네트워크의 다른 기기에서도 접속할 수 있게 하는 설정입니다. 이 옵션이 없으면 라즈베리 파이 안에서만 접속되는 경우가 많습니다.
라즈베리 파이에서 직접 확인할 때는 브라우저 주소창에 입력합니다.
http://127.0.0.1:5000다른 노트북이나 스마트폰에서 접속하려면 라즈베리 파이의 주소를 확인합니다.
hostname -I예를 들어 IP 주소가 192.168.0.42라면 같은 Wi-Fi에 연결된 기기에서 다음 주소로 접속합니다.
http://192.168.0.42:5000호스트명이 잘 동작하는 환경이라면 다음처럼 접속할 수도 있습니다.
http://raspberrypi.local:5000화면에 최근 온습도, 선 그래프, 측정 기록 표가 보이면 성공입니다.
포트와 내부망 점검하기
웹 서버는 포트 번호를 통해 접속을 받습니다. 포트는 건물의 출입문 번호처럼 생각하면 됩니다. 이번 실습에서는 5000번 문을 열어 Flask 서버가 요청을 받습니다.
접속이 안 될 때는 다음 순서로 확인하세요.
라즈베리 파이에서
http://127.0.0.1:5000접속이 되는가?Flask 실행 명령에
--host=0.0.0.0을 넣었는가?노트북과 라즈베리 파이가 같은 공유기 또는 같은 Wi-Fi에 연결되어 있는가?
hostname -I로 확인한 IP 주소를 정확히 입력했는가?방화벽을 켜 둔 경우 5000번 포트를 막고 있지 않은가?
UFW 방화벽을 설치해 사용 중이라면 상태를 다음 명령으로 확인할 수 있습니다.
sudo ufw statusStatus: inactive라면 UFW 방화벽은 꺼져 있는 상태입니다. 만약 active이고 내부망에서 접속이 막힌다면, 자신의 네트워크 대역에 맞게 허용해야 합니다.
sudo ufw allow from 192.168.0.0/24 to any port 5000 proto tcp위 예시는 192.168.0.x 대역에서만 5000번 포트 접속을 허용합니다. 집의 IP 대역이 192.168.1.x라면 192.168.1.0/24로 바꿔야 합니다. sudo ufw 명령이 없다고 나오면 UFW를 따로 설치하지 않은 상태일 수 있으므로, 우선 앞의 1~4번 항목부터 확인하세요.
주의:
sudo ufw allow 5000처럼 모든 곳에서 접속을 허용하는 명령은 신중하게 사용하세요. 특히 공유기에서 포트 포워딩까지 설정하면 외부 인터넷에 노출될 수 있습니다.
백그라운드 측정과 웹 표시를 분리하는 이유
이번 프로젝트에서 collector.py와 app.py를 나눈 이유는 단순합니다. 측정은 계속되어야 하고, 웹 화면은 필요할 때만 데이터를 읽으면 되기 때문입니다.
만약 웹 서버 코드 안에서 센서를 직접 읽으면 브라우저를 새로고침할 때마다 센서 읽기가 실행됩니다. 센서가 느리거나 가끔 오류를 내면 웹 페이지도 함께 느려집니다. 반대로 측정 코드를 따로 실행하면 다음 장점이 있습니다.
센서 오류가 나도 웹 화면 구조를 따로 점검할 수 있습니다.
측정 주기를 10초, 1분, 5분처럼 독립적으로 조절할 수 있습니다.
나중에 자동 실행 서비스로 등록하기 쉽습니다.
웹 화면은 저장된 데이터를 빠르게 읽기만 하면 됩니다.
지금은 터미널 두 개에서 각각 실행하지만, 이후 장에서는 이 프로그램들을 부팅 시 자동으로 실행되도록 서비스로 등록할 수 있습니다.
마무리 점검
이번 장의 목표는 라즈베리 파이를 작은 데이터 서버로 만드는 것이었습니다. 다음 항목을 스스로 확인해 보세요.
readings.db파일이 만들어졌다.collector.py가 주기적으로 값을 저장한다.app.py가 Flask 웹 서버로 실행된다.라즈베리 파이에서
http://127.0.0.1:5000으로 접속된다.같은 네트워크의 다른 기기에서
http://라즈베리파이IP:5000으로 접속된다.대시보드에 최근 값, 그래프, 표가 표시된다.
이제 라즈베리 파이는 센서를 읽는 장치를 넘어, 데이터를 모으고 보여 주는 작은 서버가 되었습니다. 다음 단계에서는 카메라와 멀티미디어 기능을 더해 라즈베리 파이가 이미지와 영상까지 다루도록 확장합니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.