iBetter Books
수정

Ch 01. 기본 레이아웃 (sidebarLayout, fluidRow)

화면을 어떻게 나누느냐에 따라 앱의 첫인상이 결정됩니다. Shiny는 Bootstrap 그리드를 기반으로 하는 레이아웃 시스템을 제공합니다. 가장 자주 쓰이는 세 가지 레이아웃 패턴을 익혀두면 대부분의 앱 화면을 자연스럽게 구성할 수 있습니다.

sidebarLayout: 가장 고전적인 구성

sidebarLayout()은 왼쪽에 사이드바, 오른쪽에 메인 영역을 배치하는 가장 전형적인 레이아웃입니다. 입력 컨트롤은 사이드바에, 결과 출력은 메인 영역에 두는 것이 관례입니다.

library(shiny)

ui <- fluidPage(
  titlePanel("사이드바 레이아웃 예제"),

  sidebarLayout(
    position = "left",  # "right"로 변경하면 사이드바가 오른쪽

    sidebarPanel(
      width = 3,  # 전체 12 열 중 3 열 (25%)
      h4("설정"),
      sliderInput("n", "데이터 수", 10, 500, 100),
      selectInput("color", "색상",
                  choices = c("파랑" = "steelblue",
                              "빨강" = "tomato",
                              "초록" = "seagreen"))
    ),

    mainPanel(
      width = 9,  # 나머지 9 열 (75%)
      plotOutput("scatter")
    )
  )
)

server <- function(input, output, session) {
  output$scatter <- renderPlot({
    x <- rnorm(input$n)
    y <- rnorm(input$n)
    plot(x, y, col = input$color, pch = 19, cex = 1.2,
         main = paste("랜덤 산점도 (n =", input$n, ")"))
  })
}

shinyApp(ui, server)

sidebarPanel()mainPanel()width 합은 12가 되어야 합니다. 기본값은 각각 4와 8입니다.

fluidRow와 column: 자유로운 그리드 배치

fluidRow()column()은 Bootstrap 12열 그리드를 직접 제어합니다. 사이드바가 없는 대시보드 스타일이나, 여러 위젯을 가로로 나란히 배치할 때 씁니다.

library(shiny)

ui <- fluidPage(
  titlePanel("그리드 레이아웃 예제"),

  # 첫 번째 행: 요약 카드 3개
  fluidRow(
    column(4,
      wellPanel(
        h4("총 관측치"),
        textOutput("n_obs")
      )
    ),
    column(4,
      wellPanel(
        h4("평균 연비(mpg)"),
        textOutput("mean_mpg")
      )
    ),
    column(4,
      wellPanel(
        h4("최고 마력(hp)"),
        textOutput("max_hp")
      )
    )
  ),

  # 두 번째 행: 필터와 그래프
  fluidRow(
    column(3,
      selectInput("cyl", "실린더",
                  choices = c("전체" = 0, "4" = 4, "6" = 6, "8" = 8),
                  selected = 0)
    ),
    column(9,
      plotOutput("mpg_plot")
    )
  )
)

server <- function(input, output, session) {
  filtered <- reactive({
    if (input$cyl == 0) mtcars
    else mtcars[mtcars$cyl == as.integer(input$cyl), ]
  })

  output$n_obs    <- renderText(nrow(filtered()))
  output$mean_mpg <- renderText(round(mean(filtered()$mpg), 1))
  output$max_hp   <- renderText(max(filtered()$hp))

  output$mpg_plot <- renderPlot({
    boxplot(mpg ~ cyl, data = filtered(),
            main = "실린더별 연비", col = "#3498db",
            xlab = "실린더 수", ylab = "연비(mpg)")
  })
}

shinyApp(ui, server)

column(width, ...)에서 width는 1에서 12 사이 정수입니다. 한 fluidRow()column()width 합이 12를 넘으면 자동으로 줄 바꿈됩니다.

wellPanel: 시각적 구분 상자

wellPanel()은 회색 배경의 둥근 테두리 박스를 만들어 내용을 시각적으로 묶습니다. 관련 있는 컨트롤을 그룹화할 때 유용합니다.

sidebarPanel(
  wellPanel(
    h5("데이터 필터"),
    sliderInput("year", "연도", 2010, 2024, c(2015, 2024)),
    selectInput("region", "지역", choices = c("전체", "서울", "부산"))
  ),
  wellPanel(
    h5("차트 설정"),
    radioButtons("chart_type", "종류",
                 choices = c("막대" = "bar", "선" = "line"),
                 inline = TRUE),
    checkboxInput("grid", "격자 표시", TRUE)
  )
)

레이아웃 선택 가이드

어떤 레이아웃을 쓸지 고민될 때 아래 기준을 참고하면 됩니다.

상황 추천 레이아웃
입력 컨트롤 + 단일 출력 sidebarLayout()
여러 차트를 격자로 배치 fluidRow() + column()
관리자 대시보드 스타일 fluidRow() + column() + navbarPage()
입력 컨트롤 그룹화 wellPanel() 중첩

fluidPage()는 화면 너비에 따라 유연하게 늘어납니다. 반면 fixedPage()는 최대 너비가 고정된 컨테이너를 씁니다. 대부분의 경우 fluidPage()가 더 자연스럽습니다.

중첩 레이아웃

sidebarLayout() 안에 fluidRow()를 넣는 식으로 레이아웃을 중첩할 수 있습니다.

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      sliderInput("n", "수", 10, 100, 50)
    ),
    mainPanel(
      # 메인 영역을 다시 두 열로 분리
      fluidRow(
        column(6, plotOutput("plot1")),
        column(6, plotOutput("plot2"))
      ),
      fluidRow(
        column(12, tableOutput("tbl"))
      )
    )
  )
)

레이아웃은 HTML 테이블이 아닌 Bootstrap 그리드이므로, 복잡하게 중첩해도 반응형 동작이 유지됩니다.