iBetter Books
수정

Ch 04. 파일 업로드와 다운로드

앱이 외부 데이터를 받아 분석하고, 결과를 파일로 내보낼 수 있다면 훨씬 실용적인 도구가 됩니다. Shiny의 fileInput()은 사용자의 컴퓨터에서 파일을 받아오고, downloadHandler()는 서버에서 만든 파일을 사용자에게 전달합니다. 이 두 가지를 함께 익히면 데이터 분석 워크플로우를 앱 하나로 완성할 수 있습니다.

fileInput으로 파일 받기

fileInput()은 파일 선택 버튼을 만듭니다. 사용자가 파일을 선택하면 서버의 임시 경로에 저장되고, input$file_id로 파일 정보를 담은 데이터프레임이 반환됩니다.

fileInput(
  inputId  = "upload",
  label    = "파일을 선택하세요",
  multiple = FALSE,            # 다중 파일 허용 여부
  accept   = c(".csv", ".xlsx", ".xls"),  # 허용 확장자
  buttonLabel = "파일 선택",
  placeholder = "선택된 파일 없음"
)

input$upload는 파일이 선택되지 않으면 NULL이고, 선택되면 다음 열을 가진 데이터프레임입니다.

설명
name 원본 파일명
size 파일 크기(바이트)
type MIME 타입
datapath 서버 임시 저장 경로

실제 파일은 input$upload$datapath로 읽습니다.

CSV 파일 업로드

library(shiny)

ui <- fluidPage(
  fileInput("csv_file", "CSV 파일 업로드", accept = ".csv"),
  tableOutput("preview"),
  verbatimTextOutput("info")
)

server <- function(input, output, session) {
  # 업로드된 데이터를 reactive로 관리
  uploaded_data <- reactive({
    req(input$csv_file)  # 파일이 없으면 실행 멈춤
    read.csv(input$csv_file$datapath,
             fileEncoding = "UTF-8-BOM",  # 한글 CSV 대응
             stringsAsFactors = FALSE)
  })

  output$preview <- renderTable({
    head(uploaded_data(), 10)
  })

  output$info <- renderPrint({
    df <- uploaded_data()
    cat("파일명:", input$csv_file$name, "\n")
    cat("행 수:", nrow(df), "\n")
    cat("열 수:", ncol(df), "\n")
    cat("열 이름:", paste(names(df), collapse = ", "), "\n")
  })
}

shinyApp(ui, server)

req(input$csv_file)은 파일이 업로드되지 않은 상태에서 코드가 실행되는 것을 막습니다. 이 한 줄이 없으면 앱이 처음 로드될 때 NULL 관련 오류가 발생합니다.

Excel 파일 업로드

Excel 파일을 읽으려면 readxl 패키지가 필요합니다.

library(shiny)
library(readxl)

ui <- fluidPage(
  fileInput("excel_file", "Excel 파일 업로드",
            accept = c(".xlsx", ".xls")),
  uiOutput("sheet_selector"),
  tableOutput("excel_preview")
)

server <- function(input, output, session) {
  # 시트 목록 동적 렌더링
  output$sheet_selector <- renderUI({
    req(input$excel_file)
    sheets <- excel_sheets(input$excel_file$datapath)
    selectInput("sheet", "시트 선택", choices = sheets)
  })

  output$excel_preview <- renderTable({
    req(input$excel_file, input$sheet)
    read_excel(input$excel_file$datapath,
               sheet = input$sheet,
               n_max = 10)
  })
}

shinyApp(ui, server)

Excel 파일은 시트가 여러 개일 수 있으므로, excel_sheets()로 시트 목록을 읽어 selectInput()을 동적으로 만드는 패턴이 실용적입니다.

파일 크기 제한

Shiny의 기본 업로드 크기 제한은 5MB입니다. options(shiny.maxRequestSize = ...)로 늘릴 수 있습니다.

# 앱 최상단 또는 server 함수 시작 부분에 추가
options(shiny.maxRequestSize = 50 * 1024^2)  # 50MB로 제한 확장

downloadHandler로 파일 내보내기

downloadHandler()downloadButton() 또는 downloadLink()와 함께 씁니다. filename으로 저장될 파일명을 지정하고, content에서 파일을 실제로 씁니다.

ui <- fluidPage(
  downloadButton("download_csv", "CSV 다운로드", class = "btn-primary"),
  downloadButton("download_xlsx", "Excel 다운로드", class = "btn-success")
)

server <- function(input, output, session) {
  # 분석 결과 데이터 (실제 앱에서는 reactive 값)
  result_data <- reactive({
    mtcars
  })

  # CSV 다운로드
  output$download_csv <- downloadHandler(
    filename = function() {
      paste0("분석결과_", format(Sys.Date(), "%Y%m%d"), ".csv")
    },
    content = function(file) {
      write.csv(result_data(), file,
                row.names = FALSE,
                fileEncoding = "UTF-8")
    }
  )

  # Excel 다운로드
  output$download_xlsx <- downloadHandler(
    filename = function() {
      paste0("분석결과_", format(Sys.Date(), "%Y%m%d"), ".xlsx")
    },
    content = function(file) {
      writexl::write_xlsx(result_data(), path = file)
    }
  )
}

shinyApp(ui, server)

filename은 파일명을 반환하는 함수입니다. contentfile이라는 임시 경로를 인수로 받아, 그 경로에 파일을 쓰는 함수입니다.

그래프 다운로드

그래프도 downloadHandler()로 내보낼 수 있습니다.

output$download_plot <- downloadHandler(
  filename = function() {
    paste0("chart_", Sys.Date(), ".png")
  },
  content = function(file) {
    png(file, width = 1200, height = 800, res = 150)
    print(
      ggplot(mtcars, aes(x = hp, y = mpg)) +
        geom_point() +
        theme_minimal()
    )
    dev.off()
  }
)

PDF로 저장하고 싶다면 png() 대신 pdf()를, SVG라면 svg()를 씁니다. ggplot2 객체는 print()를 명시적으로 호출해야 파일에 실제로 저장됩니다.

실전 패턴: 업로드-분석-다운로드 워크플로우

library(shiny)
library(dplyr)

ui <- fluidPage(
  titlePanel("CSV 분석 도구"),
  sidebarLayout(
    sidebarPanel(
      fileInput("file", "CSV 업로드", accept = ".csv"),
      hr(),
      uiOutput("col_picker"),
      hr(),
      downloadButton("dl_result", "결과 다운로드", class = "btn-primary")
    ),
    mainPanel(
      h4("원본 데이터 (상위 5행)"),
      tableOutput("raw_preview"),
      hr(),
      h4("열별 요약 통계"),
      tableOutput("summary_out")
    )
  )
)

server <- function(input, output, session) {
  raw_data <- reactive({
    req(input$file)
    read.csv(input$file$datapath, stringsAsFactors = FALSE)
  })

  output$col_picker <- renderUI({
    req(raw_data())
    num_cols <- names(raw_data())[sapply(raw_data(), is.numeric)]
    checkboxGroupInput("selected_cols", "분석할 열 선택",
                       choices  = num_cols,
                       selected = num_cols[1:min(3, length(num_cols))])
  })

  summary_data <- reactive({
    req(raw_data(), input$selected_cols)
    raw_data() %>%
      select(all_of(input$selected_cols)) %>%
      summarise(across(everything(), list(
        평균   = ~ round(mean(.x, na.rm = TRUE), 2),
        표준편차 = ~ round(sd(.x, na.rm = TRUE), 2),
        최솟값 = ~ min(.x, na.rm = TRUE),
        최댓값 = ~ max(.x, na.rm = TRUE)
      )))
  })

  output$raw_preview <- renderTable(head(raw_data(), 5))
  output$summary_out <- renderTable(summary_data())

  output$dl_result <- downloadHandler(
    filename = function() paste0("summary_", Sys.Date(), ".csv"),
    content  = function(file) write.csv(summary_data(), file, row.names = FALSE)
  )
}

shinyApp(ui, server)

이 패턴이 실전에서 가장 많이 쓰이는 구조입니다. 파일을 받아 reactive()로 관리하고, 분석 결과도 reactive()로 만들어 화면 표시와 다운로드 모두에 재활용합니다.