iBetter Books
수정

Shiny 모듈로 코드 분리

PART 07에서 만든 대시보드를 떠올려보세요. 챕터마다 비슷한 필터 UI가 반복됐습니다. 앱이 커지면 같은 패턴을 여러 번 복사·붙여넣기하게 되고, 하나를 수정할 때 나머지도 찾아 고쳐야 합니다. Shiny 모듈은 이 문제를 해결합니다.

왜 모듈이 필요한가

Shiny의 반응형 시스템은 전역 ID를 씁니다. inputId = "region"을 두 곳에서 쓰면 충돌이 발생합니다. 모듈은 네임스페이스(namespace)를 격리해서 같은 ID를 여러 번 안전하게 재사용할 수 있게 합니다.

일반 Shiny                    Shiny 모듈
─────────────────────          ─────────────────────────
input$region (충돌 위험)       input$filter1-region  (격리됨)
input$region (중복 불가)       input$filter2-region  (격리됨)

모듈의 두 가지 함수

모듈은 항상 두 함수 한 쌍으로 구성됩니다.

함수 역할 위치
필터UI(id) UI 정의, NS(id)로 네임스페이스 적용 ui.R 또는 모듈 파일
필터Server(id, ...) 서버 로직, moduleServer(id, ...) 사용 server.R 또는 모듈 파일

관례적으로 UI 함수 이름은 xxxUI, 서버 함수 이름은 xxxServer로 짓습니다.

지역 필터 모듈 만들기

PART 07의 지역·연도 필터를 모듈로 추출합니다.

# 새 파일: modular-app/R/filter_module.R

# ── 모듈 UI ───────────────────────────────────────────────
filterUI <- function(id, regions) {
  ns <- NS(id)   # 네임스페이스 생성기

  tagList(
    selectInput(
      inputId  = ns("region"),      # "region" → "id-region"
      label    = "지역 선택",
      choices  = regions,
      selected = regions[1],
      multiple = TRUE
    ),
    sliderInput(
      inputId = ns("year_range"),
      label   = "연도 범위",
      min     = 2018, max = 2023,
      value   = c(2018, 2023),
      sep     = ""
    )
  )
}

# ── 모듈 Server ───────────────────────────────────────────
filterServer <- function(id, data) {
  moduleServer(id, function(input, output, session) {

    # 필터링된 데이터를 반응형으로 반환
    reactive({
      data |>
        dplyr::filter(
          region %in% input$region,
          year   >= input$year_range[1],
          year   <= input$year_range[2]
        )
    })

  })
}

NS(id)가 핵심입니다. ns("region")"id-region" 같은 고유 문자열을 반환하므로, 같은 모듈을 여러 번 써도 ID가 겹치지 않습니다.

모듈을 조립하는 app.R

# 새 파일: modular-app/app.R
library(shiny)
library(bslib)
library(tidyverse)
library(plotly)

source("R/filter_module.R")

# 샘플 데이터
set.seed(42)
regions  <- c("서울", "부산", "대구", "인천", "광주")
pop_data <- expand.grid(region = regions, year = 2018:2023) |>
  mutate(population = as.integer(runif(n(), 200000, 10000000)))

# ── UI ─────────────────────────────────────────────────────
ui <- page_fluid(
  theme = bs_theme(bootswatch = "flatly"),
  h2("모듈 대시보드"),

  layout_columns(
    col_widths = c(6, 6),

    # 같은 모듈을 두 번 — id만 다름
    card(
      card_header("패널 A"),
      filterUI("filter_a", regions),   # id = "filter_a"
      plotlyOutput("chart_a")
    ),
    card(
      card_header("패널 B"),
      filterUI("filter_b", regions),   # id = "filter_b"
      plotlyOutput("chart_b")
    )
  )
)

# ── Server ─────────────────────────────────────────────────
server <- function(input, output, session) {

  # 각 모듈 서버 호출 — 반응형 데이터를 반환받음
  data_a <- filterServer("filter_a", pop_data)
  data_b <- filterServer("filter_b", pop_data)

  output$chart_a <- renderPlotly({
    p <- data_a() |>
      ggplot(aes(x = year, y = population, fill = region)) +
      geom_col(position = "dodge") +
      theme_minimal()
    ggplotly(p)
  })

  output$chart_b <- renderPlotly({
    p <- data_b() |>
      ggplot(aes(x = region, y = population, fill = region)) +
      geom_col() +
      theme_minimal() +
      theme(legend.position = "none")
    ggplotly(p)
  })
}

shinyApp(ui, server)

filterUI("filter_a", ...), filterServer("filter_a", ...)처럼 같은 id를 UI와 Server에 동시에 전달해야 연결이 됩니다.

모듈 간 통신

모듈 서버에서 반응형 값을 반환하면 부모 서버가 받아서 다른 모듈에 전달할 수 있습니다.

# 모듈 A가 반환한 데이터를 모듈 B가 입력으로 받는 구조
data_a    <- filterServer("filter_a", pop_data)
chart_out <- chartServer("chart_b", data = data_a)   # 반응형을 그대로 전달

모듈 서버 함수가 data 인자로 반응형을 받을 때는 data() 형태로 호출해야 합니다.

chartServer <- function(id, data) {
  moduleServer(id, function(input, output, session) {
    output$plot <- renderPlotly({
      df <- data()   # 반응형 호출
      ...
    })
  })
}

프로젝트 구조 권장안

modular-app/
├── app.R            ← UI·Server 조립만 담당
├── global.R         ← 데이터·패키지 로드
└── R/
    ├── filter_module.R
    ├── chart_module.R
    └── table_module.R

모듈 파일을 R/ 폴더에 모아두면 source() 대신 shiny::loadSupport()가 자동으로 불러옵니다 (Shiny 1.5 이상).