iBetter Books
수정

반응식 캐싱 (bindCache)

사용자가 지역을 "서울"로 선택했다가 "부산"으로 바꿨다가 다시 "서울"로 돌아올 때, 앱은 "서울" 결과를 처음부터 다시 계산합니다. 계산이 1초라면 괜찮지만, 10초짜리 모델 학습이라면 사용자는 매번 기다려야 합니다. bindCache()는 이미 계산한 결과를 기억해뒀다가 같은 조건이 오면 즉시 반환합니다.

bindCache() 기본 사용법

reactive() 또는 render*() 뒤에 |> bindCache(...)를 붙이면 됩니다. 괄호 안의 값이 캐시 키(key)가 됩니다.

# 수정: modular-app/app.R (server 함수 내부)

server <- function(input, output, session) {

  # 캐시 없는 버전
  filtered <- reactive({
    Sys.sleep(2)   # 무거운 연산 시뮬레이션
    pop_data |> filter(region %in% input$region)
  })

  # 캐시 있는 버전 — input$region이 같으면 저장된 결과 반환
  filtered_cached <- reactive({
    Sys.sleep(2)
    pop_data |> filter(region %in% input$region)
  }) |> bindCache(input$region)

  output$chart <- renderPlotly({
    ...
  }) |> bindCache(input$region, input$year_range)
}

bindCache(input$region)을 붙이면 input$region이 바뀔 때만 새로 계산합니다. 같은 지역 선택이 다시 오면 저장된 결과를 돌려줍니다.

캐시 키 설계

캐시 키는 출력 결과를 유일하게 결정하는 입력의 조합이어야 합니다.

# 올바른 키: 결과에 영향을 주는 입력 모두 포함
reactive({ ... }) |> bindCache(input$region, input$year_range, input$metric)

# 잘못된 키: year_range가 누락 — 연도가 바뀌어도 캐시를 재사용해 엉뚱한 결과 반환
reactive({ ... }) |> bindCache(input$region)

캐시 범위 설정

bindCache()는 기본적으로 세션(사용자) 범위로 캐싱합니다. 모든 사용자가 공유하는 앱 전역 캐시를 쓰려면 cache 인자를 지정합니다.

# 앱 수준 캐시 (모든 세션이 공유)
reactive({
  read_heavy_data(input$region)
}) |> bindCache(input$region, cache = "app")

# 세션 수준 캐시 (기본값, 각 사용자별 분리)
reactive({
  ...
}) |> bindCache(input$region, cache = "session")

공공데이터처럼 사용자마다 동일한 결과가 나오는 경우에는 cache = "app"이 훨씬 효율적입니다.

memoise 패키지 — 일반 R 함수 캐싱

bindCache()는 Shiny 반응형 전용입니다. 일반 R 함수를 캐싱할 때는 memoise 패키지를 씁니다.

install.packages("memoise")
# 수정: modular-app/global.R
library(memoise)

# 원래 함수
fetch_data <- function(region, year) {
  Sys.sleep(3)   # API 호출 가정
  data.frame(region = region, year = year, value = rnorm(10))
}

# 메모이즈된 함수 — 같은 인자면 저장된 결과 반환
fetch_data_cached <- memoise(fetch_data)
# server.R에서 사용
server <- function(input, output, session) {
  output$chart <- renderPlotly({
    df <- fetch_data_cached(input$region, input$year)   # 캐시 자동 활용
    ...
  })
}

memoise는 세션을 넘어 앱 재시작 전까지 캐시를 유지합니다. 디스크에 저장하려면 memoise(f, cache = cache_filesystem("cache/")) 형태로 씁니다.

캐시 무효화 전략

데이터가 갱신되면 캐시를 지워야 합니다.

# memoise 캐시 전체 초기화
forget(fetch_data_cached)

# 특정 인자 조합만 제거는 지원하지 않음 — 전체 forget 후 재시작

bindCache는 앱 재시작 시 자동으로 초기화됩니다. 장기 캐시가 필요하면 cachem 패키지의 cache_disk()를 씁니다.

언제 캐싱을 쓸까

상황 권장 방법
Shiny 반응식이 느릴 때 bindCache()
일반 R 함수가 반복 호출될 때 memoise()
앱 시작 시 한 번만 로드할 때 global.R에서 변수로 저장
파일 기반 캐시가 필요할 때 cachem::cache_disk()

캐싱은 만능이 아닙니다. 캐시 키를 잘못 설계하면 오래된 결과를 보여주는 버그가 생깁니다. 캐싱을 적용할 때는 "이 입력 조합이 항상 같은 출력을 내는가"를 먼저 확인하세요.