library(shiny)
ui <- fluidPage(
titlePanel("Old Faithful Geyser Data"),
sidebarLayout(
sidebarPanel(
sliderInput(inputId = "bins",
label = "Number of bins:",
min = 1,
max = 50,
value = 30)
),
mainPanel(
plotOutput(outputId = "distPlot")
)
)
)
server <- function(input, output) {
output$distPlot <- renderPlot({
x <- faithful[, 2]
bins <- seq(min(x), max(x), length.out = input$bins + 1)
hist(x, breaks = bins, col = 'darkgray', border = 'white',
xlab = 'Waiting time to next eruption (in mins)',
main = 'Histogram of waiting times')
})
}
shinyApp(ui = ui, server = server)Lab_11: Shiny를 활용한 웹 앱 제작
개요
여기서는 웹 앱 개발 도구로서의 Shiny에 대해 배운다. Shiny는 서버-기반의 웹 앱을 개발하기 위한 프레임워크를 제공한다. 원래 R 패키지로 개발되었으나 이제는 R과 Python 모두를 위한 웹 앱 개발 도구로 발전하고 있다.
Shiny를 배우기 위한 다양한 리소스가 존재한다. 가장 중요한 리소스는 Shiny for R 홈페이지이다. Get Started를 살펴본 후 Gallery의 다양한 예제를 살펴보는 것으로 시작하는 것이 좋다. 이 홈페이지의 내용만으로도 Shiny의 기본 구조와 문법을 익히기에 충분하다.
좀 더 종합적으로 Shiny을 이해하려면 다음의 웹 북을 활용할 수 있다. 첫 번째가 바이블이라고 할 수 있다.
1 기본 동작
1.1 기본 구조
Shiny는 크게 세 부분으로 나뉘어 진다(그림 1).
첫째, UI(혹은 UX) 부분이다. 프론트엔트(front-end) 부분으로 입력을 받고, Server에서 산출된 출력을 표출하는 부분이다.
둘째, Server 부분이다. 백엔드(back-end) 부분으로 UI에서 받은 입력에 바탕으로 출력을 산출하는 부분이다.
셋째, 결합 및 실행 부분이다. UI와 Server 부분을 결합해 웹 앱을 실행하는 부분이다. 결합 및 실행은 단일 파일 속에 UI와 Server를 함께 다루는 방식과, 두 개의 파일로 분할하여 다루는 방식으로 나뉜다. 여기서는 전자를 중심으로 설명한다.
1.2 Shiny 프로젝트의 생성
여기서는 RStudio에서 Shiny 프로젝트를 생성하는 방법에 대해 배운다. New Project > New Directory > Shiny Application을 선택한다. 그림 2 에서 Directory name과 Subdirectory의 위치를 설정한다.
이렇게 하면 Shiny 프로젝트가 생성되고, 자동적으로 app.R이라는 스크립트 파일이 생성된다.
1.3 웹 앱의 생성
app.R에서 스크립트 윈도우 오른쪽 상단에 위치한 Run App 버튼을 클릭하면 그림 3 과 같은 웹 앱이 새로운 윈도우에 생성된다. 왼편의 슬라이더를 움직이면 오른편의 히스토그램이 변한다는 점을 확인한다. 즉 빈(bin)의 개수를 달리하면서 데이터 분포를 탐색해보기 위한 단순한 웹 앱이 만들어진 것이다.
이 단순한 웹 앱을 살펴본다. 다음과 같은 점을 확인할 수 있다.
첫째, 크게 두 부분으로 구성되어 있는데, 왼편의 슬라이더가 있는 부분과 오른편의 히스토그램이 있는 부분으로 나뉜다. 부차적으로 상단에 제목(“Old Faithful Geyser Data”)이 있는 부분도 존재한다. 이 모든 것이 UI를 구성한다. 위에서 UI는 입력을 받고 출력을 표출한다고 했다. 왼편의 슬라이더바가 입력을 받는 부분이고, 오른편의 플롯 영역이 출력을 표출하는 부분이다.
둘째, 사용자의 입력은 빈(bin)의 갯수이고, Server는 이 값을 바탕으로 히스토그램이라는 출력을 생성한다. 생성된 출력은 UI로 이동하여 표출된다. 사용자의 입력값이 바뀌면 이 과정이 반복되고 히스토그램이 바뀌게 된다.
2 Shiny의 기본 문법 체계
이제 그림 3 의 웹 앱을 생성한 코드를 살펴보도록 한다. 코드 속에는 다양한 설명이 포함되어 있는데 그것을 제거하고 코드만 남기면 다음만 남는다. 대충 보아도 문법이 tidyverse와는 많이 다르다는 점을 금방 알 수 있다. 따라서 shiny의 문법 체계는 따로 익힐 수 밖에 없다.
코드를 자세히 살펴보자. 우선 shiny라는 R 패키지를 불러와야 한다. 코드가 ui, server, shinyApp의 세 부분으로 나뉘어져 있다는 것을 알 수 있고, 이것이 위에서 설명한 UI 부분, Server 부분, 결합 및 실행 부분을 담당한다는 점을 쉽게 이해할 수 있다.
2.1 UI 부분
UI 부분은 다양한 함수들이 위계 구조를 이루고 있는데, 전체 형태를 결정하는 UI 구조 함수(컨테이너 함수)가 입력 함수와 출력 함수를 둘러싸고 있는 구조이다. 그림 4 는 이것을 요약한 것이다.
첫째, 컨테이너 함수가 위계 구조를 이루고 있다. 최상위 fluidPage() 함수는 titlePanel()과 sidebarLayout()로 구성되고, sidebarLayout()은 다시 sidebarPanel()과 mainPanel()로 구성된다. 하나씩 위계적으로 살펴보면 다음과 같다.
-
fluidPage(): UI의 최상위 컨테이너로서 전체 UI 구조를 관장하며, 브라우저 화면 크기에 따라 자동으로 늘어나고 줄어드는 반응형 레이아웃을 제공한다. 이러한 특성 때문에 ’fluid’라는 이름이 붙었다. 고정 폭을 가진 레이아웃은fixedPage()를 통해 정의할 수 있다. Shiny 앱에서는 기본적으로fluidPage()를 사용한다고 생각하면 된다.titlePanel(): 제목이 들어가는 부분이다. 생략가능하다.-
sidebarLayout(): UI의 핵심 부분으로, 가장 전형적인 “사이드바 + 메인 영역” 레이아웃을fluidPage()내에 생성할 수 있게 해준다.sidebarPanel(): 입력을 받는 부분으로, 입력 함수 혹은 입력 위젯(widget)을 담는 컨테이너이다.mainPanel(): 출력을 표출하는 부분으로, 서버에서 생성된 출력 객체가 UI에 표시될 수 있도록 출력 함수를 담는 컨테이너이다.
둘째, sidebarPanel() 내의 sliderInput()은 특정한 입력 함수 혹은 입력 위젯 생성 함수이다. 여기서는 슬라이더 형태의 입력을 받기 때문에 sliderInput() 함수가 사용된 것이다. 입력의 형태에 따라 다양한 입력 함수가 존재하며, 일반적으로 *Input() 형식을 띤다. 뒤에서 자세히 다룬다. 개별 함수는 상호작용형 인터페이스 컴포넌트로 미리 만들어져 있기 때문에 위젯이라고 부른다. sliderInput()은 다양한 인수로 구성되어 있는데, 가장 중요한 것이 inputId 인수이다. 입력 함수는 위젯을 통해 입력값을 받고 그 입력값은 Server의 input 객체의 한 요소가 되는데 inputId 인수는 input 객체에서 해당 요소를 참조할 ’이름’을 지정한다.
셋째, mainPanel() 내의 plotOutput()은 특정한 출력 함수 혹은 출력 바인딩(binding) 함수이다. 이것은 Server에서 전달받은 출력 즉 output 객체의 요소를 표출한다. 바인딩 함수라고 부르는 것은 출력 요소와 브라우저의 특정 UI 영역을 ’연결(바인드)’하기 때문이다. 출력의 형태가 플롯(plot)이기 때문에 plotOutput() 함수가 사용된 것이다. 출력의 형태에 따라 다양한 출력 표출 함수가 존재하며, 일반적으로 *Output()의 형식을 띤다. 뒤에서 자세히 다룬다. plotOutput() 함수의 가장 중요한 인수는 outputId로 표출할 output 객체의 요소의 ’이름’을 지정한다. plotOutput(outputId = "distPlot")는 서버에서 만들어진 "distPlot"라는 이름의 플롯(output 객체의 한 요소)을 해당 위치에 표출한다는 의미이다.
결국 UI는 입력을 받고 출력을 표출하는 두 가지 일을 하는데, 전자는
sliderInput()과 같은 입력 함수를 통해 이루어지며, 후자는plotOutput()과 같은 출력 함수를 통해 이루어진다. 이들 함수는 출력과 입력의 형식에 따라 일반적으로*Input()과*Output()의 형태를 띤다.
2.2 Server 부분
Server 부분은 다음과 같은 구조를 가지고 있다.
첫째, Server 부분은 반드시 함수로 정의되며, 일반적으로 function(input, output){} 혹은 function(input, output, session){}의 형식을 갖는다(input과 output은 필수이지만 session은 부가적으로 정의될 수 있다). 반드시 함수로 정의되어야 하는 것은 input과 output(부가적으로 session)을 인수로 받아, 입력 변화에 따라 반응형 출력이 생성 및 갱신되는 로직을 정의하기 때문입니다.
둘째, 출력물은 renderPlot({})과 같은 렌더링 함수를 통해 이루어진다. 이렇게 생성된 출력은 ’이름’이 지정되어 Server의 output 객체의 한 요소로 저장된다. 예를 들어, 코드에서 output$distPlot은 생성된 출력에 "distPlot"이라는 이름을 부여하고 이를 output 객체에 해당 이름으로 저장함을 의미한다. 이 이름은 UI의 출력 함수 plotOutput(outputId = "distPlot")에서 사용되며, 이를 통해 해당 출력이 UI에 표출된다.
셋째, 출력의 형태가 플롯이기 때문에 renderPlot({}) 함수가 사용된 것이다. 출력의 형태에 따라 다양한 렌더링 함수가 존재하며, 일반적으로 render*({})의 형식을 띤다. 뒤에서 자세히 다룬다.
넷째, 입력 함수로부터 전달되어 Server의 input 객체의 요소로 저장된 입력값은 렌더링 함수 내부에서 사용된다. 예를 들어, seq(min(x), max(x), length.out = input$bins + 1)에서 sliderInput() 함수로부터 전달된 bins라는 입력값(즉, input 객체의 한 요소)이 seq() 함수 내부에서 사용된다. 입력값이 달라짐에 따라 히스토그램의 모양이 달라지는 이유가 여기에 있다.
결국 Server는 UI에서 전달된 입력에 기반하여 출력을 산출한다. 구체적으로,
input$*형식의 입력값을 Server 내부에서 사용하여renderPlot({})과 같은 렌더링 함수를 통해 출력을 생성하고, 그 결과를output$*형식으로 저장함으로써 UI에 표출될 수 있도록 한다.
2.3 결합 및 실행 부분
UI와 server 부분은 결합되어 하나의 Shiny 웹 앱을 구성하며, 이를 실행하기 위해 shinyApp() 함수가 사용된다. shinyApp(ui = ui, server = server)에서 볼 수 있듯이, shinyApp() 함수에 UI 부분과 Server 부분을 지정함으로써 Shiny 웹 앱이 생성되고 실행된다.
이것은 단일 파일 방식으로 결합 및 실행을 행하는 것이다. 콘솔창에서 실행하고자 한다면 다음과 같이하면 된다. 파일명에 app.R과 같은 UI와 Server 부분이 모두 포함된 파일의 이름을 지정하면 된다.
runApp("파일명")만일 UI와 Server 부분을 다른 파일로 구성하는 분리 파일 방식인 경우는 다음과 같이 진행한다. 우선 UI 부분의 파일을 따로 만든다. 예를 들어 ui.R이라는 스크립트 파일을 다음과 같이 생성한다. 단일 파일 방식과 달리 UI 부분을 ui 객체에 할당하지 않아도 된다.
library(shiny)
fluidPage(
titlePanel("Old Faithful Geyser Data"),
sidebarLayout(
sidebarPanel(
sliderInput(inputId = "bins",
label = "Number of bins:",
min = 1,
max = 50,
value = 30)
),
mainPanel(
plotOutput(outputId = "distPlot")
)
)
)마찬가지로 Server 부분의 파일을 따로 만든다. 예를 들어 server.R이라는 스크립트 파일을 다음과 같이 생성한다. 단일 파일 방식과 달리 Server 부분을 server라는 객체에 할당하지 않아도 된다.
콘솔 창에서 다음과 같이 실행한다. 이때 지정한 폴더에는 UI 부분을 정의한 파일(예: ui.R)과 server 부분을 정의한 파일(예: server.R)이 포함되어 있어야 한다. 이름은 반드시 ui.R과 server.R이어야 한다.
runApp("폴더명")위의 두 가지 방식을 결합한 제3의 길도 있다. 우선 위의 분리 파일 방식과 같이 UI와 Server 부분에 대한 파일을 개별적을 생성한다. 차이점은 반드시 ui와 server 객체를 할당하는 방식으로 이러우져야 한다는 점이다. my_ui.R의 내용은 다음과 같다. 이 방식에서는 UI 부분의 파일 이름이 반드시 ui.R일 필요가 없다.
ui <- fluidPage(
titlePanel("Old Faithful Geyser Data"),
sidebarLayout(
sidebarPanel(
sliderInput(inputId = "bins",
label = "Number of bins:",
min = 1,
max = 50,
value = 30)
),
mainPanel(
plotOutput(outputId = "distPlot")
)
)
)my_server.R의 내용은 다음과 같다. 마찬가지로 Server 부분의 파일의 이름이 server.R일 필요가 없다.
server <- function(input, output) {
output$distPlot <- renderPlot({
x <- faithful[, 2]
bins <- seq(min(x), max(x), length.out = input$bins + 1)
hist(x, breaks = bins, col = 'darkgray', border = 'white',
xlab = 'Waiting time to next eruption (in mins)',
main = 'Histogram of waiting times')
})
}그리고 나서 단일 파일 방식과 같이 app.R 파일을 통해 결합한다. app.R 파일의 내용은 다음과 같다.
UI 파일과 Server 파일을 분리하는 방식은 코드가 매우 복잡할 때 사용하는 것으로 보통은 단일 파일 방식을 사용한다.
3 입력 함수와 출력 함수의 종류
3.1 입력 함수
앞에서 언급한 것처럼, 입력 함수 혹은 입력 위젯 생성 함수의 종류는 다양하다. R Shiny Components 웹페이지 기준으로 대략 25개 정도이다. 그림 5 는 그 중 일부를 보여주고 있다.
몇 가지 주목할 사항이 있다.
첫째, 모든 함수의 이름이 *Input() 형식인 것은 아니다. 특히 버튼 형식의 경우 *Button() 혹은 *Buttons() 형식을 띤다. 이 외에 *Link(), *switch()로 끝나는 함수도 있다.
둘째, 입력 함수의 인수가 중요하다. 입력값에 이름을 부여하는 inputID 인수와 입력 위젯에 나타날 설명 글귀를 지정하는 label 인수는 공통이다. 그러나 나머지 인수는 입력 함수에 따라 달라진다. 위의 예에서 sliderInput() 입력 함수는 min, max, value라는 추가적인 인수를 갖는데, min과 max는 슬라이더의 최대 및 최소값을, value는 기본값을 지정하는 인수이다. 입력 함수별 인수에 대한 자세한 사항은 R Shiny Components 웹페이지를 참조한다.
3.2 출력 함수와 렌더링 함수
출력 함수는 서버에서 생성된 출력을 UI에서 표출해주는 함수인데 반해, 렌더링 함수는 서버에서 실질적으로 출력을 생성하는 함수이다. 출력 함수와 렌더링 함수 모두는 출력의 형식(플롯, 테이블, 텍스트, 이미지 등)에 따라 다양하기 때문에, 기본적으로 두 함수는 쌍을 이룰 수 밖에 없다.
다음은 다섯 가지 기본 쌍을 보여준다. 출력 함수는 *Output()의 형식을, 렌더링 함수는 render*({})의 형식을 취한다.
| 형식 | 출력 함수(UI) | 렌더링 함수(Server) |
|---|---|---|
| 플롯 | plotOutput(outputID = "") |
renderPlot({}) |
| 테이블 | tableOutput(outputID = "") |
renderTable({}) |
| 일반 텍스트 | textOutput(outputID = "") |
renderText({}) |
| 사전 포맷된 텍스트 | verbatimeTextOutput(outputID = "") |
renderPrint({}) |
| 이미지 | imageOutput(outputID = "") |
renderImage({}) |
그런데 특히 주요 시각화 패키지들은 Shiny 웹 앱 구축을 염두에 두고 개별적인 출력 함수와 렌더링 함수를 제공하고 있다. 중요한 패키지에 대해 이를 정리하면 다음과 같다. 모두 패키지명Output()형식과 render패키지명({})의 형식을 띤다.
| 패키지 | 출력 함수(UI) | 렌더링 함수(Server) |
|---|---|---|
| DT | DTOutput(outputID = "") |
renderDT({}) |
| reactable | reactableOutput(outputID = "") |
renderReactable({}) |
| plotly | plotlyOutput(outputID = "") |
renderPlotyl({}) |
| echarts4r | echarts4rOutput(outputID = "") |
renderEcharts4r({}) |
| leaflet | leafletOutput(outputID = "") |
renderLeaflet({}) |
| tmap | tmapOutput(outputID = "") |
renderTmap({}) |
Shiny 웹 앱에서의 활용을 염두에 둔 패키지들은 모두 이러한 출력 함수의 쌍을 제공하고 있다. 따라서 출력 함수는 굉장히 많을 수 있다. R Shiny Components 웹페이지는 기본 5개의 함수쌍에 주요 패키지를 포함하는 12개 정도의 출력 함수쌍을 예시로 제시하고 있다.
4 bslib 패키지를 활용한 현대적 UI 구조 디자인
4.1 개요
위에서 Shiny의 전체 레이아웃은 위계적으로 구성된 UI 구조 함수, 즉 컨테이너 함수들에 의해 결정된다고 했다. fluidPage(), titlePanel(), sidebarLayout(), sidebarPanel(), mainPanel() 과 같은 컨테이너 함수를 통해 그림 3 에 나타나 있는 것과 같은 외향의 UI 구조가 완성된 것이다.
보다 현대적인 UI 구조 디자인을 위해 등장한 것이 bslib 패키지이다. bslib 패키지는 Bootstrap을 기반으로 Shiny와 R Markdown을 위한 현대적인 UI 도구 모음을 제공한다. bslib패키지는 다양하고 유연한 컨테이너 함수를 제공할 뿐만 아니라, 카드(card), 밸류 박스(value box), 사이드바(sidebar) 등 재사용 가능한 UI 컴포넌트(콘텐츠 구성요소)를 통해 Shiny 웹 앱과 문서를 효율적으로 구성할 수 있게 한다(전통적인 Shiny에 부족한 부분). 또한 테마 시스템을 활용하여 웹 앱과 문서의 외관을 유연하게 사용자 정의하고, 이를 실시간으로 조정할 수 있다. 나아가 최신 Bootstrap 및 Bootswatch를 지원함으로써, 기본적으로 Bootstrap 3에 의존하는 기존 Shiny 및 R Markdown 환경에 비해 보다 현대적이고 일관된 웹 UI 구현을 가능하게 한다.
bslib 패키지를 이용해 좀 더 현대적인 감각의 웹 앱을 생성하기 위해서는 bslib 패키지에서 제공되는 다양한 UI 컨테이너 함수와 UI 컴포넌트 함수를 익혀야 하며, 뒤에서 자세히 다룬다. 여기서는 bslib 패키지를 통해 그림 3 웹 앱이 어떻게 달라지는지에 대해서만 살펴본다.
Shiny 패키지는 기본적으로 11개의 예제 웹 앱을 제공한다. 다음과 같이 실행해 본다.
이 들 중 첫 번째 예제인 “01_hello”가 그림 3 에 나타나 있는 웹 앱이다. 해당 예제를 실행하려면 다음과 같이 하면 된다.
runExample("01_hello")아래의 그림 6 와 같은 웹 앱이 생성된다. 이것을 그림 3 과 비교해 보라. 조금 다르다는 것을 쉽게 알아 챌 수 있는데(왼편 슬라이더바에서 < 를 눌러보라.), 이것이 bslib 패키지를 활용하여 제작된 웹 앱이다.
웹 앱 뿐만 아니라 코드도 살펴보려면 다음과 같이 실행하면 된다.
runExample("01_hello", display = "showcase")삽입되어 있는 설명 부분을 제거하고 코드만 남기면 다음과 같다. 이것을 그림 3 의 코드와 비교해 보라. 오로지 UI 부분의 코드만 다르고, 그것도 UI 함수명만 달라져 있다는 것을 알 수 있다(입력 함수, 출력 함수, 렌더링 함수는 그대로이다). 결국 bslib 패키지는 UI 구조만 바꾼다.
library(shiny)
library(bslib)
ui <- page_sidebar(
title = "Hello Shiny!",
sidebar = sidebar(
sliderInput(
inputId = "bins",
label = "Number of bins:",
min = 1,
max = 50,
value = 30
)
),
plotOutput(outputId = "distPlot")
)
server <- function(input, output) {
output$distPlot <- renderPlot({
x <- faithful$waiting
bins <- seq(min(x), max(x), length.out = input$bins + 1)
hist(
x,
breaks = bins,
col = "#75AADB",
border = "white",
xlab = "Waiting time to next eruption (in mins)",
main = "Histogram of waiting times"
)
})
}
shinyApp(ui = ui, server = server)첫째, page_sidebar()가 최상위 컨테이너 함수이다. page_sidebar()는 bslib에서 가장 널리 사용되는 페이지 컨테이너 함수로, 전체 너비를 차지하는 헤더(제목)와 사용자 입력을 위한 사이드바를 갖춘 대시보드형 UI 구조를 손쉽게 구성할 수 있도록 설계되었다. page_sidebar()함수는 전통적인 Shiny 컨테이너 함수 중 fluidPage(), titlePanel(), sidebarLayout() 함수의 역할을 동시에 수행한다고 생각할 수 있다.
둘째, page_sidebar() 의 여러 인수가 전통적인 titlePanel() 함수와 sidebarLayout()의 하위 함수인 sidebarPanel() 함수를 대신한다.
title: 대시보드의 제목을 설정할 수 있으며,titlePanel()함수와 동일한 기능을 한다.sidebar = sidebar():sidebarPanel()함수와 동일한 기능을 한다.sidebarPanel()함수 속에 입력 함수인siderInput()함수가 들어 있었던과 동일하게sidebar()속에siderInput()함수가 들어 있다.
셋째, 전통적인 Shiny의 mainPanel() 함수 없이 출력 함수(plotOutput())가 막바로 사용된다. 그러나 이것은 가장 단순한 방식으로, 대표적인 컴포넌트 함수인 card() 함수를 사용하면 보다 풍성한 옵션을 사용할 수 있다. card() 함수 내부에서 card_header()와 card_body()라는 컴포넌트 함수를 활용한다. 아래는 위의 코드에 대해 두 가지 부가적인 조치를 한 것이다. 첫째, card() 함수를 통해 출력 표출 부분을 보완한다. 둘째, 기본 플롯 함수 대신 ggplot2 패키지를 통해 히스토그램을 작성하고 그것을 plotly 패키지의 ggplotly() 함수를 통해 인터랙티브 플롯을 생성한다. plotly 패키지의 출력 생성 및 출력 표출을 위해 renderPlotly({})와 plotlyOutput() 함수가 활용되는 점에 주목한다.
library(shiny)
library(bslib)
library(tidyverse)
library(plotly)
ui <- page_sidebar(
title = "Hello Shiny!",
sidebar = sidebar(
sliderInput(
inputId = "bins",
label = "Number of bins:",
min = 1,
max = 50,
value = 30
)
),
card(
card_header("ggplot2: Histogram"),
card_body(
plotlyOutput(outputId = "distPlot")
)
)
)
server <- function(input, output) {
output$distPlot <- renderPlotly({
x <- faithful[, 2]
gg <- ggplot() +
geom_histogram(
aes(x = x), bins = input$bins, fill = 'darkgray', color = 'white'
) +
labs(
x = 'Waiting time to next eruption (in mins)',
y = "Frequencies",
title = 'Histogram of waiting times'
)
ggplotly(gg)
})
}
shinyApp(ui = ui, server = server)4.2 컨테이너 함수
컨테이너 함수란 다른 UI 요소를 담되, 그 자체가 시각적 콘텐츠의 주제가 되지 않고 공간, 전환, 계층 구조를 정의하는 함수이다. 컨테이너 함수는 크게 세 가지로 나뉜다.
페이지 컨테이너: 페이지 전체의 폭, 높이, 스크롤 방식 등 최상위 UI 구조를 정의하는 컨테이너로,
page_*()형태를 띤다.레이아웃 컨테이너: 페이지 내부에서 여러 UI 요소를 동시에 어떻게 배치할지를 결정하는 컨테이너로,
layout_*()형태를 띤다.내비게이션 컨테이너: 여러 UI 패널을 묶어 사용자 상호작용에 따라 표시되는 콘텐츠를 전환하는 컨테이너로,
navset_*()의 형태를 띤다.
분류별 컨테이너 함수를 정리하면 표 3 과 같다. 자세한 사항은 bslib 패키지 홈페이지의 Get Started나 Layouts를 참고할 수 있다.
| 분류 | 함수 | 특징 |
|---|---|---|
| 페이지 컨테이너(기본) |
고정형(고정폭: 940픽셀) 유동형(웹페이지의 전체 폭) 대시보드형(웹페이지의 전체 폭/높이) |
|
| 페이지 컨테이너(확장) |
표준형(전역적 사이드바 포함) 다중 페이지, |
|
| 레이아웃 컨테이너 |
|
|
|
내비게이션 컨테이너 (페이지/레이아웃 레벨) |
nav_panel()의 집합 |
|
|
내비게이션 컨테이너 (카드 레벨) |
nav_panel()의 집합 |
우선 단일 페이지의 구조를 결정하는 가장 기본적인 페이지 컨테이너 함수에 page_fixed(), page_fluid(), page_fillable() 가 있다. page_fixed()는 중앙 정렬된 고정 폭 페이지 컨테이너이고, page_fluid()는 브라우저 폭에 따라 콘텐츠 영역이 유동적으로 변화는 페이지 컨테이너이고, page_fillable()는 브라우저의 가시 영역을 높이까지 포함해 채우도록 설계된 페이지 컨테이너이다. 그림 7 는 이 세가지 페이지 컨테이너를 개념적으로 비교하고 있다.
그러나 가장 중요한 페이지 컨테이너 함수는 page_sidebar()로 bslib 패키지의 표준이다. 이것은 페이지 전체를 채우는 구조 위에 사이드바-메인 영역 분할을 기본으로 제공하는 고수준 페이지 컨테이너이다. 단순히 예기하면 page_sidebar()는 page_fillable()과 레이아웃 컨테이너인 layout_siderbar()를 결합한 것으로 이해할 수 있다. page_navbar()는 상단 내비게이션 바를 페이지의 기본 구조로 표함하여, 여러 화면을 전환하는 앱을 구성하는 고수준 페이지 컨테이너이다. page_navbar()는 개념적으로 페이지 컨테이너와 내비게이션 컨테이너(navbar)을 통합한다.
레이아웃 컨테이너는 페이지 전체가 아니라 페이지 내부 배치를 담당하는데, 페이지 컨테이너와 달리 중첩 사용이 가능하지만 콘텐츠 자체를 표현하지는 않는다. layout_siderbar()는 사이드바와 메인 콘텐츠 영역으로 화면을 이분하는 레이아웃 컨테이너를 생성하는데 page_*(), card(), nav_panel() 내부 어디에도 존재할 수 있다. layout_columns()는 여러 UI 요소를 동일 행 내의 다중 컬럼으로 배치하는 레이아웃 컨테이너를 생성한다. 컬럼의 수와 폭을 명시적으로 제어할 수 있으며 정돈된 레이아웃 구현에 용이하다. layout_column_wrap()은 컬럼을 행 단위로 자동 줄바꿈하여 반응형 배치를 제공하는 레이아웃 컨테이너이다. 컬럼 폭 기준으로 자동 배치하며 화면 크기에 따라 행 수가 동적으로 변한다.
내비게이션 컨테이너는 여러 UI 패널을 묶어 사용자 상호작요에 따라 표시되는 콘텐츠를 전환하는 컨테이너로, 페이지 컨테이너나 레이아웃 컨테이너의 내부에서 사용된다. 한 번에 하나의 패널만 활성화되며, 공간 배치가 아니라 상태 전환을 담당한다. navset_tab(), navset_pill(), navset_underline()는 모두 nav_panel()들의 집합으로 구성되며, 콘텐츠 전환의 방식이 다를 뿐이다. navset_tab()는 전통적인 탭 모양을, navset_pill()는 둥근 필(pill) 모양의 버튼을, navset_underline()는 밑줄을 그은 형태를 제공한다. navset_card_tab(), navset_card_pill(), navset_card_underline()은 동일한 기능을 하지만 card 스타일을 기본으로 내장한 컨테이너이다. navset_*() 함수가 페이지 또는 레이아웃의 주요 내비게이션과 같은 비교적 상위 UI 계층에서 사용되는데 반해 navset_card_*() 함수는 주로 콘텐츠 블록 내부에 위치하여 카드 수준의 국소적 전환에 사용된다.
4.3 UI 컴포넌트: 콘텐츠 구성 요소
UI 컴포넌트는 UI 구조를 정의하지 않고, 컨테이너 내부에서 정보와 기능을 하나의 의미 있는 시각적 단위로 표현하는 재사용 가능한 UI 구성 요소이다. 컨테이너가 아니므로 구조에 관여하지 않으며, 사용자가 하나의 UI 플록으로 인식하는 부분이다. 컨테이너가 아니므로 페이지나 레이아웃 구조에 직접 관여하지 않으며, 사용자가 하나의 UI 블록으로 인식하는 부분을 이룬다. 이러한 컴포넌트는 여러 위치에서 반복 사용될 수 있고, 시각적 스타일과 의미를 함께 지닌다는 특징이 있다. bslib에서 제공하는 대표적인 콘텐츠 컴포넌트로는 카드(card), 사이드바(sidebar), 밸류 박스(value box), 툴팁(tooltip), 팝오버(popover) 등이 있다. 보다 자세한 내용은 bslib 패키지 공식 홈페이지의 Components 섹션을 참고할 수 있다.
4.3.1 카드
카드(card)는 현대적인 UI에서 가장 널리 사용되는 정보 구성 단위 중 하나이다. 기본적으로 카드는 테두리와 여백을 갖는 직사각형 컨테이너에 불과하지만, 관련된 정보를 의미 있게 묶어 제시할 경우 사용자가 내용을 보다 쉽게 이해하고, 집중하며, 탐색할 수 있도록 돕는다. 이러한 특성 때문에 카드는 대시보드와 UI 설계에서 핵심적인 역할을 하며, 대부분의 성공적인 대시보드 및 UI 프레임워크는 카드를 주요 컴포넌트로 채택하고 있다. card() 함수는 다음과 같은 컴포넌트 함수를 갖는다.
card_header(): 제목card_body(): 주 콘텐츠(생략 가능: 바로 출력 표출 함수 표시 가능)card_footer(): 부가 설명
4.3.2 사이드바
사이드바(sidebar)는 필터, 설정, 기타 입력 요소를 사용자가 쉽게 접근할 수 있도록 제공하는 핵심 UI 컴포넌트로, 이들이 제어하는 대화형 기능과 나란히 배치된다는 점이 특징이다. 사이드바를 활용하면 사용자 입력과 결과 출력 간의 관계를 명확히 할 수 있어, 대시보드의 사용성과 탐색성이 크게 향상된다. bslib에서는 page_sidebar()와 page_navbar() 함수를 통해 페이지 수준(page-level)의 사이드바 레이아웃을 기본적으로 제공하며, 이 외에도 다양한 형태의 사이드바 레이아웃을 지원하여 인터페이스의 목적과 복잡도에 따라 유연한 UI 설계를 가능하게 한다.
사이드바 컴포넌트와 사이드바 레이아웃은 다르다. 사이드바 레이아웃은 공간을 나누는 구 조를 의미한다면, 사이드바 컴포넌트는 콘텐츠를 담는 UI 블록이다. 입력 위젯, 텍스트, 메뉴 등이 포함되며 독립적인 UI가 의미가 있다. 사이드바 레이아웃의 중요 함수에는 앞에서 살펴본 것과 같은 page_sidebar(), layout_sidebar() 등이 있고, 사이드바 컴포넌트의 대표적인 함수에 sidebar()가 있다.
bslib에서 제공하는 사이드바 레이아웃은 크게 부유형(floating), 채움형(filling), 다중 페이지/탭형(multi-page/tab)의 세 가지로 구분할 수 있다.
부유형 레이아웃은 layout_sidebar()를 사용하는데, 페이지의 어느 위치에나 배치할 수 있는 사이드바 레이아웃을 구성할 수 있다. 이 방식은 의미적으로 관련된 입력 요소와 출력 요소를 시각적으로 함께 묶어 제시하는 데 적합하다. 또한 card()와 함께 사용하면 전체 화면 확장(full-screen), 헤더·푸터 추가 등 카드가 제공하는 다양한 기능을 활용할 수 있다.
채움형 레이아웃은 page_sidebar()를 사용하는데 페이지 전체를 채우는 사이드바 레이아웃을 생성한다. 내부적으로 page_sidebar()는 page_fillable()과 layout_sidebar()를 단순히 감싼(wrapper) 함수에 불과하다. 이 구조를 이해하면, 하나의 채움형 레이아웃 안에 여러 개의 사이드바 레이아웃을 유연하게 배치할 수 있는 가능성이 열린다.
다중 페이지/탭 레이아웃은 page_navbar()나 navset_card_tab() 내부에서 sidebar 인수를 사용해 사이드바를 구성할 수 있다. 이 경우 사이드바는 페이지 전체를 채우는 동시에, 모든 페이지나 탭에서 동일하게 유지되어 표시된다. 이후에는 페이지별로 서로 다른 레이아웃을 구성하는 방법도 다루겠지만, 모든 페이지에서 동일한 사이드바를 사용하는 것이 바람직한 경우도 많다. 이러한 경우에는 conditionalPanel()을 활용하여 특정 페이지에서만 사이드바의 일부 내용을 표시하거나 숨길 수 있다.
4.3.3 밸류 박스
밸류 박스(value box)는 대시보드에서 핵심적인 수치나 지표를 한눈에 전달하기 위해 사용되는 UI 컴포넌트이다. 값(value), 설명(label), 아이콘(icon) 등을 함께 제시하여 사용자가 현재 상태나 주요 변화를 빠르게 파악할 수 있도록 돕는다. bslib의 value_box() 함수는 카드(card) 기반 설계를 따르므로 다른 UI 요소들과 시각적으로 일관되게 어우러지며, Shiny의 반응성과 결합해 값이 동적으로 갱신되는 대시보드를 구성하는 데 특히 유용하다.
value_box() 함수는 네 가지 주요 구성 요소로 이루어진다.
value는 표시하고자 하는 핵심 수치나 텍스트 값으로, 대시보드에서 가장 중요한 정보를 전달한다. 출력 표출 함수를 사용할 수도 있다.
title은 value 위에 선택적으로 표시되는 설명 텍스트로, 해당 값의 의미나 지표명을 명확히 한다.
showcase는 아이콘이나 기타 UI 요소와 같이 value 옆에 함께 배치되는 시각적 요소로, 정보를 보다 직관적으로 인식하도록 돕는다.
bs_icon()컴포넌트 함수를 주로 사용한다.theme은 값 상자의 색상이나 스타일을 조정하는 선택적 설정으로, 대시보드의 전체 디자인과 시각적 일관성을 유지하거나 강조 효과를 줄 수 있다.
4.3.4 툴팁과 팝오버
툴팁(tooltip)과 팝오버(popover)는 화면을 방해하지 않으면서 추가 정보를 제공하거나 상호작용을 가능하게 하는 유용한 UI 요소이다. 툴팁은 주로 간단한 설명을 표시하는 데 사용되며, 팝오버는 보다 풍부한 정보 제공이나 사용자 입력을 포함한 상호작용을 지원한다. 예를 들어, card_header()에 “도움말” 아이콘과 함께 tooltip()을 부착하면 사용자가 시각화된 데이터에 대한 추가 설명을 손쉽게 확인할 수 있다. 또한 동일한 헤더 영역에 “설정” 아이콘과 popover()를 연결하면 시각화의 매개변수를 직접 제어할 수 있는 인터페이스를 제공할 수 있다. 더 나아가 card_footer()의 링크에 popover()를 적용하면 추가 정보의 표시뿐 아니라 하이퍼링크와 같은 상호작용 요소를 함께 제공할 수 있어, 정보 탐색과 사용자 경험을 동시에 향상시킨다.
구현 방식 측면에서 보면, 툴팁과 팝오버는 매우 유사하다. 두 컴포넌트 모두 사용자의 상호작용을 통해 표시 여부가 전환되는 트리거(trigger) UI 요소와, 화면에 표시될 메시지 내용을 필요로 한다. tooltip()과 popover()는 공통적으로 첫 번째 인수를 트리거로 취급하며, 이름이 지정되지 않은 나머지 인수들은 메시지 내용으로 사용된다. 또한 popover()의 경우에는 선택적으로 제목(title)을 함께 지정할 수 있다.
반면, 사용자 경험(UX)과 활용 목적 측면에서는 두 컴포넌트의 성격이 뚜렷이 다르다. 툴팁은 포커스나 마우스 오버(hover)에 의해 표시되는 반면, 팝오버는 클릭을 통해 열리고 닫힌다. 이로 인해 팝오버는 툴팁보다 화면에 더 오래 유지되는, 즉 상대적으로 지속성이 높은 인터페이스이며, 추가적인 사용자 상호작용이 필요한 경우에 적합하다. 따라서 간단한 설명과 같은 읽기 전용(read-only) 정보에는 툴팁을 사용하는 것이 바람직하고, 메시지 자체와의 상호작용이 필요한 경우에는 팝오버를 사용하는 것이 적절하다.
4.4 사용자 정의 테마
bslib 패키지의 또 다른 중요한 기능은 다양한 사용자 정의 테마(custom themeing)의 적용을 용이하게 한다는 점이다. bslib는 Bootstrap 5를 기반으로 한 테마 시스템을 추상화하여, 사용자가 색상, 타이포그래피, 여백, 컴포넌트 스타일 등 핵심 디자인 요소를 일관된 방식으로 제어할 수 있도록 지원한다. 특히 bs_theme()를 중심으로 한 테마 정의 방식은 개별 UI 요소를 일일이 스타일링하는 접근에서 벗어나, 디자인 전반을 하나의 체계적인 설정으로 관리할 수 있게 해준다. 이를 통해 인터랙티브 대시보드나 웹 애플리케이션의 시각적 일관성을 유지하면서도, 목적과 사용자 맥락에 맞는 다양한 테마를 손쉽게 적용할 수 있다. 이러한 사용자 정의 테마 기능은 bslib를 단순한 UI 구성 도구를 넘어, 재현 가능하고 확장 가능한 시각적 설계 도구로 자리매김하게 하는 핵심 요소라 할 수 있다.
4.4.1 bs_theme() 함수
bs_theme() 함수는 bslib 패키지에서 사용자 정의 테마를 정의하는 핵심 함수로, Bootstrap 기반 UI의 시각적 전반을 하나의 설정 객체로 통합 관리한다. 즉, bs_theme() 함수는 색상, 글꼴, 여백, 컴포넌트 스타일 등 UI 전반의 디자인 규칙을 한 번에 정의하고 이를 Shiny 앱 전체에 일관되게 적요하도록 해주는 테마 객체 생성 함수이다.
bs_theme() 함수의 주요 인수는 다음과 같다.
version = 5: 사용할 Bootstrap 버전으로 5가 기본값bg = "#ffffff",fg = "#212529": 기본 배경색 및 기본 텍스트 색상primary = "#0d6efd",secondary = "",success = "",info = "",warning = "",danger = "": Bootstrap의 의미 기반 색상 체계base_font = font_google("Noto Sans KR"),heading_font = font_google("Noto Serif KR"),code_font = font_google("JetBrains Mono"): 본문 텍스트, 제목, 코드 블록의 타이포그래피 일관성 확보base_font_size = "1rem": 기본 글자 크기로 rem 단위 사용이 권장 된다.spacer = "1rem": 여백의 기준 단위border_radius = "0.5rem": 카드, 버튼 등의 모서리 둥글기bootswatch = "flatly": 미리 정의된 Bootswatch 테마
4.4.2 테마 미리보기 및 검증
bslib 패키지의 bs_theme_preview() 함수는 사용자 정의 테마를 실제 UI 구성 요소에 적용해 보며 시각적 효과를 즉시 확인할 수 있는 테마 미리보기 및 검증 도구이다.
콘솔에서 함수를 실행하면, 버튼, 카드, 입력 위젯 등 대표 UI 컴포넌트에 테마를 적용해볼 수 있고, 색상 대비, 타이포그래피, 여백 등을 한눈에 점검할 수 있을 뿐만 아니라, 테마 수정의 결과를 즉시 확인해 볼 수 있다. 그림 8 에서 볼 수 있는 것처럼 오른편의 Theme customizer를 조작하여 이러한 기능을 활용해 볼 수 있다.
5 반응성 함수
5.1 정의
Shiny의 반응성(reactivity)이란, 입력값의 변화에 따라 관련된 계산과 출력이 자동으로 재실행ㆍ갱신되는 메커니즘을 의미한다. 좀 더 기술적으로 표현하면 반응성은 입력(input)의 변화가 의존 관계(dependency)를 따라 전파되어, 반응형 표현식과 출력이 자동으로 갱신되는 실행 모델이다.
반응성 함수란, 입력값에 대한 의존성을 추적하고 입력 변화 시 자동으로 재실행되는 함수를 말한다. 따라서 Shiny에서 반응성 함수는 공통적으로 다음 특징을 갖는다.
input$*에 의존입력이 바뀌면 자동으로 재실행
사용자가 직접 호출하지 않음
Shiny의 반응성 그래프(dependency graph)에 의해 관리됨
5.2 종류
Shiny의 반응성 함수는 역할에 따라 크게 세 부류로 나눌 수 있다.
5.2.1 렌더링 함수: render* 계열
렌더링 함수는 반응형 계산을 통해 출력물을 생성하고, 그 결과를 output 객체의 요소로 저장하는 함수이다. 일반 형식은 다음과 같다.
render*({
# 반응형 코드
})반드시 output$이름 <- render*({ ... }) 형태로 사용되며, 출력의 “최종 종착점”이며, UI의 *Output()과 1:1로 대응된다. 예시는 다음과 같다.
output$distPlot <- renderPlot({
hist(x, breaks = input$bins)
})
5.2.2 반응형 표현식 함수: reactive({})
reactive({})는 입력값에 따라 달라지는 계산 결과를 반응형 객체로 생성하는 함수이다. 입력을 받아 ’반응성 표현식(reactive expression)’을 생성한다. 여기서 반응성 표현식이란 단순히 최종 산출물(값이나 객체)만을 의미하지 않고, 그 산출물을 관리하는 메커니즘(종속성 추적, 지연 실행, 캐싱 등) 포함하는 동적인 객체를 의미한다. 따라서 반드시 함수 형태로 표기한다.
하나의 입력이 여러 개의 출력 함수(출력 생성 함수)와 결부되는 경우에 주로 사용되는데 동일한 입력의 중복 사용을 회피하기 위해 해당 입력을 반응성 표현식으로 전환하여 다수의 출력 생성 함수에 입력으로 투입할 수 있다.
일반 형식은 다음과 같다.
r <- reactive({
# 반응형 계산
})출력이 아니라 중간 계산 결과를 생성하며, 반환값은 반응형 객체가 되고, 값에 접근할 때는 함수 형식으로 반드시 () 를 사용해야 한다. 예시는 다음과 같다.
입력 함수를 통해 받은 input$bins를 읽어 bins라는 새로운 반응형 객체를 생성하고, 그것을 출력 생성 함수에서 투입하는 방식을 보여주고 있다. 반응형 객체는 반드시 bins()처럼 함수 형식으로 호출해야만 한다.
그런데, 여기서는 사실 reactive({}) 함수가 반드시 필요한 것은 아니다. 즉, 다음과 같이 해도 무방하다.
bins <- seq(min(x), max(x), length.out = input$bins + 1)
output$distPlot <- renderPlot({
hist(x, breaks = bins)
})그러나 출력 생성 함수가 여러 개 사용되고 거기에 bins가 여러 번 사용된다면 위와 같이 하는 것이 훨씬 효율적이다.
5.2.3 반응형 관찰자 함수: observe*() 계열
반응형 관찰자 함수는 입력 변화에 반응하여 특정 동작(side effect)을 수행하는 함수이다. 대표적인 함수는 다음과 같다.
eventReactive()(중간적 성격)
특징은 값을 반화하지 않고, 출력 생성이 목적도 아니다. 주로 상태 변경, 메시지 출력, UI 업데이트에 사용된다. 다음에 예시가 있다.
observeEvent(input$go, {
cat("버튼이 눌렸습니다\n")
})입력 함수 actionButton(inputID = "go", ...)로부터 버튼을 누를 때 마다 "go"라는 입력 요소의 값(input$go)이 1씩 증가하고, input$go값이 변할 때마다(즉, 버튼이 눌릴 때마다) Server에서 { } 속의 내용을 실행한다. 위의 예시의 경우 버튼을 클리할 때마다 콘솔에 “버튼이 눌렸습니다”가 출력된다.
좀 더 현실적인 예시를 제시하면 다음과 같다. 버튼 클릭시 계산을 실행하게 할 수 있다.
observeEvent(input$go, {
result <- heavy_calculation()
print(result)
})UI 요소를 업데이트하게 할 수 있다.
observeEvent(input$go, {
updateSliderInput(session, "bins", value = 20)
})파일을 저장하게 할 수 있다.
observeEvent(input$save, {
write.csv(data, "result.csv")
})사실 observeEvent() 함수는 observe() 함수와 매우 유사하다. 아래는 동일하게 작동한다.
그러나 observeEvent()는 이벤트 중심이고, 의존성을 명확히 분리하며, 가독성과 안전성을 높이는 장점이 있다. 따라서 버튼 처리에는 observeEvent()가 권장된다.
6 웹 앱의 공유
6.1 웹 앱 배포(deployment)
웹 앱을 배포하는 방법은 다양할 수 있지만 여기서는 shinyapp.io를 이용하는 방법에 대해 배운다.
첫째, 해당 홈페이지(https://www.shinyapps.io/)에 접속한다.
둘째, 계정을 생성한다.
로그인 후 상단 오른쪽 끝 메뉴 “Tokens” 페이지로 이동
페이지에서 Show를 통해 인증 토큰(Token)과 Secret 확보: Show secret를 클릭하여 secret까지 보이게 한 후 복사하기
셋째, RStudio에 토큰을 설정한다. 단 한 번만 하면 된다.
- R 콘솔에 위에서 복사한 내용 붙여넣기 및 실행
넷째, Publish 버튼 클릭
스크립트 윈도우의 오른쪽 상단에 있는 Publish 버튼 클릭
배포를 원하는 R 파일만 선택
다섯째, URL을 얻는다. 다음의 형식으로 생성된다.
- https://userid.shinyapps.io/project_name/
6.2 Quarto와 Shiny의 결합
상황: Quarto 대시보드의 일부 영역에서 Shiny 사용
-
두 가지 접근
-
Shiny 중심 방식: Shiny 웹 앱의 생성
Quarto 대시보드 내에서 Quarto 문법으로 Shiny 웹 앱 구축
Quarto 대시보드 전체가 Shiny 웹 앱화: shinyapps.io를 통해 배포
-
Quarto 중심 방식: Quarto 대시보드의 유지
Quarto 대시보드 속에 독립적으로 구축된 Shiny 웹 앱 임베딩
여전히 Quarto 대시보드: Quarto Pub을 통해 배포
-
6.2.1 Shiny 중심 방식
-
옵션 1: Quarto를 Shiny 웹 앱의 레이아웃 설정 도구로 사용
-
shiny 패키지:
fluidPage()-
titlePanel(),SidebarLayout(),sidebarPanel(),mainPanel()
-
-
bslib 패키지:
page_sidebar()-
sidebar = sidebar(),card()
-
-
옵션 2: Quarto 대시보드 내에서 Shiny 웹 앱 구축
https://quarto.org/docs/dashboards/interactivity/shiny-r.html
YAML 헤더에 다음 첨가:
server: shiny-
ui 부분: 기본적으로는
r코드 청크 속에 포함, 사이드바와 메인 병렬사이드바: 레이아웃 구성요소에 넣고
{.sidebar}CSS 클래스 지정 가능메인: 사이드바와 다른 레이아웃 구성요소에 넣기 가능
-
server 부분:
r코드 청크 속에 포함, 다음 지정-
#| context: server지정
-
-
기타(패키지, 데이터 등) 부분:
r코드 청크 속에 포함, 다음 지정#| context: setup#| include: false
6.2.2 Quarto 중심 방식
보통 하나의 row에 임베딩
src=""부분만 교체
<iframe src="https://..." loading="lazy" style="width: 100%; height: 600px; border: 0px none;" allow="web-share; clipboard-write"></iframe>







