iBetter Books
수정

reactiveVal과 reactiveValues

reactive()는 입력값에서 계산을 통해 도출되는 값을 정의합니다. 그런데 때로는 사용자 인터랙션에 따라 직접 값을 바꾸고 싶을 때가 있습니다. 버튼을 누를 때마다 카운터를 올리거나, 체크박스를 클릭할 때 목록에 항목을 추가하거나, 사용자가 입력한 내용을 누적해서 저장하는 경우입니다. 이때 reactiveVal()reactiveValues()를 사용합니다.

reactiveVal() — 단일 반응형 값

reactiveVal()은 반응형 단일 값을 만듭니다. 마치 반응형 변수처럼 동작합니다.

# 초기값 0으로 반응형 값 생성
counter <- reactiveVal(0)

# 값 읽기 — 함수처럼 호출합니다
counter()       # 0

# 값 쓰기 — 새 값을 인자로 전달합니다
counter(10)
counter()       # 10

# 현재 값 기반으로 업데이트
counter(counter() + 1)
counter()       # 11

읽기와 쓰기가 같은 함수 이름 counter()로 이루어집니다. 인자 없이 호출하면 읽기, 인자를 넣으면 쓰기입니다.

카운터 앱 예제

버튼을 클릭할 때마다 숫자가 올라가는 카운터 앱입니다.

library(shiny)

ui <- fluidPage(
  titlePanel("카운터"),
  actionButton("increase", "+ 1 추가"),
  actionButton("decrease", "- 1 감소"),
  actionButton("reset",    "초기화"),
  hr(),
  h2(textOutput("count_display"))
)

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

  # 초기값 0으로 reactiveVal 생성
  count <- reactiveVal(0)

  # + 버튼: 현재 값에 1을 더합니다
  observeEvent(input$increase, {
    count(count() + 1)
  })

  # - 버튼: 현재 값에서 1을 뺍니다
  observeEvent(input$decrease, {
    count(count() - 1)
  })

  # 초기화 버튼: 0으로 되돌립니다
  observeEvent(input$reset, {
    count(0)
  })

  output$count_display <- renderText({
    count()
  })

}

shinyApp(ui, server)

countreactive()와 달리 입력값에서 계산되는 것이 아니라, observeEvent 안에서 직접 값을 설정합니다. 이것이 핵심 차이입니다.

reactiveValues() — 여러 반응형 값

여러 개의 반응형 값이 필요하다면 reactiveValues()를 사용합니다. 리스트처럼 동작하지만, 내부 값이 바뀌면 그것에 의존하는 반응식들이 자동으로 재실행됩니다.

# 초기값과 함께 생성합니다
state <- reactiveValues(
  count = 0,
  history = character(0),
  last_updated = Sys.time()
)

# 값 읽기 — $로 접근합니다
state$count
state$history

# 값 쓰기 — $로 접근해서 대입합니다
state$count <- state$count + 1
state$history <- c(state$history, "새 항목")

reactiveValues()$로 읽고 씁니다. reactiveVal()과 달리 함수 호출 형태가 아닙니다.

reactiveValues() 활용 예제 — 할 일 목록

할 일을 추가하고 삭제하는 앱입니다. 여러 상태(할 일 목록, 필터 상태)를 reactiveValues()로 관리합니다.

library(shiny)

ui <- fluidPage(
  titlePanel("할 일 목록"),
  sidebarLayout(
    sidebarPanel(
      textInput("new_item", "새 할 일"),
      actionButton("add_btn", "추가"),
      hr(),
      checkboxInput("show_done", "완료된 항목 보기", value = TRUE)
    ),
    mainPanel(
      tableOutput("todo_table"),
      textOutput("summary")
    )
  )
)

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

  # 앱 상태를 reactiveValues로 관리합니다
  state <- reactiveValues(
    items = data.frame(
      task = character(0),
      done = logical(0),
      stringsAsFactors = FALSE
    )
  )

  # 추가 버튼 클릭 시
  observeEvent(input$add_btn, {
    req(input$new_item != "")  # 빈 값이면 무시합니다

    # 새 행을 추가합니다
    new_row <- data.frame(
      task = input$new_item,
      done = FALSE,
      stringsAsFactors = FALSE
    )
    state$items <- rbind(state$items, new_row)

    # 입력 필드를 초기화합니다
    updateTextInput(session, "new_item", value = "")
  })

  # 필터 적용
  filtered_items <- reactive({
    if (input$show_done) {
      state$items
    } else {
      state$items[!state$items$done, ]
    }
  })

  output$todo_table <- renderTable({
    filtered_items()
  })

  output$summary <- renderText({
    total <- nrow(state$items)
    done  <- sum(state$items$done)
    paste0("전체 ", total, "개 / 완료 ", done, "개")
  })

}

shinyApp(ui, server)

reactiveVal() vs reactiveValues() — 비교

reactiveVal() reactiveValues()
용도 단일 값 여러 값 묶음
읽기 val() rv$key
쓰기 val(새값) rv$key <- 새값
초기화 reactiveVal(초기값) reactiveValues(key = 초기값)

단순한 카운터나 토글처럼 하나의 값만 관리한다면 reactiveVal(), 여러 상태를 함께 관리해야 한다면 reactiveValues()를 사용합니다.

상태 관리 패턴

규모가 큰 앱에서는 모든 상태를 하나의 reactiveValues()로 모으는 패턴이 코드를 관리하기 쉽게 해줍니다.

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

  # 앱의 모든 상태를 한 곳에 모읍니다
  app_state <- reactiveValues(
    selected_dept    = "통계",
    filter_threshold = 70,
    comparison_mode  = FALSE,
    selected_rows    = integer(0)
  )

  # 상태 변경은 observeEvent에서 처리합니다
  observeEvent(input$dept, {
    app_state$selected_dept <- input$dept
  })

  observeEvent(input$compare_btn, {
    app_state$comparison_mode <- !app_state$comparison_mode
  })

  # 출력에서 상태를 읽습니다
  output$mode_text <- renderText({
    if (app_state$comparison_mode) "비교 모드" else "기본 모드"
  })

}

이 패턴을 사용하면 앱의 현재 상태가 어떤지 한눈에 파악할 수 있고, 상태를 변경하는 코드와 상태를 읽는 코드가 명확하게 분리됩니다.

주의사항 — reactive() vs reactiveVal()

reactive()reactiveVal()은 이름이 비슷하지만 완전히 다른 도구입니다.

# reactive: 입력에서 계산을 통해 도출됩니다. 직접 값을 설정할 수 없습니다.
filtered <- reactive({
  students |> filter(dept == input$dept)
})

# reactiveVal: 직접 값을 설정합니다. 계산 로직이 없습니다.
my_val <- reactiveVal(0)
observeEvent(input$btn, {
  my_val(my_val() + 1)  # 직접 값을 변경합니다
})

언제 무엇을 쓸지 헷갈린다면 이 질문을 해보세요.

"이 값이 입력값에서 계산될 수 있는가?" — 그렇다면 reactive() "이 값을 직접 설정하거나 누적해야 하는가?" — 그렇다면 reactiveVal() 또는 reactiveValues()