본문 바로가기

(개인Project)_개발/PLC-PC 연결

[Program][Phase1.5] PLC 껐다 켜도 데이터 남기는 방법 (SQLite 전환으로 이력 관리 시작)

반응형

PLC 껐다 켜도 데이터 남기는 방법
SQLite 전환으로 이력 관리 시작

PLCLink Phase 1.5 개발기 — 이력이 진짜 쌓이기 시작한 순간

 


이 글이 나한테 해당되는 얘기인지 먼저 확인해보세요

이런 상황이 하나라도 해당된다면, 이 글이 도움이 될 겁니다.

  • 공장 설비에서 알람이 났는데 언제 났는지, 몇 건인지 나중에 확인할 방법이 없다
  • 그 신호가 변할 때 다른 값들이 어떤 상태였는지 직접 옆에서 보지 않으면 알 수 없다
  • HMI(전용 모니터 화면) 없이 노트북이나 핸드폰으로 PLC 상태를 보고 싶다
  • 설정 화면을 공유했더니 누군가 값을 바꿔놓은 적이 있다
  • PLC 데이터를 엑셀 없이 자동으로 수집하고 저장하고 싶다

PLCLink는 PLC와 직접 통신해서 브라우저로 보고, 이력을 자동 저장하고, 신호 변화 순간을 캡처하는 도구입니다.

이 글은 이력 저장과 자동 캡처 기능을 구현한 Phase 1.5 개발 과정을 정리한 기록입니다.

이전 글을 읽지 않으셔도 이 글만으로 내용을 이해할 수 있게 썼습니다.
다만 PLCLink 전체 맥락이 궁금하시다면
Phase 0 글(IO 상태 확인 구현)과 Phase 1 글(데이터 기록과 알람 구현)을
먼저 보시면 더 자연스럽게 이어집니다.


① 무엇이 문제였나 — 껐다 켜면 다 사라졌다

Phase 1까지의 PLCLink는 데이터를 브라우저 로컬스토리지(localStorage) 에 저장했습니다.

로컬스토리지는 웹 브라우저가 자체적으로 가지고 있는 작은 메모리 공간입니다.

웹사이트가 설정이나 데이터를 잠깐 기억해두는 용도로 쓰는데, 이 공간에는 근본적인 한계가 있습니다.

 

문제 1 — 브라우저를 닫으면 이력이 사라집니다.
알람이 10건 났어도 새로고침하면 0건이 됩니다.

PC를 끄면 당연히 다 날아가죠. "어제 알람 몇 건 났어요?"라는 질문에 대답할 수가 없었습니다.

 

문제 2 — 다른 기기에서 접속하면 빈 화면입니다.
로컬스토리지는 그 브라우저, 그 PC에만 저장됩니다.

핸드폰으로 접속하거나 옆 PC에서 열면 아무것도 없는 초기 화면이 뜹니다.

 

문제 3 — 누구든 설정 화면에 들어올 수 있었습니다.
URL 뒤에 ?view=1 같은 파라미터를 붙여서 엔지니어 모드와 확인 모드를 나눴는데, 주소만 알면 누구든 엔지니어 모드로 들어올 수 있었습니다. 값을 잘못 바꾸거나 설정을 건드리는 사고가 생길 수 있는 구조였어요.

▲ 로컬스토리지 기반(좌)과 SQLite 기반(우)의 데이터 흐름 비교. 프로그램을 종료해도 데이터가 유지되는 구조로 전환했습니다.


② 해결책 — 파일 하나가 곧 데이터베이스, SQLite

"데이터베이스를 쓴다"고 하면 복잡한 서버를 설치해야 할 것 같은 느낌이 듭니다.

MySQL이나 PostgreSQL 같은 데이터베이스는 별도 설치, 계정 설정, 서비스 실행까지 신경 써야 할 것들이 많아요.

현장 PC에서 이런 과정을 거치는 건 현실적으로 어렵습니다.

 

그래서 선택한 게 SQLite입니다.

SQLite는 설치가 필요 없습니다.

data/plclink.db라는 파일 하나가 데이터베이스 전체입니다.

Python에서 aiosqlite 라이브러리 하나면 읽고 쓸 수 있고, 백업도 그냥 파일 복사하면 끝입니다.

▲ 로컬스토리지와 SQLite의 차이. SQLite는 파일 하나로 운영되기 때문에 이동, 백업, 복원이 모두 파일 복사 한 번으로 해결됩니다.

 

Phase 1.5에서 만든 테이블은 총 17개입니다.

처음에 보면 많아 보이지만, 구조는 단순합니다.

sites (현장/사이트)
  └── io_points (감시 포인트)
  └── alarm_rules (알람 조건)
       └── alarm_history (알람 이력)
  └── trigger_rules (트리거 조건)
       └── trigger_targets (캡처할 주소 목록)
       └── trigger_snapshots (캡처된 데이터)
  └── write_log (PLC 값 변경 이력)

모든 데이터가 site_id(현장 번호)를 기준으로 연결됩니다.

나중에 여러 PLC를 한 화면에서 관리하는 기능을 추가할 때, 이 구조가 그대로 확장됩니다.

 

개발하면서 깨달은 것 — 스키마는 처음부터 완벽할 수 없다

DB를 만들 때 테이블 구조를 미리 다 정의해도, 개발하다 보면 컬럼을 추가해야 하는 상황이 반드시 생깁니다.

그런데 CREATE TABLE IF NOT EXISTS(테이블이 없으면 만들어라)는 이미 있는 테이블의 구조를 바꾸지 않습니다.

그래서 서버가 시작될 때마다 자동으로 누락된 컬럼을 추가하는 _migrate() 함수를 만들었습니다.

# 서버 시작 시 자동 실행
migrations = [
    ("trigger_rules",    "name",        "TEXT"),
    ("trigger_snapshots","trigger_id",  "INTEGER"),
    ("sites",            "description", "TEXT"),
    # 나중에 추가할 컬럼도 여기에 한 줄 추가하면 됨
]

새 컬럼이 필요할 때마다 목록에 한 줄 추가하면 됩니다.

서버를 재시작하면 자동 적용됩니다.

DB 파일을 지우지 않고도 구조를 계속 개선할 수 있어서,

현장에서 운영 중인 DB를 건드리지 않고 업데이트를 배포할 수 있었습니다.


③ 모드 분리 — 볼 수 있는 사람과 건드릴 수 있는 사람을 나눴다

비밀번호 인증 방식으로 전환

URL 파라미터 방식에서, 헤더의 "엔지니어 모드" 버튼을 누르면 비밀번호를 입력하는 방식으로 바뀌었습니다.

맞으면 엔지니어 세션이 열리고, 탭을 닫으면 자동으로 확인 모드로 돌아갑니다.

핵심은 확인 모드에서는 설정 변경 버튼 자체가 화면에 그려지지 않는다는 점입니다.

개발자 도구를 열어서 소스를 뜯어봐도 없는 버튼을 만들어낼 수 없어요.

볼 수는 있지만 건드릴 수는 없는 구조입니다.

트리거 로거와 시스템 로그는 엔지니어 전용입니다.

확인 모드에서는 메뉴 항목 자체가 사이드바에 보이지 않습니다.

 

비밀번호 변경 기능 추가

Phase 1.5 이전에는 엔지니어 비밀번호가 코드에 '1234'로 고정되어 있었습니다.

이제 사이드바 하단 관리 패널에서 현재 비밀번호를 확인한 후 새 비밀번호로 변경할 수 있습니다.

비밀번호를 잃어버렸다면?
서버 PC에서 브라우저 개발자 도구를 열고 → 애플리케이션 탭 → 로컬스토리지에서
plclink_eng_pw 항목을 삭제하면 기본값 1234로 초기화됩니다.


④ 트리거 로거 — Phase 1.5의 핵심 기능

이 기능이 이번 버전에서 가장 공들인 부분입니다.

왜 필요한가

현장에서 이런 상황이 자주 생깁니다.

"어제 포장 완료 신호가 ON 됐을 때, 그 시점 무게값이랑 온도가 어떻게 나왔는지 알아야 하는데."

 

PLC는 현재 값은 읽을 수 있지만, 과거의 특정 순간 값은 따로 기록하지 않는 이상 날아갑니다.

직접 옆에서 화면을 보고 있지 않으면 알 방법이 없습니다.

트리거 로거는 "특정 신호가 조건을 만족하는 순간, 지정한 주소들의 값을 자동으로 캡처해서 저장하는 기능"입니다.

어떻게 작동하나

① 트리거 조건 등록
   → "Y3921이 ON 되면"

② 캡처 대상 등록
   → 그 순간 D100(무게), D101(온도), Y3920(입력 상태)를 읽어라

③ 백그라운드 자동 감시
   → 앱이 살아있는 동안 100ms마다 신호를 확인

④ 조건 충족 시 즉시 캡처
   → 타임스탬프 + 트리거값 + 지정 주소 값을 DB에 저장

⑤ 화면 조회 또는 CSV 내보내기

 

▲ 신호 등록 → 백그라운드 감시 → 조건 충족 → 자동 캡처 → DB 저장 흐름. 사용자가 화면을 보지 않아도 자동으로 진행됩니다. 지원하는 조건도 신호 종류에 따라 나뉩니다.

 

  • 비트 신호(ON/OFF로 표현되는 신호): ON, OFF, CHANGED(변화 감지)
  • 워드 신호(숫자값을 갖는 신호): >, >=, <, <=, ==(같다), !=(다르다), CHANGED

중복 캡처를 막는 쿨다운

신호가 ON 상태를 5초간 유지하면, 100ms마다 체크하는 구조에서는 조건이 50번 성립합니다.

쿨다운 없이 두면 같은 이벤트에 대해 스냅샷 50개가 생겨버립니다.

쿨다운(cooldown)은 "첫 번째 캡처 후 다시 감지하기까지 최소 대기 시간"입니다.

공정 한 사이클 시간보다 조금 짧게 잡으면, 이벤트 하나당 스냅샷 하나만 남습니다.

신호 ON (첫 감지) → 캡처 ✓ → 쿨다운 2초 시작
2초 동안 신호 지속 → 무시 (쿨다운 중)
2초 후            → 다시 감지 가능

자동 내보내기 — 자리를 비워도 데이터가 쌓인다

트리거 로거에서 직접 "CSV 내보내기" 버튼을 누르는 것 외에, 자동 내보내기 기능도 있습니다.

"500건이 쌓이면 CSV로 자동 저장하고 DB를 초기화"처럼 설정해두면, 몇 시간씩 자리를 비워도 데이터가 자동으로 쌓이고 내보내집니다. 파일 이름에 타임스탬프가 붙어서 나오기 때문에 폴더에 날짜별로 분류됩니다.

CSV 구조도 신경 썼습니다.

D100부터 4개 주소를 캡처하도록 설정하면, CSV에서 D100, D101, D102, D103이 각각 다른 열로 분리됩니다.

엑셀에서 열자마자 차트를 그릴 수 있는 형태로 나옵니다.


⑤ 알람 및 트리거 — 이제 백그라운드에서 항상 감시한다

어느 페이지에 있어도 감시가 멈추지 않는다

Phase 1에서 알람 감시는 알람 페이지에 있을 때만 작동했습니다.

다른 페이지로 이동하면 감시가 멈췄어요.

Phase 1.5에서는 앱의 최상위 레벨에서 1초마다 조건을 체크합니다.

IO 페이지를 보고 있어도, 다른 탭에서 다른 일을 하고 있어도, 알람 조건이 충족되면 브라우저 알림이 오고 DB에 이력이 저장됩니다.

트리거 로거도 마찬가지입니다.

트리거 페이지에 있지 않아도 100ms 간격으로 계속 감시합니다.

CHANGED 조건의 방향까지 기록된다

이전에는 신호가 "변화했다"는 사실만 기록됐습니다.

이제는 어느 방향으로 바뀌었는지가 이력에 남습니다.

2026-04-30 09:35:12  Y3920 CHANGED (OFF→ON)  값: 1
2026-04-30 09:36:55  Y3920 CHANGED (ON→OFF)  값: 0

알람 감시와 트리거 감시를 따로 켜고 끌 수 있다

두 기능은 독립적으로 제어됩니다.

트리거 감시만 잠깐 멈추고 싶을 때 알람 감시를 건드리지 않아도 됩니다.

이 설정은 앱을 재시작해도 유지됩니다.


⑥ 그 밖에 추가된 것들

메인 기능 외에도 실용적인 보완이 여러 개 들어갔습니다.

PLC 쓰기 이력 — 누가 언제 어떤 값을 바꿨는지 남긴다

Data List 화면에서 PLC 주소의 값을 직접 변경할 수 있는데, 이 변경 동작의 이력이 이제 자동으로 기록됩니다.

변경 전 값, 변경 후 값, 성공/실패 여부가 저장되고, 시스템 페이지에서 조회하거나 전체 삭제할 수 있습니다.

설비에서 이상이 생겼을 때 "혹시 누군가 값을 바꿨나?" 확인할 수 있는 근거가 생긴 셈입니다.

현장/라인 이름 표시 — 어느 설비 화면인지 한눈에 구분

공장명과 라인명을 입력하면 헤더와 사이드바에 표시됩니다.

여러 현장을 돌아다니거나 여러 창을 띄워둘 때 "이게 어느 라인 화면이지?" 헷갈리는 일이 없어집니다.

입력한 이름은 DB에도 저장되어 다음 접속 시에도 유지됩니다.

오프라인 폰트 내장 — 인터넷이 없는 현장에서도 화면이 제대로 보인다

이전 버전에서는 폰트를 구글 서버에서 실시간으로 받아왔습니다.

그래서 현장 PC가 인터넷에 연결되어 있지 않으면 폰트가 깨지거나 기본 폰트로 대체됐습니다.

Phase 1.5에서는 폰트 파일을 프로젝트 안에 직접 포함시켰습니다.

현장 설비 PC는 보안 정책상 인터넷이 차단된 경우가 많기 때문에, 이 부분은 실용성에서 꽤 중요한 변경입니다.


⑦ 만들면서 막혔던 것들 — 애로사항 기록

▲ 개발 중 실제로 발생한 문제 3가지. 비슷한 상황을 겪는 분들께 도움이 되길 바라며 기록합니다.

 

애로사항 1 — 저장 버튼을 누르면 무조건 500 에러가 났다

현상: 트리거 룰 저장 버튼을 눌렀더니 서버가 500 에러를 뱉었습니다.

화면에서는 아무 설명도 없이 저장이 실패했어요.

 

원인: 서버 로그를 열어보니 table trigger_rules has no column named name이라고 나왔습니다.

테이블 설계할 때 컬럼 이름을 label로 정의해놓고, 저장 코드에서는 name으로 넣으려 했던 거였습니다.

한 사람이 만들어도 이런 실수가 생깁니다.

 

해결: CREATE TABLE IF NOT EXISTS는 이미 있는 테이블을 수정하지 않아서 서버를 재시작해도 자동으로 고쳐지지 않습니다.

앞서 소개한 _migrate() 시스템에 해당 컬럼을 등록해서 서버 시작 시 자동으로 추가되도록 했습니다.


애로사항 2 — 기능을 추가했더니 화면 전체가 사라졌다

현상: 새 기능을 추가하고 저장했더니 화면 전체가 하얀 빈 화면이 됐습니다.

어디가 문제인지 화면에서는 전혀 알 수 없었어요.

 

원인: 브라우저 개발자 도구 콘솔을 열어보니 React.useState is not defined 오류가 있었습니다.

React에서 기능을 가져다 쓰는 방식이 두 가지인데, 이 파일은 개별 기능을 이름으로 가져오는 방식(import { useState } from 'react')을 쓰고 있었습니다.

그런데 새로 추가한 코드는 React.useState()라는 다른 방식을 썼고, React라는 이름을 찾지 못해서 파일 전체가 실행 불가 상태가 됐습니다.

// ❌ 이 파일에서는 이렇게 쓰면 에러
const [mode, setMode] = React.useState('num')

// ✅ 이렇게 써야 합니다
const [mode, setMode] = useState('num')

 

해결: 파일 내 방식을 통일했습니다. 처음 파일을 만들 때 어떤 방식으로 시작했는지 확인하고 맞춰 쓰는 게 기본입니다.


애로사항 3 — 자동 저장 설정 후 앱이 멈췄다

현상: 자동 내보내기를 설정하고 나서 앱이 버벅이다가 멈추는 현상이 생겼습니다.

새 스냅샷이 들어올 때마다 재현됐어요.

 

원인: 자동 내보내기 함수가 스냅샷 목록을 의존성으로 가지고 있었기 때문입니다.

새 스냅샷이 생기면 → 목록이 바뀌고 → 함수가 재생성되고 → 감시 효과가 다시 실행되고 → 또 함수가 재생성되는 순환이 만들어졌습니다.

이 루프가 초당 수십 번 반복되면서 앱이 버벅이다 멈췄어요.

 

해결: 함수 안에서 스냅샷 목록을 직접 읽는 대신, ref(참조, 값이 바뀌어도 재실행을 유발하지 않는 변수)를 통해 읽도록 분리했습니다.

순환 고리가 끊기면서 문제가 해결됐습니다.


⑧ Phase 1.5로 가능해진 것들

▲ Phase 1.5 완료 후 달라진 것들 요약. 브라우저를 닫아도 이력이 남고, 신호 변화 순간을 자동으로 캡처합니다. 지금 현장에서 이런 것들이 가능해졌습니다.

 

포장 공정에서: 포장 완료 신호가 ON 되는 순간, 그 시점의 무게·온도·라인 번호가 자동으로 기록됩니다.

나중에 불량이 발생했을 때 "그 시점 데이터가 어땠는지" 바로 조회할 수 있습니다.

알람은 화면을 보지 않아도 옵니다.

다른 작업을 하다가도 브라우저 알림으로 받을 수 있고, 이력은 DB에 남습니다.

설정을 바꾸려면 비밀번호가 필요합니다.

확인용 URL을 공유받은 사람은 볼 수는 있지만 건드릴 수 없습니다.

값을 바꾼 기록이 남습니다.

누가 언제 어떤 주소를 어떤 값으로 바꿨는지 시스템 페이지에서 확인할 수 있습니다.

인터넷 없는 현장에서도 화면이 제대로 보입니다.

폰트를 포함해서 UI가 의도한 대로 표시됩니다.


TMI — 더 궁금한 것들

트리거 쿨다운은 어떻게 정하는 게 좋나요?

공정 한 사이클 시간보다 조금 짧게 잡는 게 기준입니다.

포장 라인에서 포장 하나 완료에 3초 걸린다면 쿨다운을 2000~3000ms로 설정하면 됩니다.

이벤트 하나당 스냅샷 하나만 쌓입니다.

 

DB 파일이 너무 커지면 어떻게 되나요?

SQLite는 수십만 건까지는 성능 문제 없이 처리합니다.

트리거 로거의 자동 내보내기를 설정해두면 "N건 쌓이면 CSV로 저장 → DB 초기화 → 재수집" 사이클로 운영할 수 있어서 파일이 무한정 커지지 않습니다.

 

엔지니어 비밀번호를 잃어버리면 어떻게 하나요?

기본값은 1234입니다. 서버 PC에서 브라우저 개발자 도구 → 애플리케이션 탭 → 로컬스토리지에서 plclink_eng_pw 항목을 삭제하면 기본값으로 초기화됩니다.

 

다음 Phase 2에서는 뭐가 추가되나요?

멀티 사이트 전환(여러 PLC를 한 화면에서 관리), Data List 시계열 수집, OPC-UA 프로토콜 지원을 계획 중입니다.

지금의 DB 구조가 멀티 사이트를 염두에 두고 설계되어 있어서, 추가 자체는 크게 어렵지 않을 것 같습니다.


마무리

Phase 1.5는 "기능을 늘렸다"기보다는 "실제로 쓸 수 있는 도구가 됐다" 는 느낌에 더 가깝습니다.

이력이 남고, 감시가 끊기지 않고, 설정이 보호되고, 현장 환경에서도 제대로 작동하는 것 — 화면에 보이는 기능 수보다 이런 기반이 더 중요하다는 걸 만들면서 다시 확인했습니다.

다음 글에서는 Phase 2 구현 과정을 정리해 올 예정입니다.

반응형