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_container는 id = "item_container"인 요소를 가리킵니다.
세 가지 방법 비교
| 방법 | 처리 위치 | 언제 사용 |
|---|---|---|
conditionalPanel() |
클라이언트(JS) | 단순 보이기/숨기기, 서버 왕복 불필요 |
renderUI() + uiOutput() |
서버(R) | 선택지가 데이터에 따라 동적으로 변함 |
insertUI() / removeUI() |
서버(R) | 요소 수 자체가 동적으로 늘거나 줄어듦 |
세 방법 모두 같은 결과를 낼 수 있는 경우도 있습니다. 가장 단순한 방법인 conditionalPanel()을 먼저 고려하고, 서버 데이터가 필요하면 renderUI(), 요소 수가 바뀌어야 하면 insertUI/removeUI를 택하는 것이 좋은 순서입니다.