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은 파일명을 반환하는 함수입니다. content는 file이라는 임시 경로를 인수로 받아, 그 경로에 파일을 쓰는 함수입니다.
그래프 다운로드
그래프도 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()로 만들어 화면 표시와 다운로드 모두에 재활용합니다.