iBetter Books
수정

대용량 데이터 처리 전략

100만 행짜리 CSV 파일을 read.csv()로 읽으면 Shiny 앱이 요청마다 수십 초씩 걸립니다. 이 챕터에서는 실무에서 검증된 네 가지 전략으로 앱을 빠르게 만드는 방법을 다룹니다.

전략 1: global.R에서 데이터 한 번만 로드

Shiny는 앱 시작 시 global.R을 딱 한 번 실행합니다. 무거운 데이터 로드를 여기에 넣으면 사용자가 접속할 때마다 읽는 부담이 사라집니다.

modular-app/
├── global.R    ← 앱 시작 시 1회 실행
├── app.R       ← UI·Server 정의
└── data/
    └── large_dataset.csv
# 새 파일: modular-app/global.R
library(tidyverse)
library(arrow)

message("데이터 로딩 중...")

# 앱 전체에서 공유되는 전역 변수
APP_DATA <- read_csv("data/large_dataset.csv", show_col_types = FALSE)

# 자주 쓰는 집계 결과도 미리 계산
SUMMARY_BY_REGION <- APP_DATA |>
  group_by(region, year) |>
  summarise(
    total   = sum(value, na.rm = TRUE),
    average = mean(value, na.rm = TRUE),
    .groups = "drop"
  )

message("데이터 로딩 완료: ", nrow(APP_DATA), "행")

server.R에서는 APP_DATA를 그냥 씁니다. read_csv()를 매번 호출하지 않아도 됩니다.

전략 2: Arrow와 Parquet 파일

CSV는 사람이 읽기에는 좋지만 컴퓨터가 읽기에는 느립니다. Parquet는 컬럼 기반 바이너리 형식으로, 같은 데이터를 10배 빠르게 읽습니다.

install.packages("arrow")
# 새 파일: modular-app/global.R (Arrow 버전)
library(arrow)
library(dplyr)

# CSV를 Parquet로 변환 (최초 1회만)
if (!file.exists("data/large_dataset.parquet")) {
  df <- read.csv("data/large_dataset.csv")
  write_parquet(df, "data/large_dataset.parquet")
}

# Parquet 읽기 — CSV보다 훨씬 빠름
APP_DATA <- read_parquet("data/large_dataset.parquet")

더 나아가 open_dataset()으로 파일을 메모리에 올리지 않고 필요한 컬럼·행만 읽을 수 있습니다.

# 디스크에서 직접 쿼리 (메모리 절약)
ds <- open_dataset("data/large_dataset.parquet")

server <- function(input, output, session) {
  filtered <- reactive({
    ds |>
      filter(region == input$region) |>
      collect()   # 이 시점에 실제로 읽음
  })
}

전략 3: 사전 집계로 쿼리 크기 줄이기

원본 데이터를 매번 필터링하는 대신, 자주 쓰는 집계 결과를 미리 만들어 저장합니다.

# 수정: modular-app/global.R
library(tidyverse)

# 원본 데이터 (100만 행)
RAW_DATA <- read_parquet("data/raw.parquet")

# 사전 집계 (1만 행) — 대시보드 차트용
DAILY_SUMMARY <- RAW_DATA |>
  group_by(date, region) |>
  summarise(
    count   = n(),
    revenue = sum(revenue, na.rm = TRUE),
    .groups = "drop"
  )

# 월별 집계 (1천 행) — KPI 카드용
MONTHLY_KPI <- DAILY_SUMMARY |>
  mutate(month = floor_date(date, "month")) |>
  group_by(month, region) |>
  summarise(
    total_count   = sum(count),
    total_revenue = sum(revenue),
    .groups = "drop"
  )

대시보드가 DAILY_SUMMARY를 쓰면 100만 행 대신 1만 행을 필터링하므로 속도가 수십 배 빨라집니다.

전략 4: reactiveValues 최소화

reactiveValues를 과도하게 쓰면 예상치 못한 시점에 재실행이 일어납니다. 읽기 전용 데이터는 반응형으로 감싸지 않고 일반 변수로 씁니다.

# 나쁜 패턴 — APP_DATA는 바뀌지 않는데 반응형으로 감쌈
server <- function(input, output, session) {
  rv <- reactiveValues(data = APP_DATA)   # 불필요

  output$chart <- renderPlotly({
    rv$data |> filter(...)
  })
}

# 좋은 패턴 — global.R의 APP_DATA를 직접 참조
server <- function(input, output, session) {
  filtered <- reactive({
    APP_DATA |> filter(region %in% input$region)
  })

  output$chart <- renderPlotly({
    filtered() |> ...
  })
}

reactiveValues는 앱 상태가 실제로 변할 때(파일 업로드, 버튼 클릭으로 데이터 추가)만 씁니다.

성능 측정

어느 부분이 느린지 모르면 개선도 어렵습니다. profvis 패키지로 병목을 찾습니다.

install.packages("profvis")
library(profvis)

# 느린 코드 블록을 profvis로 감싸기
profvis({
  df <- read.csv("data/large_dataset.csv")
  result <- df |>
    group_by(region) |>
    summarise(total = sum(value))
})

RStudio에서 실행하면 각 함수가 얼마나 시간을 썼는지 시각적으로 보여줍니다. 빨간 막대가 긴 함수가 최적화 대상입니다.

전략별 효과 요약

전략 효과 복잡도
global.R 로드 사용자 요청당 IO 제거 낮음
Parquet 파일 읽기 속도 5~10배 향상 낮음
Arrow 디스크 쿼리 메모리 사용량 대폭 감소 중간
사전 집계 필터링 데이터 크기 감소 중간
reactiveValues 최소화 불필요한 재실행 방지 낮음

대부분의 앱은 global.R + Parquet + 사전 집계만으로도 충분한 성능이 나옵니다. Arrow 디스크 쿼리는 데이터가 수억 행 이상일 때 고려합니다.