reactive()와 observe()
render*() 함수만으로도 Shiny 앱을 만들 수 있지만, 코드가 복잡해지면 한계가 생깁니다. reactive()와 observe()는 Shiny 반응형 시스템을 더 정교하게 활용할 수 있게 해주는 두 가지 핵심 도구입니다.
왜 reactive()가 필요한가
앞 챕터의 예제를 다시 살펴봅시다.
server <- function(input, output, session) {
output$result_table <- renderTable({
students |>
filter(dept == input$dept, score >= input$min_score) |>
arrange(desc(score))
})
output$score_plot <- renderPlot({
students |>
filter(dept == input$dept, score >= input$min_score) |> # 중복!
arrange(desc(score)) |> # 중복!
ggplot(aes(x = name, y = score)) +
geom_col(fill = "steelblue")
})
}
데이터 필터링 코드가 두 곳에 중복됩니다. 만약 필터 조건이 바뀌면 두 곳을 모두 수정해야 합니다. 나중에 세 번째, 네 번째 출력이 추가된다면 더 심각해집니다.
더 큰 문제는 성능입니다. input$dept가 바뀌면 renderTable과 renderPlot이 각자 독립적으로 필터링을 수행합니다. 같은 계산을 두 번 하는 셈입니다.
reactive() — 값을 반환하는 반응식
reactive()는 중간 계산 결과를 정의하고 캐시합니다. 엑셀의 중간 계산 셀(=A1*B1)과 같습니다.
server <- function(input, output, session) {
# 필터링 결과를 reactive로 정의합니다
filtered_data <- reactive({
students |>
filter(dept == input$dept, score >= input$min_score) |>
arrange(desc(score))
})
# filtered_data()로 호출해서 결과를 사용합니다
output$result_table <- renderTable({
filtered_data()
})
output$score_plot <- renderPlot({
filtered_data() |>
ggplot(aes(x = name, y = score)) +
geom_col(fill = "steelblue")
})
}
코드가 훨씬 깔끔해졌습니다. reactive()의 두 가지 핵심 특징을 기억하세요.
특징 1. 값을 반환합니다.
reactive()는 결과 값을 반환합니다. 사용할 때 filtered_data()처럼 함수 호출 형태로 사용합니다. 괄호를 빠뜨리면 함수 객체 자체를 반환하므로 오류가 발생합니다.
filtered_data # 잘못된 방법 — 반응식 객체를 반환합니다
filtered_data() # 올바른 방법 — 계산된 값을 반환합니다
특징 2. 캐시(Cache)합니다.
input$dept가 바뀌면 filtered_data()는 한 번만 재계산됩니다. renderTable과 renderPlot이 둘 다 filtered_data()를 사용해도 실제 계산은 한 번만 일어납니다. 두 번째 호출에서는 캐시된 결과를 돌려줍니다.
reactive()의 의존성 추적
filtered_data <- reactive({
students |>
filter(dept == input$dept, score >= input$min_score)
})
이 반응식은 input$dept와 input$min_score 두 가지에 의존합니다. 둘 중 하나라도 바뀌면 다시 계산됩니다.
input$dept ─────────┐
├──→ filtered_data() ──→ output$result_table
input$min_score ────┘ └──→ output$score_plot
observe() — 부수효과를 실행하는 관찰자
observe()는 reactive()와 달리 값을 반환하지 않습니다. 대신, 반응형 값이 바뀔 때 "어떤 일을 한다"는 부수효과(Side Effect)를 정의합니다.
콘솔에 로그를 출력하거나, 파일을 저장하거나, 알림을 보내거나, 다른 위젯을 업데이트할 때 사용합니다.
server <- function(input, output, session) {
# input$dept가 바뀔 때마다 콘솔에 기록합니다
observe({
cat("[로그] 학과 변경:", input$dept, "\n")
})
}
observeEvent() — 특정 이벤트에만 반응
observe()가 내부에서 읽은 모든 반응형 값에 반응하는 반면, observeEvent()는 지정한 이벤트에만 반응합니다.
server <- function(input, output, session) {
# 버튼이 클릭될 때만 실행됩니다
observeEvent(input$submit_btn, {
cat("Submit 버튼 클릭됨\n")
cat("선택 학과:", input$dept, "\n")
})
}
첫 번째 인자에 지정한 반응형 값이 변경될 때만 두 번째 인자의 코드 블록이 실행됩니다. input$dept가 바뀌어도 버튼을 클릭하지 않으면 아무것도 실행되지 않습니다.
reactive() vs observe() — 언제 무엇을 쓸까
reactive() |
observe() / observeEvent() |
|
|---|---|---|
| 값 반환 | 반환한다 | 반환하지 않는다 |
| 실행 시점 | 누군가 값을 요청할 때 (지연) | 의존성이 바뀌자마자 (즉시) |
| 캐시 | 있음 | 없음 |
| 용도 | 중간 계산, 데이터 변환 | 부수효과 (로그, 알림, UI 업데이트) |
reactive()를 쓸 때
- 계산 결과를 여러 출력에서 공유할 때
- 오래 걸리는 계산을 캐시하고 싶을 때
render*()함수에 결과를 제공할 때
observe()를 쓸 때
- 콘솔에 로그를 남길 때
- 버튼 클릭에 반응해서 무언가를 실행할 때
- 다른 위젯의 값이나 선택지를 업데이트할 때 (
updateSelectInput()등) - 데이터베이스에 저장하거나 파일을 내려받을 때
실전 예제 — 두 가지를 함께 사용하기
학과를 선택하면 데이터를 필터링하고(reactive), 버튼을 클릭하면 콘솔에 현재 상태를 기록하는(observe) 앱입니다.
library(shiny)
library(tidyverse)
students <- tibble(
name = c("Alice", "Bob", "Charlie", "Diana", "Eve"),
dept = c("통계", "수학", "통계", "컴퓨터", "수학"),
score = c(85, 92, 78, 95, 88)
)
ui <- fluidPage(
titlePanel("성적 조회"),
sidebarLayout(
sidebarPanel(
selectInput("dept", "학과",
choices = c("통계", "수학", "컴퓨터")),
sliderInput("min_score", "최소 점수",
min = 0, max = 100, value = 70),
actionButton("log_btn", "현재 상태 기록")
),
mainPanel(
tableOutput("table"),
textOutput("count_text")
)
)
)
server <- function(input, output, session) {
# reactive: 필터링 결과를 캐시합니다
filtered <- reactive({
students |>
filter(dept == input$dept, score >= input$min_score)
})
output$table <- renderTable({
filtered()
})
output$count_text <- renderText({
paste(nrow(filtered()), "명이 조건을 만족합니다.")
})
# observeEvent: 버튼 클릭 시에만 실행됩니다
observeEvent(input$log_btn, {
cat("=== 현재 상태 ===\n")
cat("학과:", input$dept, "\n")
cat("최소 점수:", input$min_score, "\n")
cat("결과 수:", nrow(filtered()), "\n")
})
}
shinyApp(ui, server)
filtered()를 renderTable, renderText, observeEvent 세 곳에서 사용하지만, 계산은 input$dept나 input$min_score가 바뀔 때 한 번만 일어납니다.