iBetter Books
수정

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가 바뀌면 renderTablerenderPlot이 각자 독립적으로 필터링을 수행합니다. 같은 계산을 두 번 하는 셈입니다.

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()는 한 번만 재계산됩니다. renderTablerenderPlot이 둘 다 filtered_data()를 사용해도 실제 계산은 한 번만 일어납니다. 두 번째 호출에서는 캐시된 결과를 돌려줍니다.

reactive()의 의존성 추적

filtered_data <- reactive({
  students |>
    filter(dept == input$dept, score >= input$min_score)
})

이 반응식은 input$deptinput$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$deptinput$min_score가 바뀔 때 한 번만 일어납니다.