library(shiny)
library(bslib)
# UI
<- page_navbar(
ui title = "TEST PAGE",
theme = bs_theme(version = 5),
nav_panel(
title = "1번 탭"
)
)
# Server
<- function(input, output, session){}
server
# 앱 실행
shinyApp(ui = ui, server = server)
Shiny Application 만들어보기
R과 Shiny로 만드는 웹 애플리케이션: 탐구학습용 학습도구의 개발
1. Shiny란 무엇인가요? 🤔
Shiny는 R 언어로 웹 앱을 손쉽게 만드는 멋진 도구입니다. 여기서 앱(Application)은, 아래의 I-P-O 구조를 갖는다는 뜻입니다.
Input (입력): 사용자가 조작할 수 있는 버튼, 슬라이더, 텍스트 상자 같은 위젯이에요.
Processing (처리): 입력값에 따라 자동으로 계산이나 분석을 해 주는 ‘뇌’ 역할이에요.
Output (출력): 처리된 결과를 화면에 보여주는 그래프, 표, 텍스트 등입니다.
이렇게 세 부분이 상호 연결되어, 사용자가 값을 바꾸면 서버가 재빠르게 계산해서 결과를 갱신해 줍니다.
2. Shiny App의 구조
I-P-O 중, 우리 눈에 보이는 부분은 Input과 Output입니다. 그리고 Process는 눈에 보이지 않고, 내부적으로 작동하죠. 눈에 보이는 부분을 Shiny에서는 ui
(user interface)라고 하는 객체에 할당합니다. 그리고 눈에 보이지 않는 Process는 server
라는 객체에 할당하죠. 할당이 끝나면 shinyApp(ui, server)
를 입력해 애플리케이션을 작동시킵니다.
UI 객체: input & output
server 객체: Process
실행:
ShinyApp(ui, server)
3. UI 만들기 🎫
먼저 우리는 bslib
패키지가 제공하는 page_navbar()
함수로 UI를 만들 것입니다. 첫째, page_navbar()
안에 여러 개의 nav_panel()
을 사용하면, 상단 네비게이션 바에 탭(페이지)을 손쉽게 추가할 수 있어요.
nav_panel(title = "탭 제목", ...)
형태로,title
에 문자열을 지정하면 해당 탭이 생성됩니다.
3.2. fluidRow와 column으로 행과 열 나누기 📐
fluidRow()
와 column()
을 사용하면, 화면을 행(row) 단위로 나누고, 그 안에서 열(column) 폭을 12단계 그리드로 설정해 세부 레이아웃을 조정할 수 있습니다.
column(width = 6, ...)
는 전체 폭의 절반을 의미합니다.
여러 column()
을 한 행에 배치하면, 원하는 비율로 콘텐츠를 배치할 수 있어요.
library(shiny)
library(bslib)
# UI
<- page_navbar(
ui title = "TEST PAGE",
theme = bs_theme(version = 5),
nav_panel(
title = "1번 탭",
sidebarLayout(
sidebarPanel(
h4("입력값")
),mainPanel(
fluidRow(
column(
width = 6,
p("출력값 1")
),column(
width = 6,
p("출력값 2")
)
)
)
)
)
)
# Server
<- function(input, output, session){}
server
shinyApp(ui, server)
3.3. 통합 예제 코드
아래는 탭 3개를 가진 기본 앱으로, 각 탭에서 화면 구성을 다르게 보여 줍니다.
library(shiny)
library(bslib)
<- page_navbar(
ui title = "나의 Shiny 앱",
theme = bs_theme(version = 5),
# 1) 홈 탭: 간단한 텍스트
nav_panel(
title = "홈",
p("홈 페이지입니다. 여기에 앱 소개나 설명을 넣어 보세요.")
),
# 2) 데이터 탭: fluidRow + column 사용
nav_panel(
title = "데이터",
fluidRow(
column(width = 6,
h4("왼쪽 열"),
p("이곳에 그래프나 테이블을 넣을 수 있습니다.")
),column(width = 6,
h4("오른쪽 열"),
p("이곳에는 설명 문구나 다른 출력 요소를 배치해 보세요.")
)
)
),
# 3) 분석 탭: 사이드바 + 메인 영역
nav_panel(
title = "분석",
sidebarLayout(
sidebarPanel(
sliderInput(
inputId = "num",
label = "숫자를 선택하세요:",
min = 1,
max = 10,
value = 5
)
),mainPanel(
verbatimTextOutput(outputId = "square")
)
)
)
)
<- function(input, output, session) {
server # 분석 탭의 출력: 입력한 숫자의 제곱 계산
$square <- renderText({
outputpaste0("입력값: ", input$num,
" → 제곱: ", input$num^2)
})
}
# 앱 실행
shinyApp(ui = ui, server = server)
실습 방법
- 위 코드를
app.R
에 복사+붙여넣기 하고 저장하기Run App
을 눌러 실행- 상단 탭을 클릭해 각 레이아웃(텍스트, 사이드바, 다중 열)이 제대로 표시되는지 확인하기
- 글자 바꿔보면서 구조 파악하기
이제 nav_panel()
으로 탭을 만들고, 각 탭 안에서 다양한 레이아웃 함수를 활용하는 방법을 익혔습니다.
4. UI와 Server의 연결
Shiny 앱의 핵심은 UI에서 입력된 값이 서버를 거쳐 결과로 돌아오는 과정이에요. 이 챕터에서는 그 과정을 4단계로 나누어 아주 쉽게 설명해 볼게요.
4.1. 입력 위젯에서 ID 부여받기
UI 코드 예시:
sliderInput(inputId = "obs", label = "관측치 개수:", min = 1, max = 100, value = 50)
첫 번째 인수(
"obs"
)가 바로 ID예요.
이 ID 덕분에 서버가 사용자가 슬라이더에서 고른 값을 알 수 있어요.
library(shiny)
library(bslib)
# UI
<- page_navbar(
ui title = "TEST PAGE",
theme = bs_theme(version = 5),
nav_panel(
title = "1번 탭",
sidebarLayout(
sidebarPanel(
h4("입력값"),
sliderInput(inputId = "obs",
label = "관측치 개수:",
min = 1,
max = 100,
value = 50)
),mainPanel(
fluidRow(
column(
width = 6,
p("출력값 1")
),column(
width = 6,
p("출력값 2")
)
)
)
)
)
)
# Server
<- function(input, output, session){}
server
shinyApp(ui, server)
4.2. 서버에서 입력값 읽기: input$id
이제 Server를 한 번 건드려보겠습니다. UI에서 “obs”라는 id를 부여받은 입력값은, Server에서 input$obs
로 인식되게 됩니다.
서버 함수 안에서 다음과 같이 입력값을 가져옵니다:
$obs input
input$obs
는 사용자가 슬라이더를 움직일 때마다 자동 갱신되는 리액티브 값이에요.
4.3. 처리 함수 거쳐서 output$y
만들기
이제 입력값인
input$obs
를 가지고 server는 어떤 처리(process)를 진행합니다.예를 들어 관측값에 3을 곱하거나, 10을 더할 수 있겠죠?
server는
render*()
함수로 결과를 계산하고, 이 결과를output
객체에 저장해요.예시:
$multiply <- renderText({
output$obs*3
input })
renderText()
안에input$obs
를 사용해 곱셈을 하고,- 그 결과를
multiply
라는 이름(output$multiply
)으로 저장합니다.
4.4. UI에서 출력 위젯으로 보여주기
이제 Server에서 계산한 output 값을 다시 UI로 보내주어야 합니다.
이 때, Server에서 계산한 값의 형태에 따라, UI에서 대응되는 함수가 조금씩 달라집니다. 예를 들어, 문자 형태라면
textOutput()
이라는 함수를 UI에 써줍니다.textOutput("multiply")
그림 형태라면
plotOutput()
이라는 함수를, 표 형태라면tableOutput()
이라는 함수를 써주어야 합니다.
library(shiny)
library(bslib)
# UI
<- page_navbar(
ui title = "TEST PAGE",
theme = bs_theme(version = 5),
nav_panel(
title = "1번 탭",
sidebarLayout(
sidebarPanel(
h4("입력값"),
sliderInput(inputId = "obs",
label = "관측치 개수:",
min = 1,
max = 100,
value = 50)
),mainPanel(
fluidRow(
column(
width = 6,
textOutput("multiply")
),column(
width = 6,
textOutput("add")
)
)
)
)
)
)
# Server
<- function(input, output, session){
server $multiply <- renderText({
output$obs*3
input
})$add <- renderText({
output$obs+10
input
})
}
shinyApp(ui, server)
4.5. 전체 흐름 요약
- UI:
sliderInput("obs", ...)
→ ID =obs
부여 - Server 입력:
input$obs
로 값 읽기 - Server 처리:
output$distPlot <- renderPlot({ … })
으로 결과 저장 - UI 출력:
plotOutput("distPlot")
로 화면에 표시
이 4단계를 이해하면, Shiny 앱의 기본 메커니즘을 탄탄하게 잡을 수 있습니다.
참고!
UI에서 Input을 만드는 함수는 sliderInput
외에도 여러 가지가 있습니다. 또한 Server에서 Render 함수의 종류에 상응하는 UI에서의 출력 함수가 정해져 있습니다. 예를 들어, Server에서 그림, 즉 Plot을 만들기 위해서는 Server에서 renderPlot()
함수를 써야하며, UI에서는 plotOutput()
함수를 써야 합니다. 아래의 표를 참고해보세요!
UI 입력 위젯 | 서버 (input → render → output) | UI 출력 위젯 |
---|---|---|
sliderInput("obs", "관측치 개수:", 1,100,50) |
output$distPlot <- renderPlot({ hist(rnorm(input$obs)) }) |
plotOutput("distPlot") |
textInput("caption", "제목 입력:", "안녕하세요") |
output$captionText <- renderText({ input$caption }) |
textOutput("captionText") |
selectInput("species", "종 선택:", choices) |
output$speciesTable <- renderTable({ subset(data, species==input$species) }) |
tableOutput("speciesTable") |
numericInput("num", "숫자 입력:", 10, step=1) |
output$numPrint <- renderPrint({ input$num }) |
verbatimTextOutput("numPrint") |
dateInput("date", "날짜 선택:", Sys.Date()) |
output$dateText <- renderText({ format(input$date, "%Y-%m-%d") }) |
textOutput("dateText") |
아래의 코드를 보고, UI에서 입력 데이터를 어떻게 부여하는지, 이것이 Server에서 어떻게 프로세스를 거쳐 결과값으로 바뀌는지, 마지막으로 그 결과값이 어떻게 다시 UI로 출력되는지 살펴보시기 바랍니다. 시간을 10분 정도 드릴테니, 꼼꼼하게 뜯어보시기 바랍니다.
library(shiny)
library(bslib)
library(ggplot2)
library(dplyr)
# UI
<- page_navbar(
ui title = "TEST PAGE",
theme = bs_theme(version = 5),
nav_panel(
title = "1번 탭",
sidebarLayout(
sidebarPanel(
h4("입력값"),
sliderInput(inputId = "obs",
label = "관측치 개수:",
min = 1,
max = 100,
value = 50),
textInput("name", "이름 입력:", "홍길동"),
selectInput("brand",
"제조사를 고르세요:",
unique(mpg$manufacturer)),
radioButtons("class", "종류를 고르세요:",
unique(mpg$class))
),mainPanel(
fluidRow(
column(
width = 6,
textOutput("multiply")
),column(
width = 6,
tableOutput("table")
)
),fluidRow(
column(
width = 6,
textOutput("hello")
),column(
width = 6,
plotOutput("graph")
)
)
)
)
)
)
# Server
<- function(input, output, session){
server $multiply <- renderText({
output$obs*3
input
})$add <- renderText({
output$obs+10
input
})$hello <- renderText({
outputpaste0("안녕하세요, 제 이름은 ", input$name, "입니다.")
})$graph <- renderPlot({
output|> filter(manufacturer == input$brand) |>
mpg ggplot(aes(x=cty, y=hwy))+
geom_point() +
theme_bw()
})$table <- renderTable({
output|>
mpg filter(class == input$class) |>
head(10) |>
select(-c(trans, year, displ, fl, class))
})
}
shinyApp(ui, server)
5. 예제
이제 앞서 배웠던 위성영상 데이터 분석을 적용해보겠습니다.
library(shiny)
library(bslib)
library(terra)
library(sf)
library(dplyr)
# SHP Seoul File
<- st_read("data/SIGUN.shp", options = "ENCODING=CP949") |>
seoul filter(SG1_NM == "서울특별시")
# Mask Raster File
<- rast("data/landsat_seoul.tif") |>
bands_stack mask(seoul)
# UI
<- page_navbar(
ui title = "Landsat Image Composite",
theme = bs_theme(version = 5),
nav_panel(
"Composite",
sidebarLayout(
sidebarPanel(
h4("Select Bands for Composite"),
selectInput(
inputId = "red_band",
label = "Red Channel",
choices = names(bands_stack),
selected = "B4_Red"
),selectInput(
inputId = "green_band",
label = "Green Channel",
choices = names(bands_stack),
selected = "B3_Green"
),selectInput(
inputId = "blue_band",
label = "Blue Channel",
choices = names(bands_stack),
selected = "B2_Blue"
)
),
mainPanel(
plotOutput("composite_map", height = "600px"),
)
) # nav_panel 닫기
),
nav_panel(
"Threshold",
sidebarLayout(
sidebarPanel(
h4("Threshold Range"),
selectInput(
inputId = "sel_band",
label = "Select the Band",
choices = names(bands_stack),
selected = "B4_Red"
),sliderInput(
inputId = "threshold",
label = "Value Range",
min = 0,
max = 20000,
value = 100,
step = 100
)
),
mainPanel(
plotOutput("NDVI_masked", height = "600px")
)# sidebarLayout 닫기
)
) # page_navbar 닫기
)
# Server 정의
<- function(input, output, session) {
server
# 선택한 밴드로 합성 래스터 생성
<- reactive({
composite_raster <- c(input$red_band,
sel $green_band,
input$blue_band)
input
bands_stack[[sel]]
})
<- reactive({
select_raster <- input$sel_band
selc
bands_stack[[selc]]
})
# Leaflet에 RGB 또는 다중 밴드 합성 결과 그리기
$composite_map <- renderPlot({
output<- composite_raster()
comp plotRGB(comp, r = 1, g = 2, b = 3,
stretch = "lin")
})
# NDVI 또는 첫 번째 밴드 마스크 후 플롯
$NDVI_masked <- renderPlot({
outputreq(input$sel_band, input$threshold)
<- select_raster()
rstr <- terra::mask(
mask_r
rstr,< input$threshold,
rstr maskvalues=TRUE)
plot(mask_r,
col = rev(terrain.colors(10)),
main="Masked Raster",
axes=FALSE,
box=FALSE)
})
}
# 앱 실행
shinyApp(ui = ui, server = server)
6. 마치며…
이른 시간 안에 Shiny를 배우는 것은 사실 어려운 일입니다. 그러나 ChatGPT 등의 AI 도구가 발전함에 따라, 간단한 코딩은 정말 간단한 일이 되고 있습니다. 바이브 코딩(Vibe Coding)이 최근 화제가 되고 있는 이유이기도 합니다.
그러나 AI가 멋진 결과물을 만들어주기 위해서는, 질문자의 사전 지식도 매우 중요합니다. 얼마나 구체적인 질문을 하느냐에 따라 답변의 퀄리티가 완전히 달라지기 때문입니다.
오늘 학습 내용(원격탐사에 대한 이론적 지식, R, Quarto, Shiny 등의 기술적 지식)을 통해, 선생님들께서는 훌륭한 질문을 할 수 있는 사전 지식을 쌓았을 것이라고 믿습니다. 이를 바탕으로, 지금부터는 각자의 조로 이동해 집단 지성과 AI, 그리고 멘토의 도움을 통해 멋진 학습 도구를 만들어보시기 바랍니다!