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)
count는 reactive()와 달리 입력값에서 계산되는 것이 아니라, 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()