반응형 디버깅
Shiny 앱을 만들다 보면 "왜 이게 업데이트가 안 되지?", "왜 이게 너무 자주 실행되지?" 같은 상황을 마주칩니다. 반응형 시스템은 눈에 보이지 않기 때문에 일반 R 코드보다 디버깅이 까다롭습니다. 이번 챕터에서는 Shiny 앱을 효과적으로 디버깅하는 방법을 알아봅니다.
print()와 cat() — 가장 빠른 방법
가장 간단한 디버깅은 콘솔에 값을 출력하는 것입니다.
server <- function(input, output, session) {
filtered <- reactive({
cat("[filtered] 실행됨. dept:", input$dept, "\n") # 언제 실행되는지 확인
result <- students |> filter(dept == input$dept)
cat("[filtered] 결과 행 수:", nrow(result), "\n")
result
})
output$table <- renderTable({
cat("[renderTable] 실행됨.\n")
filtered()
})
}
앱을 실행하고 위젯을 조작하면 RStudio 콘솔에서 각 반응식이 언제 실행되는지 확인할 수 있습니다.
print()는 콘솔에 출력하지만 반환값에 영향을 줄 수 있으므로 중간 확인에는 cat()을 주로 사용합니다.
message() 활용
cat() 대신 message()를 사용하면 표준 에러(stderr)로 출력되어 일반 출력과 구분됩니다.
filtered <- reactive({
message("filtered 실행 — input$dept: ", input$dept)
students |> filter(dept == input$dept)
})
browser() — 중단점으로 상태 검사
browser()는 코드 실행을 해당 지점에서 멈추고 R 콘솔에서 직접 변수를 확인할 수 있게 해줍니다.
server <- function(input, output, session) {
filtered <- reactive({
browser() # 이 지점에서 실행이 멈춥니다
students |> filter(dept == input$dept)
})
}
앱을 실행하고 filtered()가 처음 호출될 때 RStudio 디버거가 활성화됩니다. 이 상태에서 input$dept를 입력하면 현재 값을 확인할 수 있고, n을 입력하면 한 줄씩 실행할 수 있습니다.
디버깅이 끝나면 browser() 줄을 지웁니다.
reactlog — 의존성 그래프 시각화
reactlog 패키지는 Shiny의 반응형 의존성 그래프를 시각적으로 보여줍니다. 어떤 반응식이 어떤 것에 의존하는지, 언제 실행됐는지 한눈에 파악할 수 있습니다.
먼저 패키지를 설치합니다.
install.packages("reactlog")
사용 방법은 두 단계입니다.
library(shiny)
library(reactlog)
# 1단계: 로그 기록을 활성화합니다
reactlog_enable()
# 2단계: 앱을 실행합니다
shinyApp(ui, server)
앱을 실행하고 위젯을 조작한 후 앱을 종료합니다. 그 다음 아래 명령어로 그래프를 엽니다.
shiny::reactlogShow()
브라우저에서 인터랙티브 의존성 그래프가 열립니다.
실행 중인 앱에서 바로 확인하고 싶다면 키보드 단축키를 사용합니다.
- macOS:
Cmd + F3 - Windows:
Ctrl + F3
reactlog 그래프 읽는 법
reactlog 화면에서 볼 수 있는 주요 노드 유형입니다.
| 색상/모양 | 의미 |
|---|---|
| 주황색 타원 | 반응형 소스 (input$*) |
| 연두색 사각형 | 반응형 표현식 (reactive()) |
| 파란색 사각형 | 반응형 출력 (render*(), observe()) |
| 화살표 | 의존성 방향 |
그래프에서 각 노드를 클릭하면 그 반응식이 실행된 횟수, 최근 실행 시각, 의존하는 노드 목록을 확인할 수 있습니다.
흔한 실수와 해결법
실수 1. 반응형 값을 반응형 컨텍스트 밖에서 읽는다
# 잘못된 코드 — 오류 발생
server <- function(input, output, session) {
current_dept <- input$dept # 오류: 반응형 컨텍스트 밖
output$text <- renderText({
paste("학과:", current_dept)
})
}
# 올바른 코드
server <- function(input, output, session) {
output$text <- renderText({
paste("학과:", input$dept) # render*() 안에서 읽습니다
})
}
input$*는 반드시 reactive(), render*(), observe() 같은 반응형 컨텍스트 안에서 읽어야 합니다.
실수 2. reactiveVal()을 괄호 없이 읽는다
my_val <- reactiveVal(0)
# 잘못된 코드
output$text <- renderText({
my_val # 반응식 객체를 반환합니다. 값이 아닙니다.
})
# 올바른 코드
output$text <- renderText({
my_val() # 괄호를 붙여야 값을 읽습니다.
})
실수 3. reactive() 결과를 괄호 없이 사용한다
filtered <- reactive({ students |> filter(dept == input$dept) })
# 잘못된 코드
output$table <- renderTable({
filtered # 반응식 객체를 반환합니다.
})
# 올바른 코드
output$table <- renderTable({
filtered() # 괄호를 붙여야 계산 결과를 반환합니다.
})
실수 4. observe()에서 반환값을 기대한다
# 잘못된 코드 — observe()는 값을 반환하지 않습니다
result <- observe({
students |> filter(dept == input$dept)
})
# 올바른 코드 — 값이 필요하면 reactive()를 사용합니다
result <- reactive({
students |> filter(dept == input$dept)
})
실수 5. 순환 의존성
# A가 B에 의존하고, B가 A에 의존하면 무한 루프가 발생합니다
a <- reactive({ b() + 1 })
b <- reactive({ a() + 1 })
오류 메시지를 보면 "maximum reactive dependency depth reached" 같은 내용이 나옵니다. reactlog로 의존성 그래프를 확인하면 순환 관계를 시각적으로 찾을 수 있습니다.
req() — 조건이 충족될 때만 실행하기
req()는 주어진 조건이 충족되지 않으면 반응식 실행을 조용히 멈춥니다. 오류를 일으키지 않고 "아직 준비가 안 됐다"는 것을 Shiny에 알립니다.
server <- function(input, output, session) {
output$table <- renderTable({
req(input$dept) # dept가 NULL이 아닐 때만 실행합니다
req(nchar(input$dept) > 0) # 빈 문자열이 아닐 때만 실행합니다
students |> filter(dept == input$dept)
})
}
앱이 처음 로드될 때 아직 사용자가 아무것도 선택하지 않은 상태에서 출력을 안전하게 처리할 때 특히 유용합니다.
validate()와 need() — 사용자에게 오류 메시지 표시
req()가 조용히 멈추는 것과 달리, validate()는 사용자에게 친절한 오류 메시지를 표시합니다.
server <- function(input, output, session) {
output$table <- renderTable({
validate(
need(input$dept != "", "학과를 선택해주세요."),
need(input$min_score >= 0, "점수는 0 이상이어야 합니다.")
)
students |>
filter(dept == input$dept, score >= input$min_score)
})
}
조건이 충족되지 않으면 출력 영역에 지정한 메시지가 표시됩니다. 데이터가 없거나 조건이 잘못됐을 때 사용자에게 안내하는 데 적합합니다.
이 챕터에서 배운 디버깅 도구들을 활용하면 반응형 시스템의 동작을 투명하게 파악할 수 있습니다. 특히 reactlog는 Shiny 앱이 복잡해질수록 진가를 발휘합니다. 다음 파트에서는 Shiny의 다양한 입력과 출력 위젯을 본격적으로 다룹니다.