
이 글이 나한테 해당되는지 먼저 확인해보세요
이런 경험이 있다면 이 글을 끝까지 읽어야 합니다.
- 모니터링 프로그램을 켜놨는데 화면을 끄면 알람 기록이 안 된다
- 야간에 설비가 돌아가는데 아무도 화면을 보고 있지 않아서 이력이 없다
- "분명히 알람 났을 텐데" 싶은데 이력이 텅 비어 있다
- React나 JavaScript로 주기적인 조회를 구현했는데 탭을 닫으면 멈춘다
현장에서 만드는 모니터링 툴이 야간에 무용지물이 되는 이유, 그리고 어떻게 바꿨는지 정리해봤습니다.
용어 먼저 짚고 넘어갈게요

이 글에서 자주 나오는 단어 두 개를 먼저 설명하겠습니다.
기술 용어라 처음 보면 낯설 수 있는데, 개념 자체는 단순합니다.
폴링 (Polling)이란
폴링은 "주기적으로 확인하는 행위"입니다.
1초마다 PLC에 값을 읽어오는 것, 5초마다 서버에 새 데이터가 있는지 물어보는 것, 이런 게 전부 폴링입니다.
카카오톡 알림이 오길 기다리는 게 아니라, 내가 직접 1분마다 앱을 열어서 새 메시지가 있는지 확인하는 것과 같습니다.
브라우저 폴링 (Browser-side Polling)이란
이 폴링을 브라우저(크롬, 엣지 같은 인터넷 창) 안에서 실행하는 방식입니다.
화면을 띄운 채로 JavaScript가 1초마다 PLC 또는 서버에 직접 물어봅니다.
직관적이고 구현이 쉬운데, 치명적인 약점이 있습니다. 창을 닫으면 멈춥니다.
서버 사이드 폴링 (Server-side Polling)이란
같은 폴링을 브라우저가 아닌 서버(PC에서 항상 실행 중인 백엔드 프로그램)에서 실행하는 방식입니다.
브라우저는 결과를 보여주는 역할만 하고, 실제 감시는 서버가 합니다.
창을 닫아도 서버는 계속 돌기 때문에 감시가 끊기지 않습니다.
이 두 가지 차이가 이 글 전체 내용입니다.
경비원 이야기부터 시작하겠습니다
건물에 경비원이 있다고 합시다.
임무는 간단합니다. 건물에 이상한 일이 생기면 기록하고, 심각하면 알람을 울리는 것.
그런데 이 경비원이 퇴근하면 어떻게 될까요?
밤새 누가 들어오든, 뭔가 이상한 일이 생기든 기록이 없습니다.
PLCLink 초기 버전의 알람 감시가 정확히 이랬습니다.
경비원이 건물 안(서버)이 아닌 방문객 대기실(브라우저)에 있었어요.
방문객이 퇴근하면(브라우저 탭을 닫으면) 경비원도 같이 사라지는 구조였습니다.
처음 만든 방식 : 브라우저가 직접 PLC를 감시했다

Phase 1.5까지 PLCLink의 알람 감시 구조는 이랬습니다.
브라우저(React 앱)가 직접 PLC에 1초마다 연결해서 값을 읽고, 알람 조건을 판단하고, DB에 기록하는 흐름이었어요.
브라우저가 1초마다 하는 일
1. PLC에 직접 TCP 연결
2. 알람 조건 판단
3. DB에 alarm_history 기록
4. 화면에 뱃지 표시
코드로는 setInterval 또는 useEffect로 주기적으로 API를 호출하는 방식입니다.
React에서 흔하게 쓰는 패턴이라, 처음에는 이게 문제라고 생각을 못 했어요.
그런데 현장에서 문제가 생겼습니다.
"어젯밤 알람이 왜 없어요?"
납품 후 얼마 지나지 않아 연락이 왔습니다.
"어젯밤에 설비가 멈췄던 것 같은데, PLCLink에 알람 이력이 없어요."
확인해보니 원인은 단순했습니다.
야간에 현장 인원이 퇴근하면서 브라우저 탭을 닫았던 겁니다.
그 순간 알람 감시도 같이 꺼졌어요.
설비는 밤새 돌아갔지만 감시자가 없었습니다.
이력은 텅 비어 있었고요.
이게 브라우저 폴링의 근본적인 한계입니다.
브라우저는 사람이 화면을 보고 있을 때만 동작합니다.
탭을 닫으면 그 탭 안에서 실행되던 모든 JavaScript가 멈춥니다.
setInterval이든 useEffect든 어떤 방식으로 만들었든 상관없이.
브라우저는 결국 사람이 보는 도구이기 때문에, 사람이 없으면 동작하지 않는 게 설계 원칙이에요.
경비원을 서버로 옮겼다
해결 방법은 생각보다 명확했습니다.
경비원을 방문객 대기실(브라우저)에서 꺼내서 경비실(서버)로 옮기는 겁니다.
서버는 브라우저와 다릅니다.
사람이 화면을 보든 안 보든, 탭을 닫든 PC를 껐다 켜든 서버 프로세스는 계속 돌아갑니다.
(적어도 PC가 켜져 있는 동안은요.)
Phase 2에서 바꾼 구조입니다.
서버에서 하는 일 (항상, 1초마다)
1. AlarmPoller가 PLC에 TCP 연결
2. 알람 조건 판단
3. DB에 alarm_history 기록 (active 상태 포함)
4. TriggerPoller가 스냅샷 캡처
브라우저에서 하는 일 (탭이 열려 있을 때만)
1. 서버 API 조회 (PLC 직접 연결 안 함)
2. 새 이력 감지 - 팝업, 뱃지 표시
브라우저는 이제 PLC를 직접 바라보지 않습니다.
서버가 이미 읽어놓은 이력을 조회하는 역할만 합니다.
CCTV 카메라 앞에 서는 게 아니라, 이미 녹화된 영상을 재생하는 것처럼요.
FastAPI에서 어떻게 구현했나

Python FastAPI 기준으로 설명하겠습니다.
FastAPI에는 asyncio와 함께 쓸 수 있는 BackgroundTask 개념이 있습니다.
그런데 여기서 말하는 "백그라운드 작업"은 요청이 들어올 때 추가로 실행하는 작업이지, 서버가 시작하자마자 계속 도는 루프가 아닙니다.
PLCLink에서 쓴 방식은 lifespan 이벤트를 활용한 무한 루프 태스크입니다.
# main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
import asyncio
@asynccontextmanager
async def lifespan(app: FastAPI):
# 서버가 시작될 때 AlarmPoller 시작
task = asyncio.create_task(alarm_poller_loop())
yield
# 서버가 종료될 때 태스크 취소
task.cancel()
app = FastAPI(lifespan=lifespan)
# alarm_poller_loop 내부 (간략화)
async def alarm_poller_loop():
while True:
try:
rules = get_alarm_rules() # DB에서 알람 룰 가져오기
for rule in rules:
value = await read_plc(rule) # PLC 값 읽기
if check_condition(rule, value):
insert_alarm_history(rule, value) # 이력 기록
except Exception:
pass # 에러가 나도 루프 유지
await asyncio.sleep(1) # 1초 대기
핵심은 while True 루프와 asyncio.sleep(1)의 조합입니다.
서버가 살아있는 동안 이 루프는 멈추지 않습니다.
브라우저가 연결되어 있든 없든, 아무도 화면을 보고 있지 않든.
한 가지 중요한 포인트가 있습니다.
루프 안에서 예외가 발생해도 루프 자체는 유지되어야 합니다.
PLC와 통신이 끊겼을 때 오류가 발생하는 건 자연스러운 일인데, 그 오류 하나 때문에 루프 전체가 죽으면 안 되니까요.
try-except로 오류를 잡고 다음 루프로 넘어가는 구조가 필수입니다.
브라우저를 새로 열었을 때 팝업이 쏟아지지 않으려면

여기서 한 가지 더 신경 써야 할 게 있습니다.
서버가 밤새 알람을 감지해서 DB에 50개를 기록했다고 합시다.
아침에 출근해서 브라우저를 열면 어떻게 될까요?
브라우저 입장에서는 "50개의 새 알람이 있구나" 하고 50개의 팝업을 한꺼번에 쏟아냅니다.
화면이 팝업으로 도배되는 거예요.
이걸 막으려면 기준선을 잡아야 합니다.
택배 앱을 예로 들면, 오늘 처음 앱을 열었을 때 "오늘 이전" 내역은 알림 없이 목록에만 표시하고, "지금부터 새로 오는" 내역만 알림을 주는 방식이랑 같습니다.
// 브라우저가 처음 열릴 때
const initRef = useRef(false);
useEffect(() => {
if (!initRef.current) {
// 지금 DB의 최대 id를 기준선으로 저장
const maxId = alarmHistory[0]?.id ?? 0;
baselineIdRef.current = maxId;
initRef.current = true;
}
}, []);
// 이후 폴링에서 새 id 감지
useEffect(() => {
const newAlarms = alarmHistory.filter(a => a.id > baselineIdRef.current);
if (newAlarms.length > 0) {
// 팝업 발동
}
}, [alarmHistory]);
처음 열릴 때의 최대 id를 기준선으로 잡아두면, 그 이전 이력은 "이미 알고 있는 것"으로 처리되고 팝업이 뜨지 않습니다.
기준선 이후에 새로 추가되는 이력만 팝업을 발동시킵니다.
아침에 출근해서 어젯밤 알람을 이력 페이지에서 조용히 확인하고, 오늘 낮에 새로 나는 알람은 실시간 팝업으로 받는 구조가 완성됩니다.
정리 : 브라우저 폴링 vs 서버 사이드 폴링

| 항목 | 브라우저 폴링 | 서버 사이드 폴링 |
|---|---|---|
| 구현 난이도 | 낮음 (setInterval, useEffect) | 보통 (asyncio, lifespan) |
| 탭을 닫으면 | 감시 중단 | 계속 감시 |
| 야간 무인 운전 | 불가 | 가능 |
| PLC 연결 주체 | 브라우저 | 서버 |
| 다중 접속 시 PLC 부하 | 탭 수만큼 연결 증가 | 서버 1개만 연결 |
다중 접속 부분도 포인트입니다.
브라우저 폴링 방식에서 탭을 3개 열면 PLC에 연결이 3개가 붙습니다.
PLC 입장에서 통신 부하가 3배가 되는 거예요.
서버 사이드 폴링에서는 서버 하나만 PLC에 연결되고, 브라우저가 몇 개든 관계없습니다.
언제 브라우저 폴링을 써도 괜찮은가
서버 사이드 폴링이 무조건 좋은 건 아닙니다.
단순히 "지금 이 값이 뭔지" 보여주는 대시보드라면 브라우저 폴링으로 충분합니다.
화면을 보고 있을 때만 값을 갱신하면 되고, 이력을 쌓을 필요가 없다면 구현이 더 단순한 브라우저 폴링이 맞습니다.
서버 사이드 폴링이 필요한 상황은 이런 경우입니다.
- 이력을 쌓아야 할 때 : 화면을 보고 있지 않아도 기록이 남아야 함
- 야간 무인 운전 : 아무도 없어도 감시가 계속되어야 함
- 다중 클라이언트 : 여러 PC에서 동시에 보는 환경
- 알람 즉시 감지 : 브라우저가 꺼진 상태에서도 발화시켜야 함
현장 모니터링이라면 대부분 이 조건에 해당합니다.
설비는 24시간 돌고, 사람은 퇴근하니까요.
마치며
처음에 브라우저 폴링으로 만들었을 때 "이게 왜 문제지?" 싶었습니다.
기능은 동작했고, 화면도 잘 바뀌었으니까요.
문제는 현장에 나갔을 때 보였습니다.
아무도 없는 새벽에 설비가 멈췄는데 이력이 없는 상황.
구조를 바꾸는 건 생각보다 어렵지 않았습니다.
폴링 로직을 브라우저에서 서버로 옮기는 작업이라, 코드 자체는 비슷합니다.
달라지는 건 실행 위치와 생명주기뿐입니다.
현장 자동화 소프트웨어를 만들고 있다면, 한 번쯤 이 질문을 해보는 게 좋습니다.
"내가 만든 이 감시 기능, 아무도 화면을 보고 있지 않을 때도 동작하고 있나?"
'(개인Project)_개발 > PLC-PC 연결' 카테고리의 다른 글
| [Program][보족] 설정이 계속 날아가는 이유 (localStorage의 한계와 DB 전환 시점) (0) | 2026.05.22 |
|---|---|
| [Program][보족] 알람이 났는데 왜 배너에는 안 뜨나요? (레벨 알람과 이벤트 알람의 차이) (0) | 2026.05.21 |
| [Program][Phase 2] 브라우저를 꺼도 알람이 쌓인다 (PLCLink Phase 2 완성기) (0) | 2026.05.19 |
| [Program][보족] bat 파일 더블클릭했는데 창이 바로 닫힌다 (줄바꿈 문자 문제일 수 있습니다) (0) | 2026.05.16 |
| [Program][보족] 설정을 마쳤는데 화면은 그대로였다 (커스텀 훅을 두 곳에서 쓰면 생기는 일) (1) | 2026.05.15 |