공공데이터 탐색 대시보드
공공 데이터를 활용해 지역과 기간을 고르면 차트와 테이블이 동시에 바뀌는 대시보드를 완성합니다. 이 챕터는 앞서 배운 selectInput, reactive(), renderPlotly, renderDT를 하나의 앱으로 엮는 실전 연습입니다.
완성 앱 미리보기
사이드바에서 지역과 연도를 선택하면 오른쪽 메인 패널의 막대 차트와 데이터 테이블이 즉시 갱신됩니다. 데이터는 행정안전부 공개 인구 통계 CSV를 가상으로 모사한 샘플을 사용합니다.
[ 사이드바 ] [ 메인 패널 ]
지역 선택 ┌──────────────────┐
○ 서울 │ plotly 막대 차트 │
○ 부산 └──────────────────┘
○ 대구 ┌──────────────────┐
│ DT 데이터 테이블 │
연도 범위 └──────────────────┘
2018 ~ 2023
프로젝트 구조
앱을 하나의 app.R로 작성합니다. 실무에서는 ui.R / server.R로 분리하지만, 이 챕터는 전체 흐름을 한눈에 보는 것이 목적이므로 단일 파일로 구성합니다.
public-dashboard/
└── app.R
패키지 설치
install.packages(c("shiny", "bslib", "tidyverse", "plotly", "DT"))
샘플 데이터 생성
실제 CSV 파일 대신 앱 내부에서 재현 가능한 샘플 데이터를 만듭니다.
# 새 파일: public-dashboard/app.R
library(shiny)
library(bslib)
library(tidyverse)
library(plotly)
library(DT)
# ── 샘플 데이터 ────────────────────────────────────────────
set.seed(42)
regions <- c("서울", "부산", "대구", "인천", "광주", "대전", "울산")
years <- 2018:2023
pop_data <- expand.grid(region = regions, year = years) |>
mutate(
population = as.integer(runif(n(), min = 200000, max = 10000000)),
birth_rate = round(runif(n(), min = 3.5, max = 9.0), 2),
death_rate = round(runif(n(), min = 4.0, max = 8.0), 2)
)
# ── UI ─────────────────────────────────────────────────────
ui <- page_sidebar(
title = "공공데이터 탐색 대시보드",
theme = bs_theme(bootswatch = "flatly"),
sidebar = sidebar(
selectInput(
inputId = "region",
label = "지역 선택",
choices = regions,
selected = "서울",
multiple = TRUE
),
sliderInput(
inputId = "year_range",
label = "연도 범위",
min = 2018,
max = 2023,
value = c(2018, 2023),
sep = ""
),
selectInput(
inputId = "metric",
label = "지표 선택",
choices = c(
"인구수" = "population",
"출생률" = "birth_rate",
"사망률" = "death_rate"
),
selected = "population"
)
),
layout_columns(
col_widths = 12,
card(
card_header("지역별 추이"),
plotlyOutput("trend_chart", height = "350px")
),
card(
card_header("원본 데이터"),
DTOutput("data_table")
)
)
)
# ── Server ─────────────────────────────────────────────────
server <- function(input, output, session) {
# 필터링된 데이터 — 두 출력이 공유
filtered <- reactive({
pop_data |>
filter(
region %in% input$region,
year >= input$year_range[1],
year <= input$year_range[2]
)
})
# plotly 차트
output$trend_chart <- renderPlotly({
req(nrow(filtered()) > 0)
p <- filtered() |>
ggplot(aes(
x = year,
y = .data[[input$metric]],
fill = region,
text = paste0(
"지역: ", region, "\n",
"연도: ", year, "\n",
"값: ", format(.data[[input$metric]], big.mark = ",")
)
)) +
geom_col(position = "dodge") +
scale_x_continuous(breaks = 2018:2023) +
labs(
x = "연도",
y = switch(input$metric,
population = "인구수",
birth_rate = "출생률",
death_rate = "사망률"
),
fill = "지역"
) +
theme_minimal()
ggplotly(p, tooltip = "text")
})
# DT 테이블
output$data_table <- renderDT({
filtered() |>
arrange(region, year) |>
rename(
지역 = region,
연도 = year,
인구수 = population,
출생률 = birth_rate,
사망률 = death_rate
) |>
datatable(
options = list(
pageLength = 10,
lengthMenu = c(5, 10, 20),
searching = TRUE,
scrollX = TRUE
),
rownames = FALSE
) |>
formatCurrency("인구수", currency = "", interval = 3, mark = ",", digits = 0)
})
}
shinyApp(ui, server)
핵심 패턴 정리
reactive()로 데이터 공유하기
filtered()를 한 번만 정의했지만 renderPlotly와 renderDT 두 곳에서 동시에 씁니다. 필터 조건이 바뀌면 두 출력이 함께 갱신됩니다.
filtered <- reactive({
pop_data |>
filter(region %in% input$region, ...)
})
output$chart <- renderPlotly({ filtered() |> ... })
output$table <- renderDT({ filtered() |> ... })
reactive() 없이 input$region을 양쪽에서 직접 쓰면, 필터 로직이 중복되고 미묘한 타이밍 차이가 생길 수 있습니다.
req()로 빈 선택 방어하기
selectInput에서 multiple = TRUE를 쓰면 아무것도 선택하지 않을 수 있습니다. 이때 req(nrow(filtered()) > 0)이 실행을 멈춰 빈 차트 오류를 막습니다.
.data[[input$metric]] 패턴
ggplot2에서 변수 이름을 문자열로 전달할 때 .data[[...]]를 씁니다. aes_string()은 구식 방법이므로 이 패턴을 기억해두세요.
앱 실행
RStudio에서 app.R을 열고 "Run App" 버튼을 클릭합니다. 터미널에서 실행하려면 아래 명령어를 씁니다.
shiny::runApp("public-dashboard")
지역을 여러 개 선택하면 차트에 색상별 막대가 나란히 표시되고, 테이블은 선택한 지역과 기간으로 자동 필터링됩니다. 이 구조가 앞으로 만들 모든 대시보드의 뼈대입니다.