
Phase 3가 끝났을 때 남은 한계
Phase 3까지의 PLCLink는 데이터를 "보는" 도구였습니다.
알람이 나면 팝업이 뜨고, DataList에 주소를 등록하면 폴링된 값이 테이블로 표시됐어요.
기능은 있었는데, 현장에서 쓰다 보면 한 가지 불편함이 계속 생겼습니다.
"실린더 7번 전진 완료됐어요?"
DataList 화면에서 여러 행 중 해당 주소 행을 찾아야 합니다.
HMI가 있다면 화면에 배치된 LED 인디케이터 하나를 보면 즉시 알 수 있는 것을, PLCLink에서는 스크롤하며 행을 찾아야 했어요.
Phase 3.5의 목표는 이걸 해결하는 것이었습니다.
자유 배치 HMI 화면.
원하는 위치에 위젯을 놓고, PLC 주소를 연결하고, 실시간으로 값을 확인할 수 있게.
기존 HMI 소프트웨어의 문제

HMI 소프트웨어(VT Studio, GOT2000, WinCC)를 써봤다면 알 겁니다.
설치가 필요하고, 라이선스 비용이 나오고, 특정 PC에 종속됩니다.
화면 하나를 바꾸려면 전용 소프트웨어를 켜야 하고, 수정한 내용을 PLC에 다운로드해야 반영됩니다.
PLCLink 캔버스 HMI는 다릅니다.
브라우저만 있으면 됩니다.
같은 네트워크 안에 있는 어떤 기기에서든 동일한 화면을 볼 수 있어요.
화면 구성을 수정하면 새로고침 한 번으로 모든 접속자에게 즉시 반영됩니다.
설계 원칙 세 가지
기능을 하나씩 만들기 전에 원칙을 먼저 잡았습니다.
첫째, 기존 API를 그대로 쓴다.
PLC 읽기/쓰기 API(/api/io/read, /api/io/write)는 이미 잘 동작하고 있었어요.
캔버스 위젯의 폴링도 이 API를 그대로 사용합니다.
새 엔드포인트를 만들지 않아도 됩니다.
둘째, 현장에서 실제로 필요한 것을 먼저 만든다.
HMI 소프트웨어가 제공하는 수백 가지 기능 중에서 설치 초반이나 디버깅 단계에서 실제로 필요한 건 한정적입니다.
비트 ON/OFF 확인, 숫자값 표시, 출력 제어, 알람 표시 정도면 충분히 유용해요.
셋째, 편집 경험이 사용 경험만큼 중요하다.
HMI 화면을 구성하는 과정이 번거로우면 아무도 쓰지 않습니다.
드래그&드롭, 정렬 도구, 패턴 등록 같은 편의 기능이 없으면 위젯 수십 개를 하나씩 배치해야 해요.
데이터 구조 : 두 테이블로 관리한다
캔버스 데이터는 canvas_pages와 canvas_widgets 두 테이블로 관리합니다.
canvas_pages는 페이지 목록입니다. 이름과 순서를 가집니다.
canvas_widgets는 각 페이지 안의 위젯입니다.
위치(x, y), 크기(w, h), 타입, PLC 주소, 그리고 config라는 JSON 컬럼이 있어요.
config 컬럼이 핵심입니다.
타입별로 다른 설정(색상, 범위, 소수점, 쓰기 여부 등)을 한 컬럼에 JSON으로 담습니다.
새 위젯 타입을 추가해도 DB 스키마 변경이 불필요해요.
z_order는 나중에 레이어 순서가 필요해져서 추가했는데,
이미지를 배경에 깔고 위에 다른 위젯을 올리는 레이어 작업이 가능해졌습니다.
위젯 종류 : 왜 각각을 만들었나

LED 위젯
가장 먼저 만든 위젯입니다. 현장에서 가장 자주 나오는 질문이 "이 신호 지금 살아있어?"이기 때문이에요.
실린더 전진 완료(M100), 센서 감지(X200) 같은 비트 신호를 색상으로 즉시 확인할 수 있습니다.
Y 디바이스(출력 릴레이)에 연결하면 클릭으로 ON/OFF를 전환할 수 있어요. 반드시 확인 모달이 뜨도록 했습니다.
현장에서 실수로 출력을 건드리면 설비가 움직이기 때문이에요.
크기에 따라 표시 방식이 자동으로 바뀝니다.
80px 이상이면 원형 LED + 라벨 + ON/OFF 텍스트, 24~80px이면 작은 점 + 라벨 한 줄, 24px 미만이면 점만 표시합니다.
신호가 많은 화면에서 나노 모드는 상태 표시판처럼 활용할 수 있어요.

숫자 위젯
D 레지스터나 DM 레지스터에 담긴 숫자값을 표시합니다.
속도 설정값, 카운트, 온도 등. 소수점 자리수와 단위를 설정할 수 있어 1234.5 RPM, 23.4 °C 형태로 표시됩니다.
처음에는 읽기 전용이었는데, 중반에 "숫자값을 화면에서 직접 바꿀 수 없나?"라는 요청이 나왔어요.
writable=true 설정을 추가해 클릭하면 입력 모달이 열리도록 했습니다.
Enter를 눌러야 쓰기가 실행됩니다.
실수로 건드리는 걸 막기 위한 의도적인 2단계 확인이에요.
도형 위젯
처음에는 LED와 숫자로 충분할 것 같았어요.
막상 써보니 "도면 위에 신호 상태를 겹쳐 표시하고 싶다"는 필요가 생겼습니다.
SVG로 렌더링합니다.
사각형, 원형, 삼각형, 마름모, 육각형, 직선, 화살표 7종을 지원해요.
모서리 둥글기도 설정할 수 있습니다.
도형은 두 레이어로 구성됩니다.
SVG 레이어(실제 도형)와 컨테이너 박스(감싸는 사각 영역).
이 구조 덕분에 삼각형 도형 위에 사각형 배경을 한 번에 설정할 수 있어요.
이미지 위젯
"도면 캡처본을 배경에 깔고 싶다"는 요구였습니다.
공장 도면이나 설비 사진을 배경으로 올리면 어떤 신호가 어느 위치에 있는지 직관적으로 파악할 수 있어요.
비트 주소를 연결하면 신호 ON/OFF에 따라 이미지가 나타나거나 사라집니다.
"공정 중" 상태를 나타내는 이미지를 특정 비트에 연결하면, 그 비트가 켜질 때만 이미지가 표시됩니다.
버튼 위젯 (페이지 전환)
단일 페이지로는 복잡한 설비 전체를 담기 어렵습니다.
"라인 A 전체 현황" 화면과 "실린더 세부 상태" 화면을 나누고, 버튼으로 이동할 수 있으면 좋겠다는 요구였어요.
운전자 모드에서도 동작합니다.
드래그&드롭 : 세 번 만들었다
이게 Phase 3.5에서 가장 오래 걸린 부분입니다.

1차 시도 : document.addEventListener
직관적인 방법입니다.
마우스다운 시 document에 mousemove/mouseup을 등록하는 방식이에요.
React 18에서 동작하지 않았습니다.
React 18은 이벤트를 document가 아닌 React 루트 컨테이너에 위임합니다.
네이티브 DOM 리스너와 React 합성 이벤트 사이에 타이밍 충돌이 생겼어요.
2차 시도 : setPointerCapture
브라우저 표준 기능입니다.
마우스가 요소 밖으로 나가도 이벤트를 수신할 수 있어요.
역시 React의 합성 이벤트 처리 타이밍과 맞지 않아 작동하지 않았습니다.
최종 해결 : 드래그 오버레이 패턴
드래그 시작 시 화면 전체를 덮는 투명한 div를 생성합니다.
이 오버레이가 마우스 이벤트를 받아요.
{overlay && (
<div
style={{ position: 'fixed', inset: 0, zIndex: 9999, cursor: 'grabbing' }}
onMouseMove={handleOverlayMove}
onMouseUp={handleOverlayUp}
/>
)}
마우스가 어디에 있어도 오버레이가 이벤트를 받기 때문에, 빠르게 드래그해도 위젯을 놓치지 않습니다.
React의 일반 합성 이벤트만 사용해서 타이밍 충돌이 없어요.
드래그가 끝나면 오버레이를 제거합니다.
Figma, Notion 같은 제품에서도 사용하는 패턴입니다.
줌 좌표 보정
캔버스 줌 기능을 추가하면서 드래그 좌표 보정이 필요했습니다.
50% 줌에서 마우스를 100px 드래그하면 캔버스에서는 200px 이동해야 해요.
const dx = (e.clientX - startX) / zoom
const dy = (e.clientY - startY) / zoom
이 보정은 드래그, 리사이즈, 고무줄 선택 세 곳 모두에 적용됩니다.
자동저장과 저장 충돌 방지

자동저장 : 5회 또는 3초
드래그할 때마다 API를 호출하면 서버 부하가 급격히 올라갑니다.
위젯 하나를 2초 동안 드래그하면 10회 이상의 API 호출이 생겨요.
변경 횟수를 카운팅하다가 5회가 쌓이거나 3초 동안 조작이 없을 때 한 번에 저장합니다.
드래그 중간에는 저장 API 호출이 없어요.
저장 충돌 문제
위젯을 드래그해서 위치를 바꾼 다음, 오른쪽 패널에서 라벨을 수정하고 "저장" 버튼을 누르면 어떻게 될까요?
처음 구현에서는 위젯이 드래그 전 위치로 돌아가는 버그가 있었습니다.
자동저장이 아직 DB에 반영되지 않은 상황에서, 패널의 저장 버튼이 PATCH 요청을 보내고 DB 응답을 받습니다.
DB는 드래그 전의 위치를 가지고 있고, 이 값이 로컬 상태를 덮어썼어요.
해결은 PATCH 요청 시 현재 로컬 위치를 반드시 함께 전송하는 것이었습니다.
패널에서 어떤 속성을 바꾸든, 현재 로컬의 위치와 크기가 함께 저장됩니다.
배치 편의 기능 : 하나씩 만들기는 너무 힘들다
위젯을 하나씩 등록하는 것이 불편하다는 피드백이 있었습니다.
실린더 20개의 전진/후진 신호를 보려면 LED 40개를 하나씩 만들어야 했어요.

패턴 등록
시작 주소와 개수, 간격을 입력하면 위젯 N개를 한 번에 배치합니다.
시작: M100, 개수: 8, 간격: 1 → M100~M107 LED 8개 자동 생성
패턴 생성 시 미리보기가 표시됩니다. 어떤 위젯이 만들어질지 확인할 수 있어요.
위젯 복사 (Address Offset)
기존 위젯을 복사할 때 주소 offset을 지정하면 복사본의 주소가 자동으로 달라집니다.
원본: M100 LED → 복사 (offset=10, count=3) → M110, M120, M130
복사 후 생성된 위젯들은 모두 선택된 상태가 되어 바로 드래그해서 위치를 잡을 수 있어요.
주소 Offset 순서 배정
다중 선택한 위젯들을 위→아래, 좌→우 순서로 정렬한 뒤, 기준 주소와 간격을 입력하면 순번대로 주소를 배정합니다.
이미 위치를 다 잡아놓은 상태에서 "이 위젯들 주소가 ZR110100부터 차례대로여야 하는데"를 발견했을 때 특히 유용합니다.
위젯들을 다시 만들 필요 없이 선택 후 시작 주소와 간격만 입력하면 됩니다.
PPT 스타일 정렬 도구
2개 이상의 위젯을 선택하면 툴바에 정렬 드롭다운이 나타납니다.
정렬 6종(좌/우/상/하/가로 중앙/세로 중앙), 간격 균등 2종, 크기 일치 3종을 지원합니다.
멀티 페이지와 페이지 복사

멀티 페이지
하나의 캔버스에 여러 페이지를 만들 수 있습니다.
상단 탭으로 전환하며, 페이지 이름을 더블클릭하면 인라인 편집이 가능해요.
페이지 복사 + Address Offset
같은 레이아웃에 다른 PLC 주소를 사용하는 화면이 필요할 때 유용합니다.
2차전지 제조 라인에서 라인 A, B, C의 설비가 동일한 구조이지만 PLC 주소가 다른 경우가 많아요.
라인 A: ZR110100~ → 페이지 복사 (offset=10000) → 라인 B: ZR120100~
버튼 위젯과 결합하면 "라인 A 보기", "라인 B 보기" 버튼으로 이동하는 네비게이션 HMI를 만들 수 있습니다.
Export / Import
전체 캔버스를 JSON 파일로 내보내고, 다른 사이트에서 가져올 수 있습니다.
동일한 설비가 여러 현장에 배치된 경우, 한 번 만든 화면을 재사용할 수 있어요.
주소 맵 : 위젯이 많아지면 필요해지는 것
위젯이 많아지면 "M100 주소가 어느 페이지의 어느 위젯에 연결되어 있지?"를 찾기 어렵습니다.
주소 맵은 전체 캔버스 페이지의 모든 PLC 주소를 한 화면에서 검색할 수 있는 기능입니다.
디바이스, 주소, 라벨, 페이지명으로 검색할 수 있고, 결과를 클릭하면 해당 페이지로 이동하고 위젯이 선택됩니다.
서버 종료 시 traceback이 쏟아졌다
Phase 3.5 개발 중 Ctrl+C로 서버를 종료할 때 콘솔에 긴 traceback이 출력됐습니다.
어떨 때는 나오고 어떨 때는 안 나와서 혼란스러웠어요.
원인은 두 단계였습니다.
첫 번째는 BaseHTTPMiddleware 문제였습니다.
no-cache 헤더를 위해 추가한 미들웨어가 내부적으로 anyio task group을 사용하는데, 이 안에서 CancelledError가 발생하면 traceback이 출력됐어요.
순수 ASGI 미들웨어로 교체해서 해결했습니다.
두 번째는 PLC TCP 연결 시도 중 종료였습니다.
PLC가 오프라인인 상태에서 연결 시도 중에 Ctrl+C를 누르면 CancelledError가 발생했어요.
"어떨 때는 나오고 어떨 때는 안 나왔던" 이유가 여기 있었습니다.
PLC가 연결돼 있거나 폴링 요청이 없는 상태에서 종료하면 조용하게 끝났던 거예요.
CancelledError는 asyncio 규약상 반드시 위로 전파해야 합니다.
catch해서 다른 예외로 변환하면 서버 종료가 지연되거나 리소스 누수가 생길 수 있어요.
대신 uvicorn 에러 로거에 필터를 달아서 종료 시 발생하는 이 패턴의 로그만 억제했습니다.
Phase 3.5 vs 일반 HMI 소프트웨어 비교

| 기능 | HMI 소프트웨어 | PLCLink 캔버스 |
|---|---|---|
| 비트 램프 (ON/OFF 표시) | 있음 | LED 위젯 |
| 비트 쓰기 (버튼) | 있음 | Y 클릭 + 확인 모달 |
| 워드값 표시 | 있음 | 숫자 위젯 |
| 워드값 쓰기 | 있음 | 숫자 위젯 writable |
| 바 게이지 | 있음 | 게이지 위젯 |
| 도형 그리기 | 있음 | SVG 7종 |
| 이미지/배경 | 있음 | 이미지 위젯 |
| 화면 전환 버튼 | 있음 | 버튼 위젯 |
| 그리드 스냅 | 있음 | 8px |
| 그룹 | 있음 | groupId 방식 |
| 레이어 | 있음 | z_order |
| 알람 표시 | 있음 | AlarmPage (별도) |
아직 미구현인 것들도 있습니다.
원형 게이지, 슬라이더 입력, 멀티 상태 램프 등은 Phase 4에서 추가할 예정입니다.
다음 단계
Phase 4 단기 계획 :
히스토리 재현 기능입니다. 이미 트리거 스냅샷 데이터는 수집되고 있어요.
캔버스 위젯이 이 데이터를 읽어서 과거 특정 시점의 신호 상태를 재생하는 구조입니다.
원형 게이지, 슬라이더 위젯도 추가할 예정입니다.
Phase 5 중기 계획 :
PyInstaller로 단일 EXE 패키징입니다.
현장 PC에 Python/Node.js 설치가 불필요해지고, NSSM으로 Windows 서비스로 등록하면 PC 켜면 자동으로 시작됩니다.
코드는 GitHub에서 확인할 수 있습니다.
github.com/Gweongooing/PLCLink
'(개인Project)_개발 > PLC-PC 연결' 카테고리의 다른 글
| [Program][보족] 자동저장을 만들었는데 다른 저장이 덮어쓴다 : 로컬 상태와 서버 상태를 동기화하는 방법 (0) | 2026.06.02 |
|---|---|
| [Program][보족] 드래그 구현했는데 빠르게 움직이면 위젯을 놓쳐버린다 : 오버레이 패턴으로 해결 (0) | 2026.06.01 |
| [Program][보족] 실제 하드웨어로 테스트하기 전까지 발견 못 한 버그 : SLMP 디바이스 주소 오파싱 (0) | 2026.05.29 |
| [Program][보족] React 프로덕션 빌드에서만 나는 오류의 원인을 찾는 방법 (0) | 2026.05.28 |
| [Program][보족] PDF 매뉴얼을 알람 팝업에 연결하기까지 (3차 시도와 최종 해결) (0) | 2026.05.27 |