iBetter Books
수정

Ch 03. 동적 UI (renderUI, conditionalPanel)

앱이 복잡해질수록 상황에 따라 UI가 변해야 할 때가 생깁니다. 사용자가 "파일 업로드"를 선택하면 파일 선택 버튼이 나타나고, "직접 입력"을 선택하면 텍스트 박스가 나타나는 식입니다. Shiny는 이런 동적 UI를 만드는 방법을 여럿 제공합니다.

conditionalPanel: 조건에 따라 보이고 숨기기

conditionalPanel()은 JavaScript 조건식이 참일 때만 패널을 보여줍니다. 서버 코드 없이 순수하게 UI 레벨에서 처리하므로 반응이 빠릅니다.

library(shiny)

ui <- fluidPage(
  radioButtons("input_method", "데이터 입력 방법",
               choices = c("예제 데이터" = "sample",
                           "파일 업로드" = "upload"),
               selected = "sample"),

  # input_method가 "upload"일 때만 표시
  conditionalPanel(
    condition = "input.input_method == 'upload'",
    fileInput("data_file", "CSV 파일 선택",
              accept = ".csv")
  ),

  # 예제 데이터일 때만 표시
  conditionalPanel(
    condition = "input.input_method == 'sample'",
    selectInput("dataset", "예제 데이터셋",
                choices = c("mtcars", "iris", "airquality"))
  ),

  tableOutput("preview")
)

server <- function(input, output, session) {
  output$preview <- renderTable({
    if (input$input_method == "sample") {
      head(get(input$dataset))
    } else {
      req(input$data_file)
      head(read.csv(input$data_file$datapath))
    }
  })
}

shinyApp(ui, server)

condition 인수는 JavaScript 코드 문자열입니다. Shiny 입력값은 input.inputId 형태로 접근하고, 출력값은 output.outputId로 접근합니다. R 코드가 아니라 JS 코드이므로 ==, !=, &&, ||를 씁니다.

renderUI와 uiOutput: 서버에서 UI 만들기

conditionalPanel()은 UI를 미리 만들어두고 보이고 숨기는 방식입니다. 하지만 선택지 자체가 데이터에 따라 바뀌어야 한다면 서버에서 UI를 동적으로 생성해야 합니다. 이때 renderUI()uiOutput()을 씁니다.

library(shiny)

ui <- fluidPage(
  selectInput("dataset", "데이터셋 선택",
              choices = c("mtcars", "iris", "airquality")),

  # 서버가 만들어줄 자리
  uiOutput("col_selector"),

  plotOutput("col_plot")
)

server <- function(input, output, session) {
  # 선택된 데이터셋의 열 이름으로 selectInput 생성
  output$col_selector <- renderUI({
    df <- get(input$dataset)
    numeric_cols <- names(df)[sapply(df, is.numeric)]

    selectInput(
      inputId  = "x_col",
      label    = paste0(input$dataset, "의 변수 선택"),
      choices  = numeric_cols,
      selected = numeric_cols[1]
    )
  })

  output$col_plot <- renderPlot({
    req(input$x_col)
    df <- get(input$dataset)

    hist(df[[input$x_col]],
         main = paste(input$dataset, "-", input$x_col),
         xlab = input$x_col,
         col  = "#3498db")
  })
}

shinyApp(ui, server)

renderUI() 안에서는 어떤 Shiny UI 함수든 반환할 수 있습니다. 단일 위젯뿐 아니라 tagList()로 여러 위젯을 묶어 반환할 수도 있습니다.

output$dynamic_panel <- renderUI({
  tagList(
    sliderInput("alpha", "투명도", 0, 1, 0.7, step = 0.1),
    selectInput("color", "색상",
                choices = c("파랑" = "blue",
                            "빨강" = "red",
                            "초록" = "green")),
    checkboxInput("legend", "범례 표시", TRUE)
  )
})

renderUI 사용 시 주의사항

renderUI()로 만든 위젯의 inputId는 처음에 존재하지 않습니다. 따라서 서버에서 input$x_col 같은 값을 읽을 때 반드시 req(input$x_col)로 보호해야 합니다.

또한 renderUI()가 재실행되면 내부 위젯이 초기값으로 리셋됩니다. 값을 유지하고 싶다면 updateSelectInput() 같은 update* 계열 함수를 사용하는 편이 좋습니다.

insertUI와 removeUI: 동적으로 추가/삭제

insertUI()removeUI()는 DOM에 UI 요소를 직접 추가하거나 삭제합니다. "항목 추가" 버튼을 눌러 입력 행을 동적으로 늘리는 패턴에 유용합니다.

library(shiny)

ui <- fluidPage(
  actionButton("add_btn", "항목 추가", class = "btn-success"),
  actionButton("remove_btn", "마지막 항목 삭제", class = "btn-danger"),
  hr(),
  div(id = "item_container")  # 항목이 삽입될 자리
)

server <- function(input, output, session) {
  item_count <- reactiveVal(0)

  observeEvent(input$add_btn, {
    n <- item_count() + 1
    item_count(n)

    insertUI(
      selector = "#item_container",
      where    = "beforeEnd",  # 컨테이너 안 마지막에 삽입
      ui       = div(
        id = paste0("item_", n),
        style = "margin-bottom: 8px;",
        textInput(paste0("text_", n),
                  label = paste0("항목 ", n),
                  placeholder = "내용 입력")
      )
    )
  })

  observeEvent(input$remove_btn, {
    n <- item_count()
    if (n > 0) {
      removeUI(selector = paste0("#item_", n))
      item_count(n - 1)
    }
  })
}

shinyApp(ui, server)

where 인수에는 "beforeBegin", "afterBegin", "beforeEnd", "afterEnd" 중 하나를 씁니다. CSS 선택자 #item_containerid = "item_container"인 요소를 가리킵니다.

세 가지 방법 비교

방법 처리 위치 언제 사용
conditionalPanel() 클라이언트(JS) 단순 보이기/숨기기, 서버 왕복 불필요
renderUI() + uiOutput() 서버(R) 선택지가 데이터에 따라 동적으로 변함
insertUI() / removeUI() 서버(R) 요소 수 자체가 동적으로 늘거나 줄어듦

세 방법 모두 같은 결과를 낼 수 있는 경우도 있습니다. 가장 단순한 방법인 conditionalPanel()을 먼저 고려하고, 서버 데이터가 필요하면 renderUI(), 요소 수가 바뀌어야 하면 insertUI/removeUI를 택하는 것이 좋은 순서입니다.