R Shiny 기본 문법 정리: reactive부터 layout까지 초보자용 실전 가이드

R Shiny를 처음 배우는 사람을 위해 ui와 server 구조, reactive 계열 함수, observeEvent, eventReactive, reactiveVal, 레이아웃, 동적 UI까지 단계별로 정리한 입문 가이드입니다.
데이터·통계
저자

Ikmyungterran

공개

2026년 3월 8일

0. 이 글은 무엇을 해결해 주는가

R Shiny를 처음 접하면 보통 두 가지에서 막힙니다. 첫째는 uiserver가 어떻게 연결되는지 감이 안 잡히는 문제이고, 둘째는 reactive(), observeEvent(), eventReactive()처럼 이름이 비슷한 함수들이 각각 언제 필요한지 헷갈리는 문제입니다.

이 글은 제공된 정리 노트를 바탕으로, Shiny 초보자가 실제로 앱을 구성할 때 자주 마주치는 개념을 한 흐름으로 다시 풀어 쓴 입문 가이드입니다. 단순히 함수 목록을 나열하지 않고, “입력값을 받고”, “계산을 재사용하고”, “버튼을 눌렀을 때만 실행하고”, “화면 구성을 바꾸는” 순서로 설명합니다.

R Shiny 로고

이 글에서 다루는 핵심 범위는 다음과 같습니다.

  • ui, server, shinyApp()의 기본 구조
  • reactive(), isolate(), observe(), observeEvent(), eventReactive()
  • reactiveVal()reactiveValues()
  • renderUI()uiOutput(), updateSelectInput() 같은 업데이트 계열 함수
  • tags$..., CSS, fluidRow(), sidebarLayout(), tabsetPanel(), navbarPage() 같은 레이아웃 문법
  • JavaScript와 연결하는 Shiny.setInputValue()

1. R Shiny를 한 문장으로 이해하면

R Shiny는 R 코드로 웹 애플리케이션을 만드는 프레임워크입니다. 사용자가 슬라이더를 움직이거나 버튼을 누르면, 그 입력이 server로 전달되고, 서버는 다시 그래프나 표 같은 결과를 갱신해서 ui에 보여줍니다.

아주 거칠게 말하면 구조는 아래와 같습니다.

구성 요소 역할 초보자 관점에서 이해할 포인트
ui 사용자가 보는 화면 버튼, 슬라이더, 텍스트 상자, 그래프 위치를 정의
server 뒤에서 실행되는 로직 입력값을 받아 계산하고 결과를 생성
shinyApp(ui, server) 앱 실행 앞의 두 조각을 하나로 묶어 실제 앱으로 실행

Shiny를 처음 배울 때는 “웹 개발”이라고 해서 너무 어렵게 생각하기 쉽지만, 실제로는 입력값(input) -> 계산 -> 출력(output) 흐름을 반응형으로 연결하는 것이 핵심입니다.


2. 가장 작은 Shiny 앱부터 시작하기

아래 예제는 가장 기본적인 Shiny 앱입니다. 슬라이더로 막대 수를 바꾸면 히스토그램이 바로 다시 그려집니다.

library(shiny)

ui <- fluidPage(
  titlePanel("첫 번째 Shiny 앱"),
  sliderInput("num", "표본 개수", min = 10, max = 500, value = 100),
  plotOutput("hist")
)

server <- function(input, output, session) {
  output$hist <- renderPlot({
    hist(rnorm(input$num))
  })
}

shinyApp(ui, server)

Shiny 기본 예제 실행 화면

이 코드에서 꼭 이해해야 할 연결은 세 가지입니다.

  1. sliderInput("num", ...)이 만들어내는 값은 input$num으로 읽습니다.
  2. plotOutput("hist")output$hist <- renderPlot({...})와 연결됩니다.
  3. input$num이 바뀌면 renderPlot() 안의 코드가 다시 실행됩니다.

즉, Shiny의 기본 학습은 사실상 “어떤 UI 위젯이 어떤 input$...를 만들고, 어떤 출력 함수가 어떤 render...()와 연결되는가”를 익히는 과정입니다.


3. input과 output은 어떻게 연결되는가

Shiny는 UI에서 만든 입력 위젯이 input$이름으로 들어오고, 출력 자리표시자가 output$이름으로 연결됩니다.

UI 함수 서버에서 읽는 값 출력 함수 서버 쪽 렌더 함수
sliderInput("num", ...) input$num plotOutput("hist") renderPlot()
textInput("title", ...) input$title textOutput("msg") renderText()
selectInput("x", ...) input$x tableOutput("tbl") renderTable()
uiOutput("dynamic_ui") 동적으로 생성됨 uiOutput("dynamic_ui") renderUI()

Shiny 문법이 처음 어렵게 느껴지는 이유는 HTML처럼 화면을 그리는 코드와, R처럼 계산하는 코드가 함께 등장하기 때문입니다. 하지만 실제 연결 규칙은 매우 단순합니다.

  • UI에서 "num"이라고 이름을 붙이면 서버에서는 input$num
  • UI에서 "hist"라고 출력 자리를 만들면 서버에서는 output$hist

이 규칙만 익혀도 기본 앱의 절반은 이해한 셈입니다.


4. 반응형 프로그래밍의 핵심: reactive 계열 함수

Shiny를 배우다 보면 가장 먼저 만나는 벽이 반응형 함수들입니다. 이름이 비슷해서 혼동되지만, 역할을 나누면 정리가 됩니다.

함수 한 줄 요약 언제 쓰는가
reactive() 계산 결과를 재사용하는 반응형 식 같은 계산을 여러 출력에서 공유할 때
isolate() 반응성을 잠시 끊고 값을 읽음 입력값은 쓰되, 그 변화에 자동 반응하고 싶지 않을 때
observe() 백그라운드에서 계속 감시하며 실행 값이 바뀔 때 부수효과를 만들 때
observeEvent() 특정 이벤트가 발생할 때만 실행 버튼 클릭처럼 명확한 트리거가 있을 때
eventReactive() 특정 이벤트가 있을 때만 값 갱신 버튼을 눌렀을 때만 새 데이터를 만들고 싶을 때
reactiveVal() 단일 반응형 값 저장 값 하나만 상태로 관리할 때
reactiveValues() 여러 반응형 값 저장 상태 여러 개를 묶어서 관리할 때

이제 하나씩 보겠습니다.

4.1 reactive()는 “반복 계산을 하나로 묶는 함수”다

예를 들어 히스토그램과 요약통계를 동시에 보여주는데, 두 출력이 모두 같은 난수 데이터를 써야 한다고 가정해 보겠습니다. 이때 매번 rnorm(input$num)를 각각 쓰면 같은 화면인데도 서로 다른 데이터가 생성될 수 있습니다.

이럴 때 reactive()로 파생 값을 하나 만들어 재사용합니다.

library(shiny)

ui <- fluidPage(
  sliderInput("num", "표본 개수", min = 10, max = 500, value = 100),
  plotOutput("hist"),
  verbatimTextOutput("stats")
)

server <- function(input, output, session) {
  data_r <- reactive({
    rnorm(input$num)
  })

  output$hist <- renderPlot({
    hist(data_r(), main = paste("평균:", round(mean(data_r()), 2)))
  })

  output$stats <- renderPrint({
    summary(data_r())
  })
}

shinyApp(ui, server)

여기서 중요한 점은 두 가지입니다.

  • reactive({...})가 반환하는 것은 일반 벡터가 아니라 반응형 함수이므로 사용할 때 data_r()처럼 괄호를 붙입니다.
  • input$num이 바뀌면 data_r()가 다시 계산되고, 그것을 쓰는 출력들도 함께 갱신됩니다.

reactive()는 “계산 결과를 저장해 둔다”기보다, 의존 관계를 기억하는 반응형 계산식이라고 이해하는 편이 정확합니다.

4.2 isolate()는 “값은 읽되 반응은 하지 않게” 만든다

사용자가 슬라이더와 제목 입력창을 동시에 가지고 있다고 해 보겠습니다. 그런데 제목을 입력할 때마다 그래프가 계속 다시 그려지는 것은 오히려 불편할 수 있습니다. 제목은 단지 현재 값만 읽고 싶고, 반응은 슬라이더에만 걸고 싶을 때 isolate()를 씁니다.

library(shiny)

ui <- fluidPage(
  sliderInput("num", "표본 개수", min = 10, max = 500, value = 100),
  textInput("title", "그래프 제목", value = "정규분포 예시"),
  plotOutput("hist")
)

server <- function(input, output, session) {
  output$hist <- renderPlot({
    hist(rnorm(input$num), main = isolate(input$title))
  })
}

shinyApp(ui, server)

이 예제에서 그래프는 input$num이 바뀔 때만 다시 그려지고, input$title은 재실행의 원인이 되지 않습니다.

isolate()는 다음 상황에서 특히 유용합니다.

  • 값을 읽기는 해야 하지만, 그 입력이 반응형 의존 관계에 포함되면 안 될 때
  • 버튼을 누를 때만 묶어서 처리하고 싶을 때
  • observeEvent() 안에서 다른 입력값을 참고할 때

4.3 observe()observeEvent()는 무엇이 다른가

둘 다 어떤 일이 생겼을 때 코드를 실행한다는 점은 같지만, 쓰임새가 다릅니다.

observe()

observe()는 내부에서 참조한 반응형 값이 바뀔 때마다 다시 실행됩니다. 결과를 직접 화면에 돌려주기보다는, 상태 변경이나 알림 같은 부수효과(side effect)를 만드는 데 적합합니다.

library(shiny)

ui <- fluidPage(
  sliderInput("num", "숫자", min = 0, max = 100, value = 20),
  plotOutput("hist"),
  actionButton("click", "현재 상태 출력")
)

server <- function(input, output, session) {
  rv <- reactiveValues(label = "")

  observe({
    if (input$num > 50) {
      rv$label <- "50보다 큼"
    } else {
      rv$label <- "50 이하"
    }
  })

  output$hist <- renderPlot({
    hist(rnorm(input$num + 1), main = rv$label)
  })

  observeEvent(input$click, {
    print(rv$label)
  })
}

shinyApp(ui, server)

이 예제에서 observe()input$num이 변할 때마다 rv$label을 갱신합니다.

observeEvent()

observeEvent()는 이름 그대로 특정 이벤트가 발생했을 때만 실행됩니다. 가장 대표적인 이벤트는 버튼 클릭입니다.

library(shiny)

ui <- fluidPage(
  sliderInput("num", "표본 개수", min = 10, max = 500, value = 100),
  actionButton("draw", "그래프 다시 그리기"),
  plotOutput("hist")
)

server <- function(input, output, session) {
  observeEvent(input$draw, {
    output$hist <- renderPlot({
      hist(rnorm(isolate(input$num)))
    })
  })
}

shinyApp(ui, server)

위 예제에서는 슬라이더를 움직인다고 바로 그래프가 바뀌지 않습니다. 버튼을 눌렀을 때만 input$num의 현재 값을 읽어 실행합니다.

정리하면:

  • observe()는 “관련 값이 바뀔 때마다”
  • observeEvent()는 “지정한 이벤트가 있을 때만”

입니다.

4.4 eventReactive()는 버튼 기반 데이터 생성에 가장 많이 쓰인다

eventReactive()는 버튼을 눌렀을 때만 갱신되는 반응형 값을 만듭니다. observeEvent()reactive()를 합친 형태라고 생각하면 이해가 쉽습니다.

library(shiny)

ui <- fluidPage(
  sliderInput("num", "표본 개수", min = 10, max = 500, value = 100),
  actionButton("go", "새 데이터 생성"),
  plotOutput("hist"),
  verbatimTextOutput("summary")
)

server <- function(input, output, session) {
  data_r <- eventReactive(input$go, {
    rnorm(input$num)
  }, ignoreNULL = FALSE)

  output$hist <- renderPlot({
    hist(data_r(), main = "버튼을 눌렀을 때만 갱신")
  })

  output$summary <- renderPrint({
    summary(data_r())
  })
}

shinyApp(ui, server)

이 함수가 특히 좋은 이유는 생성된 값 data_r()를 여러 출력에서 재사용할 수 있기 때문입니다. 즉, “업데이트 시점은 버튼이 결정하고, 사용은 여러 군데에서 한다”는 구조가 아주 깔끔하게 표현됩니다.

4.5 reactiveVal()reactiveValues()는 상태 저장소다

Shiny에서는 계산뿐 아니라 상태(state) 를 저장해야 할 때가 많습니다. 예를 들어 현재 선택 모드, 누적 카운터, 최근 업로드 파일, 임시 메시지 같은 것들입니다.

reactiveVal(): 값 하나만 저장할 때

library(shiny)

ui <- fluidPage(
  actionButton("minus", "-1"),
  actionButton("plus", "+1"),
  textOutput("value")
)

server <- function(input, output, session) {
  value <- reactiveVal(0)

  observeEvent(input$minus, {
    value(value() - 1)
  })

  observeEvent(input$plus, {
    value(value() + 1)
  })

  output$value <- renderText({
    paste("현재 값:", value())
  })
}

shinyApp(ui, server)

reactiveVal()은 함수처럼 동작합니다.

  • 읽기: value()
  • 쓰기: value(새값)

reactiveValues(): 값 여러 개를 묶어서 관리할 때

library(shiny)

ui <- fluidPage(
  sliderInput("num", "표본 개수", min = 50, max = 500, value = 200),
  actionButton("norm", "정규분포"),
  actionButton("unif", "균등분포"),
  plotOutput("hist")
)

server <- function(input, output, session) {
  rv <- reactiveValues(
    data = rnorm(100),
    dist_name = "정규분포"
  )

  observeEvent(input$norm, {
    rv$data <- rnorm(input$num)
    rv$dist_name <- "정규분포"
  })

  observeEvent(input$unif, {
    rv$data <- runif(input$num)
    rv$dist_name <- "균등분포"
  })

  output$hist <- renderPlot({
    hist(rv$data, main = rv$dist_name)
  })
}

shinyApp(ui, server)

둘의 차이는 아래처럼 정리하면 됩니다.

함수 적합한 상황 접근 방식
reactiveVal() 상태가 1개뿐일 때 x(), x(new_value)
reactiveValues() 상태가 여러 개일 때 rv$a, rv$b <- ...

5. 동적으로 UI를 바꾸는 방법

기본적인 Shiny 앱은 UI가 고정되어 있지만, 실제 앱에서는 조건에 따라 입력창이 바뀌는 경우가 많습니다. 예를 들어 “고급 설정 보기”를 체크했을 때만 추가 입력을 보여주거나, 업로드한 파일 종류에 따라 다른 입력을 띄우는 식입니다.

5.1 uiOutput()renderUI()

uiOutput("id")는 화면에 “나중에 동적으로 UI를 끼워 넣을 자리”를 만드는 함수입니다. 이 자리에 실제 UI를 채우는 것은 서버 쪽의 renderUI()입니다.

library(shiny)

ui <- fluidPage(
  checkboxInput("advanced", "고급 옵션 보기", value = FALSE),
  uiOutput("more_controls")
)

server <- function(input, output, session) {
  output$more_controls <- renderUI({
    if (!input$advanced) {
      return(NULL)
    }

    tagList(
      sliderInput("n", "표본 개수", min = 10, max = 1000, value = 200),
      textInput("label", "라벨", value = "고급 설정")
    )
  })
}

shinyApp(ui, server)

실무에서 이 패턴은 매우 자주 씁니다.

  • 조건부 옵션 열기
  • 서버 계산 결과에 따라 입력 종류 변경
  • 파일 업로드 후 후속 UI 생성

5.2 update...Input() 계열 함수

Shiny는 기존 위젯을 새로 그리지 않고, 값을 업데이트하는 함수들을 제공합니다. 이것이 updateSelectInput(), updateTextInput(), updateRadioButtons() 같은 함수들입니다.

대표 패턴은 다음과 같습니다.

library(shiny)

ui <- fluidPage(
  checkboxGroupInput(
    "inCheckboxGroup",
    "선택 항목",
    choices = c("Item A", "Item B", "Item C")
  ),
  selectInput("inSelect", "최종 선택", choices = c("Item A", "Item B", "Item C"))
)

server <- function(input, output, session) {
  observe({
    x <- input$inCheckboxGroup
    if (is.null(x)) {
      x <- character(0)
    }

    updateSelectInput(
      session,
      "inSelect",
      label = paste("현재 선택 가능 항목:", length(x)),
      choices = x,
      selected = tail(x, 1)
    )
  })
}

shinyApp(ui, server)

이 방식의 장점은 분명합니다.

  • 위젯을 다시 만들지 않아도 됨
  • 반응 속도가 빠름
  • 사용자가 보고 있는 UI 구조를 크게 흔들지 않음

즉, “보여줄 입력을 통째로 바꾸는가”는 renderUI(), “기존 입력의 내용만 갱신하는가”는 update...Input()으로 구분해서 생각하면 됩니다.


6. JavaScript와 연결할 때: Shiny.setInputValue()

Shiny는 기본 위젯만으로도 많은 앱을 만들 수 있지만, 브라우저의 클릭 이벤트나 커스텀 HTML 요소 값을 서버로 보내고 싶을 때가 있습니다. 이때 JavaScript에서 Shiny.setInputValue()를 사용해 input$... 값을 직접 만들 수 있습니다.

library(shiny)

ui <- fluidPage(
  tags$button(id = "send_btn", "서버로 값 보내기"),
  verbatimTextOutput("clicked_value"),
  tags$script(HTML("
    document.getElementById('send_btn').addEventListener('click', function() {
      Shiny.setInputValue('custom_click', '버튼이 눌렸습니다', {priority: 'event'});
    });
  "))
)

server <- function(input, output, session) {
  output$clicked_value <- renderPrint({
    input$custom_click
  })
}

shinyApp(ui, server)

여기서 중요한 포인트는 아래와 같습니다.

  • Shiny.setInputValue("custom_click", 값)을 호출하면 서버에서는 input$custom_click으로 읽을 수 있습니다.
  • {priority: "event"} 옵션을 주면 값이 같더라도 이벤트성 입력으로 다시 처리할 수 있습니다.

예전에는 Shiny.onInputChange()를 많이 썼지만, 지금은 Shiny.setInputValue()를 사용하는 편이 더 일반적입니다.


7. UI를 더 잘 다루기 위한 HTML 태그와 CSS

Shiny UI는 사실상 HTML을 R 문법으로 조립하는 방식입니다. 그래서 tags$h1(), tags$p(), tags$a() 같은 함수를 이해하면 화면 구성이 훨씬 자유로워집니다.

7.1 tags$...로 직접 HTML 요소 만들기

library(shiny)

ui <- fluidPage(
  tags$h1("샤이니 앱 시작하기"),
  tags$p(
    "공식 문서는 ",
    tags$a(href = "https://shiny.posit.co/", "여기"),
    "에서 확인할 수 있습니다."
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

기본 위젯 함수만으로 부족할 때 tags$...를 섞어 쓰면 설명 문구, 링크, 아이콘, 구분 블록 같은 요소를 훨씬 자연스럽게 넣을 수 있습니다.

7.2 CSS 스타일 적용하기

Shiny에서 스타일을 바꾸는 가장 직접적인 방법은 tags$head(tags$style(...))입니다.

library(shiny)

ui <- fluidPage(
  tags$head(
    tags$style(HTML("
      .highlight-box {
        background-color: #f5f7fb;
        border-left: 6px solid #2c7be5;
        padding: 16px;
        margin-bottom: 16px;
      }
    "))
  ),
  tags$div(
    class = "highlight-box",
    tags$strong("안내"),
    tags$p("이 박스는 CSS로 꾸민 사용자 정의 영역입니다.")
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

이 패턴은 대시보드 스타일을 조정하거나, 안내문 박스, 경고 박스, 카드 UI를 직접 만들 때 유용합니다.


8. 레이아웃은 어떻게 잡아야 하는가

Shiny 레이아웃은 크게 두 가지 관점으로 이해하면 됩니다.

  • 자유롭게 행과 열을 조합하는 방식
  • 미리 구성된 레이아웃 함수를 쓰는 방식

8.1 fluidRow()column()으로 기본 그리드 만들기

Shiny의 기본 그리드는 12칸 기준입니다. 한 행(fluidRow) 안에서 열(column)의 합이 보통 12가 되도록 배치합니다.

library(shiny)

ui <- fluidPage(
  fluidRow(
    column(4, "왼쪽 영역"),
    column(8, "오른쪽 영역")
  ),
  fluidRow(
    column(3, offset = 3, "가운데로 밀린 영역"),
    column(3, offset = 3, "또 다른 영역")
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

offset은 열을 오른쪽으로 밀어 배치할 때 씁니다. 간단한 대시보드, 필터 패널, 요약 카드 배치에 매우 자주 등장합니다.

8.2 sidebarLayout()은 가장 실무적인 기본 레이아웃이다

입력 위젯은 왼쪽, 결과는 오른쪽이라는 구성이 필요하면 sidebarLayout()이 가장 빠릅니다.

library(shiny)

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      sliderInput("n", "표본 개수", min = 10, max = 500, value = 100),
      actionButton("resample", "다시 생성")
    ),
    mainPanel(
      plotOutput("hist")
    )
  )
)

server <- function(input, output, session) {
  rv <- reactiveValues(data = rnorm(100))

  observeEvent(input$resample, {
    rv$data <- rnorm(input$n)
  })

  output$hist <- renderPlot({
    hist(rv$data)
  })
}

shinyApp(ui, server)

8.3 tabsetPanel(), navlistPanel(), navbarPage()는 화면을 나누는 방식이 다르다

여러 화면을 나누는 대표 함수는 아래와 같습니다.

함수 특징 적합한 상황
tabsetPanel() 탭으로 같은 화면 내부를 전환 결과 뷰 여러 개를 전환할 때
navlistPanel() 왼쪽 메뉴 + 오른쪽 내용 문서형 앱, 설정 화면
navbarPage() 상단 내비게이션 바 페이지 수가 많은 앱, 대시보드

예를 들어 상단 메뉴형 구조는 이렇게 만듭니다.

library(shiny)

ui <- navbarPage(
  "Distribution App",
  tabPanel(
    "정규분포",
    plotOutput("norm"),
    actionButton("renorm", "다시 생성")
  ),
  navbarMenu(
    "기타 분포",
    tabPanel(
      "균등분포",
      plotOutput("unif"),
      actionButton("reunif", "다시 생성")
    ),
    tabPanel(
      "카이제곱분포",
      plotOutput("chisq"),
      actionButton("rechisq", "다시 생성")
    )
  )
)

server <- function(input, output, session) {
  rv <- reactiveValues(
    norm = rnorm(500),
    unif = runif(500),
    chisq = rchisq(500, df = 2)
  )

  observeEvent(input$renorm, { rv$norm <- rnorm(500) })
  observeEvent(input$reunif, { rv$unif <- runif(500) })
  observeEvent(input$rechisq, { rv$chisq <- rchisq(500, df = 2) })

  output$norm <- renderPlot(hist(rv$norm, breaks = 30))
  output$unif <- renderPlot(hist(rv$unif, breaks = 30))
  output$chisq <- renderPlot(hist(rv$chisq, breaks = 30))
}

shinyApp(ui, server)

9. 초보자가 가장 많이 헷갈리는 함수 차이 정리

아래 표는 실제로 많이 헷갈리는 함수들을 비교한 것입니다.

상황 가장 먼저 떠올릴 함수 이유
같은 계산 결과를 여러 출력에서 공유하고 싶다 reactive() 중복 계산과 불일치 방지
버튼을 눌렀을 때만 새 데이터를 만들고 싶다 eventReactive() 이벤트 기반으로 반응형 값 생성
버튼을 눌렀을 때 어떤 코드를 실행하고 싶다 observeEvent() 이벤트 트리거용
입력값 변화에 따라 내부 상태를 갱신하고 싶다 observe() 백그라운드 감시용
값 하나만 저장하고 싶다 reactiveVal() 단일 상태 저장
상태를 여러 개 저장하고 싶다 reactiveValues() 리스트처럼 묶어 관리
값을 읽되 그 값 변화에 자동 재실행되면 안 된다 isolate() 반응성 차단

이 표를 기억하면 함수 선택 실수가 크게 줄어듭니다.


10. 작은 앱 하나로 흐름을 통합해 보기

아래 예제는 지금까지 살펴본 요소를 한 번에 묶은 간단한 연습 앱입니다.

  • 사이드바 레이아웃
  • 슬라이더 입력
  • 버튼 클릭 기반 데이터 생성
  • eventReactive()
  • 제목 입력과 isolate()
  • 결과 텍스트와 그래프 출력
library(shiny)

ui <- fluidPage(
  titlePanel("Shiny 기초 종합 예제"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("n", "표본 개수", min = 10, max = 1000, value = 200),
      textInput("title", "그래프 제목", value = "정규분포 히스토그램"),
      actionButton("go", "데이터 생성")
    ),
    mainPanel(
      plotOutput("hist"),
      verbatimTextOutput("summary")
    )
  )
)

server <- function(input, output, session) {
  sampled_data <- eventReactive(input$go, {
    rnorm(input$n)
  }, ignoreNULL = FALSE)

  output$hist <- renderPlot({
    hist(sampled_data(), main = isolate(input$title), col = "skyblue", border = "white")
  })

  output$summary <- renderPrint({
    summary(sampled_data())
  })
}

shinyApp(ui, server)

이 앱을 이해했다면, Shiny의 가장 중요한 입문 문법은 이미 잡은 것입니다.


11. 실무에서 자주 만나는 실수

11.1 reactive()를 일반 객체처럼 사용하기

아래처럼 data_r를 함수처럼 호출하지 않으면 오류가 납니다.

data_r <- reactive({ rnorm(10) })
mean(data_r)     # 잘못된 사용
mean(data_r())   # 올바른 사용

11.2 observeEvent()eventReactive()를 혼동하기

  • observeEvent()는 “실행”이 목적
  • eventReactive()는 “값 생성”이 목적

버튼을 눌렀을 때 데이터를 만들어 여러 출력에서 쓰고 싶다면 eventReactive()가 더 자연스럽습니다.

11.3 입력 변화마다 불필요하게 앱이 다시 계산되게 만들기

이때는 isolate() 또는 버튼 기반 구조(observeEvent(), eventReactive())를 고려해야 합니다. 초보 단계에서 앱이 느리거나 너무 자주 깜빡이는 대부분의 원인은 이 반응형 의존 관계를 과하게 만든 데 있습니다.

11.4 동적 UI를 만들 수 있는데도 무조건 renderUI()만 쓰기

기존 입력창의 선택지만 바꾸면 될 상황이라면 updateSelectInput() 같은 업데이트 함수가 더 간단하고 빠릅니다.


12. 처음 공부할 때 추천하는 학습 순서

Shiny는 한 번에 모두 익히려고 하면 어렵습니다. 아래 순서로 배우는 것이 가장 안정적입니다.

  1. fluidPage(), sliderInput(), plotOutput(), renderPlot()으로 가장 작은 앱 만들기
  2. input$..., output$... 연결 규칙 익히기
  3. reactive()로 계산 재사용하기
  4. observeEvent()eventReactive()로 버튼 기반 제어 익히기
  5. reactiveVal()reactiveValues()로 상태 저장하기
  6. sidebarLayout(), tabsetPanel(), navbarPage() 같은 레이아웃 익히기
  7. renderUI()update...Input()으로 동적 UI 확장하기
  8. 필요하면 Shiny.setInputValue()로 JavaScript 연동하기

이 순서를 따르면 “문법은 아는데 앱을 못 만든다”는 상태를 훨씬 빨리 벗어날 수 있습니다.


13. 빠르게 점검하는 체크리스트

  • ui에서 만든 입력 이름이 input$...와 정확히 일치하는가
  • 출력 자리 이름과 output$... 이름이 일치하는가
  • reactive() 반환값을 사용할 때 ()를 붙였는가
  • 버튼 클릭 기반 로직이면 observeEvent() 또는 eventReactive()를 썼는가
  • 동적 UI가 필요하면 renderUI()를, 기존 입력 갱신이면 update...Input()을 쓰고 있는가
  • 같은 계산을 여러 번 반복하지 않고 reactive()로 묶었는가

14. 자주 묻는 질문

Q1. reactive()renderPlot()은 무엇이 다른가

reactive()재사용 가능한 계산 결과를 만드는 함수이고, renderPlot()그래프 출력 객체를 만드는 함수입니다. 하나는 계산식, 다른 하나는 화면 출력입니다.

Q2. observeEvent() 대신 eventReactive()를 쓰면 안 되나

둘은 대체 관계가 아니라 목적이 다릅니다. 값을 반환해야 하면 eventReactive(), 코드 실행 자체가 목적이면 observeEvent()가 맞습니다.

Q3. reactiveVal()reactiveValues() 중 무엇부터 써야 하나

상태가 하나면 reactiveVal(), 둘 이상이면 reactiveValues()가 기준입니다. 처음에는 reactiveVal()이 더 단순해서 배우기 쉽습니다.

Q4. UI를 바꾸려면 무조건 JavaScript가 필요한가

아닙니다. 상당수는 renderUI()update...Input()만으로 해결됩니다. JavaScript는 기본 위젯만으로 표현이 안 되는 동작이 필요할 때 쓰면 됩니다.


15. 마무리

Shiny 기본 문법의 핵심은 많아 보이지만 실제로는 몇 가지 축으로 정리됩니다. 입력과 출력의 연결, 반응형 계산의 재사용, 이벤트 기반 제어, 상태 저장, 그리고 레이아웃입니다. 이 다섯 가지를 구분해서 이해하면 앱 구조가 훨씬 선명해집니다.

처음부터 복잡한 대시보드를 만들기보다, 작은 앱 하나를 만든 뒤 reactive(), observeEvent(), eventReactive()를 각각 바꿔 가며 실험해 보는 방식이 가장 빠릅니다. Shiny는 개념보다 체감이 중요해서, 직접 값을 움직여 보면 문법 차이가 훨씬 빨리 정리됩니다.

함께 읽으면 좋은 글