iBetter Books
수정

실시간 데이터 대시보드

앞 챕터의 대시보드는 사용자가 직접 필터를 조작해야 화면이 바뀌었습니다. 이번 챕터에서는 아무것도 클릭하지 않아도 앱이 스스로 데이터를 읽어 화면을 갱신하는 방법을 배웁니다. Shiny가 제공하는 세 가지 도구, reactivePoll(), reactiveFileReader(), invalidateLater()를 순서대로 살펴봅니다.

왜 자동 갱신이 필요한가

주식 시세, 서버 모니터링, IoT 센서 데이터처럼 외부에서 계속 바뀌는 정보를 다룰 때 사용자가 새로고침을 누르는 방식은 현실적이지 않습니다. Shiny는 반응형 프로그래밍의 타이머 기능으로 이 문제를 깔끔하게 해결합니다.

invalidateLater() — 가장 단순한 타이머

invalidateLater(ms)는 지정한 밀리초 후에 현재 반응식을 강제로 무효화합니다. 무효화되면 Shiny가 다시 실행하므로, 루프처럼 동작합니다.

# 새 파일: realtime-dashboard/app.R
library(shiny)
library(bslib)
library(tidyverse)
library(plotly)

# ── 서버 시작 시 전역 상태 초기화 ───────────────────────────
history_data <- data.frame(
  time  = Sys.time(),
  value = 50
)

# ── UI ─────────────────────────────────────────────────────
ui <- page_fluid(
  theme = bs_theme(bootswatch = "cyborg"),
  h2("실시간 센서 모니터링"),

  layout_columns(
    col_widths = c(4, 8),

    card(
      card_header("현재 상태"),
      value_box(
        title = "현재 온도 (°C)",
        value = textOutput("current_value"),
        theme = "primary"
      ),
      value_box(
        title = "마지막 갱신",
        value = textOutput("last_update"),
        theme = "secondary"
      ),
      sliderInput(
        "interval",
        "갱신 주기 (초)",
        min = 1, max = 10, value = 2
      )
    ),

    card(
      card_header("최근 60초 추이"),
      plotlyOutput("live_chart", height = "400px")
    )
  )
)

# ── Server ─────────────────────────────────────────────────
server <- function(input, output, session) {

  # invalidateLater로 주기적 갱신
  live_data <- reactive({
    invalidateLater(input$interval * 1000, session)

    # 실제 환경에서는 DB나 API를 호출합니다
    new_row <- data.frame(
      time  = Sys.time(),
      value = 50 + rnorm(1, sd = 5)
    )

    # 전역 변수에 누적 (세션 공유 주의 — 단일 사용자 데모용)
    history_data <<- bind_rows(history_data, new_row) |>
      filter(time >= Sys.time() - 60)

    history_data
  })

  output$current_value <- renderText({
    df <- live_data()
    round(tail(df$value, 1), 1)
  })

  output$last_update <- renderText({
    live_data()
    format(Sys.time(), "%H:%M:%S")
  })

  output$live_chart <- renderPlotly({
    df <- live_data()

    p <- df |>
      ggplot(aes(x = time, y = value)) +
      geom_line(color = "#00bc8c", linewidth = 1) +
      geom_point(data = tail(df, 1), color = "white", size = 3) +
      labs(x = NULL, y = "온도 (°C)") +
      theme_minimal() +
      theme(
        plot.background  = element_rect(fill = "#303030", color = NA),
        panel.background = element_rect(fill = "#303030", color = NA),
        text             = element_text(color = "white"),
        axis.text        = element_text(color = "grey80")
      )

    ggplotly(p) |>
      layout(
        paper_bgcolor = "#303030",
        plot_bgcolor  = "#303030"
      )
  })
}

shinyApp(ui, server)

invalidateLater는 세션이 살아있는 동안 계속 동작합니다. input$interval로 갱신 주기를 사용자가 실시간으로 조절할 수 있는 것도 포인트입니다.

reactivePoll() — 데이터베이스 폴링

reactivePoll()은 두 함수를 받습니다. 첫 번째는 가벼운 "변경 감지" 함수, 두 번째는 실제 "데이터 읽기" 함수입니다. 변경이 감지될 때만 두 번째 함수를 실행해 불필요한 연산을 줄입니다.

# 수정: realtime-dashboard/app.R (reactivePoll 예시 — server 함수 내부)

server <- function(input, output, session) {

  # reactivePoll: 5초마다 행 개수를 확인하고, 바뀌면 전체 데이터를 읽음
  db_data <- reactivePoll(
    intervalMillis = 5000,
    session        = session,

    # 가벼운 체크 함수 — 행 개수만 확인
    checkFunc = function() {
      # 실제 환경에서는 DB 쿼리: SELECT COUNT(*) FROM sensor_log
      Sys.time()   # 예시: 항상 바뀌는 값으로 대체
    },

    # 무거운 읽기 함수 — 변화가 있을 때만 실행
    valueFunc = function() {
      # 실제 환경에서는 DB 쿼리 또는 API 호출
      data.frame(
        time  = seq(Sys.time() - 300, Sys.time(), by = 10),
        value = 50 + cumsum(rnorm(31, sd = 1))
      )
    }
  )

  output$live_chart <- renderPlotly({
    df <- db_data()
    # ... 차트 코드
  })
}

reactiveFileReader() — 파일 모니터링

CSV나 로그 파일이 외부 프로세스에 의해 갱신되는 경우에 씁니다. 파일 수정 시각을 주기적으로 확인하고, 바뀌면 다시 읽습니다.

# 수정: realtime-dashboard/app.R (reactiveFileReader 예시)

server <- function(input, output, session) {

  # 2초마다 파일 수정 시간 체크, 바뀌면 read_csv 재실행
  sensor_csv <- reactiveFileReader(
    intervalMillis = 2000,
    session        = session,
    filePath       = "data/sensor_log.csv",
    readFunc       = read.csv
  )

  output$live_chart <- renderPlotly({
    df <- sensor_csv()
    # ... 차트 코드
  })
}

세 가지 도구 비교

도구 언제 쓰나 특징
invalidateLater() 단순 타이머가 필요할 때 매번 실행, 변경 감지 없음
reactivePoll() DB, API 폴링 체크 함수로 불필요한 조회 최소화
reactiveFileReader() 파일 모니터링 파일 수정 시각 기준 감지

실시간 대시보드를 만들 때 자원이 충분하면 invalidateLater로 시작하고, 쿼리 비용이 걱정되면 reactivePoll로 교체하는 전략이 실용적입니다.

주의 사항

전역 변수(<<-)로 데이터를 누적하는 방식은 단일 사용자 데모에서는 동작하지만, 여러 사용자가 접속하면 데이터가 섞입니다. 다중 사용자 환경에서는 reactiveValues를 세션 함수 안에 선언해 사용자별로 상태를 분리합니다.

server <- function(input, output, session) {
  # 세션별 분리된 상태
  rv <- reactiveValues(history = data.frame(time = Sys.time(), value = 50))
  ...
}