[AIoT] 게이트웨이가 뭐길래? Level vs Edge Triggered 알람 이슈

2026. 4. 21. 10:19 · 개발자로 살아남기/트러블슈팅

들어가며

AIoT 플랫폼 개발사에서 프론트를 담당하고 있다. 프론트 개발자라고 해서 React/Vue만 알면 될 것 같지만, 산업 IoT 도메인에서는 센서가 어떻게 서버로 데이터를 쏘는지? 까지 이해해야 하는 순간이 온다...

프론트 개발자로 일하면서 "게이트웨이"라는 단어를 제대로 이해해야 할 날이 올 줄 몰랐다. 알람 카운트 버그 하나를 쫓다가, 결국 센서 → 게이트웨이 → 서버 → 프론트로 이어지는 데이터 흐름 전체를 머릿속에 그려야 했다. 오늘은 그 과정에서 배운 Level-triggered vs Edge-triggered 개념을 정리해본다.

 

 

 

상황 — 미확인 알람이 34,560건, 일주일 간 누적 합 69180건

IoT 모니터링 서비스에서 글로벌 알람 뱃지에 34,560건이라는 비정상적인 숫자가 찍혔다. 알람관리 페이지를 열어보니 더 이상했다. 성격이 전혀 다른 8개 알람(모터 누수, 배터리 이상, CAS 비정상 등)이 전부 똑같이 8,640건씩 쌓여 있었다.

한 기기의 센서가 고장났다고 해도 이상한 숫자다. 서로 다른 알람이 "같은 숫자로 수렴"한다는 게 핵심 단서였다.

 

 

함정 1 — 게이트웨이는 "이벤트"를 보내지 않는다

하드웨어 연결을 맡고 계신 차장님께 문의하니 이런 답이 돌아왔다.

"게이트웨이는 현재 알람 상태값(level)을 주기적으로 송신합니다. 별도의 알람 이벤트 발생 메시지나 OFF→ON 카운트 이벤트는 보내지 않습니다."

 

여기서 처음으로 Level-triggered라는 단어를 배웠다.

  • Level-triggered: "지금 상태가 이래요"를 주기적으로 쏴줌. 조건 충족 여부와 관계없이 현재 값만 보냄.
  • Edge-triggered: "조건이 충족된 바로 그 순간에 한 번"만 이벤트를 쏨.

게이트웨이가 level-triggered라는 건, 센서 값이 경계치를 넘었든 말든 1분마다 "지금 상태"를 계속 보낸다는 뜻이다. "알람이 발생했어요!"가 아니라 "알람 플래그가 켜져 있어요 · 계속 켜져 있어요 · 계속…"을 반복하는 것.

 

 

 

함정 2 — 성격이 다른 8개 알람이 같은 숫자였던 이유

게이트웨이가 level-triggered라면, 조건 전이(false→true, true→false)를 감지해서 새 이벤트를 생성할지 말지 결정하는 건 누구의 책임인가?

정답은 서버다. 서버가 동일 rule/source에 대해 이전 상태를 기억하면서 관리해야 한다.

이전 상태 현재 수신값 서버가 해야 할 일
false false 아무것도 안 함
false true INSERT (새 알람 생성) 
true true 아무것도 안 함 (또는 lastSeen만 업데이트)
true false UPDATE (alarm 해제 처리) 

 

그런데 우리 서버는 이 "상태 기억" 로직이 빠져 있었다. level-triggered로 들어오는 true 신호를 받을 때마다 무지성 INSERT를 하고 있었던 것이다. 1분 주기 × 24시간 × 24일 = 34,560. 서로 다른 알람들이 각자의 조건이 활성화된 시점부터 꾸준히 폴링마다 INSERT되어서, 결국 비슷한 숫자로 수렴한 것이다.

 

왜 update가 아닌 insert인지, 사실 당연한 것 같기도 했다. 알람이 10초에 한번씩 받아오지만 알람은 당연히 계속 기기에 대한 정보를 내려야하기 때문에 계속 받아와야 하는 것 아닌가? 했는데, 우리는 여기서 규칙으로 알람을 정의했다.

 

'조건 충족 시 한 번만 호출' < 이 규칙 정의가 작동하고 있지 않다는 걸 여기서 눈치챌 수 있었다.

일단 심증은 있었으나 물증이 없었고, 이를 정의하지 않으면 BE와 HW쪽에 전달할 수 없다.

 

 

 

함정 3 — 프론트는 이 흐름에서 어디쯤 있는가

버그를 쫓다가 "혹시 프론트가 숫자를 누적하고 있나?" 의심도 했다. 코드를 다시 봤다.

const updateAlarmCount = (count) => {
  countUnknownAlarms.value = count;  // 덮어쓰기, 누적 아님
};

프론트는 서버가 준 count를 그대로 표시만 하고 있었다. += 연산자로 카운트를 건드리는 코드는 0건. 

즉 이 문제에서 각 레이어의 책임은 이렇게 나뉜다.

  • 게이트웨이: 현재 상태값을 주기적으로 송신 (이게 원래 역할)
  • 서버: 이전 상태를 기억하고 전이(transition)를 감지해서 이벤트 저장
  • 프론트: 서버가 집계한 결과를 그대로 보여주기만

이 책임 분리가 무너지면, 다른 레이어가 뒤집어쓰기 딱 좋은 구조가 된다.

 

 

 

해결 방향 — 어디를 고쳐야 하는가

진단이 확실해졌으니 공은 서버로 넘어갔다. 확인할 포인트는 세 가지였다.

이것 또한 차장님께서 작성해주신 체크리스트다.

  1. 기존 active 알람이 있을 때 INSERT가 아닌 유지/UPDATE 되는지
  2. false→true 전이에서만 신규 알람이 생성되는지
  3. OR 조건 알람의 clear 기준이 "전체 false"인지

서버에서 이 로직을 제대로 구현하면, level-triggered로 들어오는 상태값이 edge-triggered 이벤트로 정확히 한 번만 DB에 기록된다.

 

그리고 결국 버그의 실체는 거창한 설계 결함이 아니라 서버의 depth 한 계층 어긋남이었다. DB 설계도 정상이었고, 프론트 로직도 정상이었는데, 서버가 저장 경로와 다른 경로에서 값을 읽고 있었다,,는 단순한 이유로 전체 시스템이 level-triggered처럼 오작동하고 있었다. 저장 구조와 읽기 구조가 한 뼘이라도 어긋나면 defaults로 미끄러지면서 완전히 다른 동작 모델이 되어버린다는 것. 

 

 

 

체크리스트 — "카운트가 이상하다" 싶을 때

✓ 게이트웨이(또는 데이터 소스)가 이벤트를 쏘는지, 상태값을 쏘는지 확인

✓ level-triggered라면 상태 전이 관리 책임이 서버에 있는지 확인

✓ 서로 다른 알람이 같은 숫자로 수렴한다면 polling hit 기준 집계를 의심

✓ 프론트에서 += 같은 누적 연산이 있는지 먼저 점검 (대부분 없음)

✓ API 응답 구조에 count, activeCount, unreleasedCount 같은 여러 카운트 필드가 있는지 살펴보기 (간혹 서버가 다른 필드에 정답을 숨겨놓음)

 

 

 

교훈

프론트 개발자로 살면서 하드웨어 게이트웨이의 송신 방식까지 파헤칠 날이 올 줄 몰랐다. 사실 컴퓨터 공학 전공하면서 이런 게 있다고만 배웠지 그걸 내가 실제로 연결하고 데이터를 받아보는 날이 올 줄 알았겠냐.. ㅠ?? 그런데 그걸 모르면 버그의 진짜 원인을 설명할 수 없었다. IoT, 실시간 대시보드, 알림 시스템 같은 도메인에서는 Level-triggered vs Edge-triggered 구분이 생각보다 자주 나온다.

센서가 뭘 쏘는지 → 서버가 어떻게 저장하는지 → 프론트가 뭘 보여주는지, 이 흐름 전체를 머릿속에 그릴 수 있으면 "공을 어디로 넘겨야 하는지"가 명확해진다. 프론트의 영역 바깥이라고 외면하면, 본인이 엉뚱한 버그를 떠안게 되는 순간이 온다. (진짜 무서웠슨)

저작자표시 비영리 변경금지 (새창열림)

'개발자로 살아남기 > 트러블슈팅' 카테고리의 다른 글

[Git] 브랜치 간 변동 사항을 옮길 때 주의할 점  (0) 2026.04.06
[CSS] ag-grid autoHeight vs normal 모드에서 가로 스크롤 제어하기  (0) 2026.04.06
[JS] Cannot read properties of null (reading 'shapeFlag') - setTimeout과 디바운스  (0) 2026.03.25
[JS] Promise.reject()와 setTimeout  (0) 2026.03.25
'개발자로 살아남기/트러블슈팅' 카테고리의 다른 글
  • [Git] 브랜치 간 변동 사항을 옮길 때 주의할 점
  • [CSS] ag-grid autoHeight vs normal 모드에서 가로 스크롤 제어하기
  • [JS] Cannot read properties of null (reading 'shapeFlag') - setTimeout과 디바운스
  • [JS] Promise.reject()와 setTimeout
ming0o
ming0o
Jr. 프론트엔드 개발자의 성장(개발)일지입니다. 이전 : https://velog.io/@ming0o
  • ming0o
    주토피아
    ming0o
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Goals
        • 2026 목표 및 월간 회고
      • 개발자로 살아남기
        • 트러블슈팅
        • 코드 리뷰
        • 기술 정리
      • Dev
        • Vue
        • Node & Express
        • React Native
      • Challenge
        • KDT-핀테크 인턴십
        • 한이음 드림업
      • Learning
        • JavaScript
        • Algorithm
        • CS
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Router
    트러블슈팅
    replace
    타입에러
    이기고만다
    수동배포
    지피티
    Push
    Vue
    try-catch문
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
ming0o
[AIoT] 게이트웨이가 뭐길래? Level vs Edge Triggered 알람 이슈
상단으로

티스토리툴바