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 이상).