iBetter Books
수정

반응형 프로그래밍 이해하기

Shiny에서 가장 이해하기 어렵지만, 가장 중요한 개념이 "반응형(Reactive)"입니다. 이 개념을 제대로 이해하면 Shiny의 모든 것이 자연스럽게 연결됩니다.

반응형이란 무엇인가

일반 R 코드는 작성한 순서대로 한 번 실행되고 끝납니다.

x <- 5
y <- x * 2
cat(y)  # 10

x <- 10  # x를 바꿔도 y는 여전히 10입니다
cat(y)   # 10 — 변하지 않습니다

x를 10으로 바꿔도 y는 자동으로 업데이트되지 않습니다. 이미 실행이 끝났으니까요.

반응형 프로그래밍은 다릅니다. 값들 사이의 "관계"를 선언하면, 그 관계가 유지됩니다.

엑셀을 생각해봅시다. A1 셀에 5, B1 셀에 =A1*2를 입력하면 B1은 10입니다. 이제 A1을 10으로 바꾸면 B1은 자동으로 20이 됩니다. B1 셀의 공식 =A1*2는 "A1의 두 배를 계산해서 보여줘"라는 관계를 정의하고 있고, A1이 바뀌면 B1도 자동으로 다시 계산됩니다.

Shiny의 반응형이 정확히 이 방식으로 동작합니다.

output$result <- renderText({
  paste("선택한 값:", input$num)  # input$num과의 "관계"를 선언
})

이 코드는 "outputresult는항상inputresult는 항상 inputnum에 'selected: '를 붙인 문자열이다"라는 관계를 선언합니다. input$num이 바뀌면 Shiny가 자동으로 output$result를 다시 계산합니다.

의존성 그래프

Shiny는 내부적으로 각 반응식이 어떤 입력에 의존하는지 추적합니다. 이것을 의존성 그래프(Dependency Graph)라고 합니다.

앞 챕터의 예제를 다시 봅시다.

output$result_table <- renderTable({
  students |>
    filter(dept == input$dept, score >= input$min_score)
})

output$score_plot <- renderPlot({
  students |>
    filter(dept == input$dept, score >= input$min_score) |>
    ggplot(aes(x = name, y = score)) +
    geom_col()
})

이 코드에서 의존성 관계는 다음과 같습니다.

input$dept ────────┬──→ output$result_table
                   └──→ output$score_plot

input$min_score ───┬──→ output$result_table
                   └──→ output$score_plot

input$dept가 바뀌면 Shiny는 그것에 의존하는 output$result_tableoutput$score_plot을 모두 다시 계산합니다. input$min_score가 바뀌어도 마찬가지입니다.

지연 평가 (Lazy Evaluation)

Shiny의 반응식은 "필요할 때만" 실행됩니다. 이를 지연 평가라고 합니다.

output$expensive_plot <- renderPlot({
  # 이 코드는 앱 시작 즉시 실행되지 않습니다
  # 사용자가 해당 탭을 열어서 이 출력이 화면에 표시될 때 처음 실행됩니다
  Sys.sleep(3)  # 오래 걸리는 계산
  plot(rnorm(1000))
})

사용자가 그 탭을 한 번도 열지 않으면 이 코드는 실행되지 않습니다. 탭을 처음 열 때 실행되고, 이후 의존하는 입력이 바뀔 때만 다시 실행됩니다.

반응형의 세 가지 구성 요소

Shiny의 반응형 시스템은 세 가지 역할로 나뉩니다.

1. 반응형 소스 (Reactive Source)

값을 제공하고, 변경 사실을 알립니다. input이 대표적인 반응형 소스입니다. 사용자가 위젯을 조작하면 값이 바뀌고, 그것에 의존하는 반응식들에게 "나 바뀌었어"라고 알립니다.

input$dept       # 반응형 소스
input$min_score  # 반응형 소스

2. 반응형 컨슈머 (Reactive Consumer)

반응형 소스나 다른 반응식을 구독하고, 변경이 있으면 다시 실행됩니다. render*() 함수들이 반응형 컨슈머입니다.

output$result_table <- renderTable({ ... })  # 반응형 컨슈머
output$score_plot   <- renderPlot({ ... })   # 반응형 컨슈머

3. 반응형 표현식 (Reactive Expression)

소스와 컨슈머의 중간 역할입니다. 값을 계산하고 캐시해두며, 다른 컨슈머가 사용할 수 있습니다. reactive()로 만듭니다. 다음 챕터에서 자세히 다룹니다.

filtered_data <- reactive({ ... })  # 반응형 표현식

반응형의 실행 규칙

Shiny의 반응형 실행은 아래 두 가지 규칙을 따릅니다.

규칙 1. 반응형 컨텍스트 안에서만 반응형 값을 읽을 수 있습니다.

# 잘못된 방법 — server 함수 안이지만 반응형 컨텍스트가 아닙니다
server <- function(input, output, session) {
  current_dept <- input$dept  # 오류 발생!
}

# 올바른 방법 — render*() 또는 reactive() 안에서 읽습니다
server <- function(input, output, session) {
  output$text <- renderText({
    current_dept <- input$dept  # 올바른 위치
    paste("선택:", current_dept)
  })
}

규칙 2. 반응식은 의존성이 변경될 때만 다시 실행됩니다.

output$result <- renderText({
  if (input$show) {
    paste("점수:", input$score)
  } else {
    "숨김"
  }
})

input$show가 FALSE일 때는 input$score를 읽지 않습니다. 따라서 이때 input$score가 바뀌어도 output$result는 다시 실행되지 않습니다. Shiny는 실제로 실행된 경로에서 읽은 값만 의존성으로 추적합니다.

일반 변수 vs 반응형 값

처음 Shiny를 배울 때 자주 혼동하는 부분이 있습니다.

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

  # 일반 변수 — 반응형이 아닙니다. 한 번 계산되고 끝입니다.
  greeting <- paste("안녕하세요,", "방문자")

  # 반응형 값 — input이 바뀌면 다시 계산됩니다.
  output$greeting <- renderText({
    paste("안녕하세요,", input$name)
  })

}

greeting은 앱이 시작될 때 한 번 계산되고 그 후로 변하지 않습니다. output$greetinginput$name이 바뀔 때마다 다시 계산됩니다.

엑셀 비유로 정리하기

Shiny의 반응형 시스템을 엑셀로 정리하면 이렇습니다.

Shiny 엑셀
input$값 사용자가 직접 입력하는 셀 (A1, B2 등)
reactive({ }) 중간 계산 셀 (=A1*2)
renderText({ }) 최종 결과를 표시하는 셀
의존성 그래프 셀 간의 공식 참조 관계
지연 평가 화면에 보이는 셀만 계산

엑셀에서 공식 셀은 참조하는 셀이 바뀌면 자동으로 업데이트됩니다. Shiny도 마찬가지입니다. 이 비유를 머릿속에 두고 다음 챕터로 넘어갑시다.