Shiny 애플리케이션 서버에서 구동하기

Shiny

로컬 호스트에서 구동되는 Shiny 애플리케이션을 Remote 서버에서 구현할 때의 이슈와 대안을 살펴봅니다.

유충현 https://choonghyunryu.github.io (한화생명)
2022-01-23

stand-alone 그리고 server

Shiny 애플리케이션을 제법 만들수 있게 되었습니다.

누구나 그렇듯, RStudio에서 애플리케이션 개발 작업을 수행합니다. 그리고 간단하게 애플리케이션 실행 아이콘을 눌러 실행합니다. 애플리케이션이 실행되면 이제부터는 RStudio 콘솔은 사용할 수 없게 됩니다. 애플리케이션을 종료할 때까지 말입니다. 가끔은 이것이 불편할 수 있습니다. 그러나 RStudio에서 쉽게 개발하는 대신에 이러한 불편함을 인내해야 합니다.

이런 방법은 RStudio에서 stand-alone(스탠드 얼론) 방식으로 가볍게 애플리케이션의 기능을 검증하고, 또 수정하고 실행하고 검증을 합니다.

어느덧, 개발한 Shiny 애플리케이션을 server에 배포하는 경지(?)에 도달했습니다. 대중을 위한 서비스 오픈은 아니고, 개인 노트북에 데모 환경을 구축하게 되었습니다. docker에 Shiny server를 구축하고 애플리케이션을 포팅하니 기존에 개발했던 코드에서 몇몇 이슈가 발생합니다. stand-alone에서는 정상적으로 구현되는 기능에 오류가 발생하기 시작한 것입니다.

stand-alone와 server는 시스템 환경이 다를 수 있으므로, Shiny server로 애플리케이션을 배포할 경우에는 반드시 server 환경에서도 테스트를 수행해야 합니다.

BitStat

BitStat라는 Shiny 애플리케이션을 개발하고 있습니다. 현재는 R 패키지로 배포되고 있습니다만, 언젠가는 shinyapps.iobinder 등에 서비스를 배포할 것입니다.

이것은 이 애플리케이션을 stand-alone에서 테스트하고 있다는 것을 의미하기도 합니다.

며칠 전부터 노트북의 docker 환경에 shiny server를 구축하고, BitStat를 비롯하여 몇몇 애플리케이션을 포팅하고 있습니다. 그러던 중에 stand-alone 환경에서 정상적으로 수행되던 기능에 오류가 발생하는 사례가 나오기 시작했습니다.

이번 글을 이런 사례에 대한 Troubleshooting에 대한 내용입니다.

어떤 문제인가요?

다음과 같은 두가지 문제가 발생했습니다.

pagedown 패키지로 PDF 파일 생성 이슈

pagedown 패키지는 크롬 브라우저를 이용해서 웹 문서를 PDF 파일로 변환합니다. Mac OSX에서 stand-alone로 사용할 경우에는 Mac OSX에 설치된 크롬 브라우저를 사용하는데, docker 컨테이너의 Linux server에는 크롬 브라우저가 설치되어 있지 않습니다.

문서의 브라우저 팝업 이슈

Shiny에서 정적 페이지를 브라우징할 때에는 utils::browseURL()를 사용합니다. 이것은 stand-alone에서는 웹브라우저에서 정상적으로 동작하지만, docker 컨테이너의 Linux server에서는 동작하지 않습니다.

엄밀히 말하자면, 웹 서비스도 client-server 방식으로 동작합니다. stand-alone에서는 사용자 환경이 server 환경이자 client 환경이기 때문에 utils::browseURL()는 사용자의 기본 웹 브라우저에서 정적 페이지를 브라우징합니다. 그러나 cli 기반의 Linux server에서는 브라우징이 어렵겠지요.

X 터미널로 접속한다면, Unix-alikes server에서는 xdg-open 유틸리티가 브라우저를 띄우겠지만, 이것은 웹 서비스 환경이 아닙니다. 웹 서비스 환경이라면 사용자의 컴퓨터(client)에서 브라우징되어야만 합니다. 이것은 결국 Javascript를 사용해야 한다는 의미입니다.

pagedown 패키지 이슈 해결

크롬 설치

다음처럼 curl을 이용해서 크롬을 다운로드한 후 설치하고 삭제합니다. 이를 위해서 앞에서 apt-get로 curl을 설치한 것입니다.

RUN apt-get update && apt-get install -y \
    curl

RUN curl -L http://bit.ly/google-chrome-stable -o google-chrome-stable.deb && \
    apt-get -y install ./google-chrome-stable.deb && \
    rm google-chrome-stable.deb

크롬을 위한 설정

pagedown 패키지가 크롬을 사용한다는 것을 앞에서 언급했습니다. 그런데 pagedown::chrome_print()가 크롬을 호출할 때, 다음처럼 “Error in is_remote_protocol_ok: Cannot find headless Chrome after 20 attempts”라는 에러가 발생합니다.

output file: diagnosis_paged_temp.knit.md


Output created: diagnosis_paged_temp.html
Warning: Error in is_remote_protocol_ok: Cannot find headless Chrome after 20 attempts
  1: runApp

Execution halted

크롬은 ‘샌드박스(sandbox)’라는 보안 개념을 적용합니다. 크롬은 브라우저에서 여러 개의 독립된 탭을 띄우고, 별도의 웹 페이지가 실행됩니다. 별도로 독립된 프로세스가 가동하는 것으로 이들 프로세스는 샌드박스처럼 격리되어서 서로 관여하지 못합니다. 즉, 어느 탭의 웹 페이지에서 악성코드가 침투하거나, 버그 또는 장애로 해당 탭의 페이지가 먹통되어도 다른 탭의 웹 페이지는 정상적으로 동작합니다.

앞에서의 에러는 크롬의 샌드박스 기능에 기인합니다. 이 에러는 해결하기 위해서는 컨테이너에서 pagedown::chrome_print()가 크롬을 호출할 때, 샌드박스의 기능을 비활성해야 합니다.

이를 위한 몇 가지 솔루션이 있습니다.


  1. pagedown::chrome_print() 함수 호출 수정
  1. 크롬 실행 시 옵션 정의


여기서는 2번 솔루션을 적용합니다.

다음과 같은 스크립트를 담은 google-chrome 파일을 ./shiny-docker 경로에 생성합니다.

#!/bin/bash

/usr/bin/google-chrome --no-sandbox $*

그리고 호스트에서 이 파일의 권한을 설정합니다. shiny 계정으로 컨테이너를 실행하기 때문에 755 권한을 부여해야 합니다.

chmod 755 google-chrome

다음으로 다음과 같은 스크립트를 담은 Renviron 파일을 ./shiny-docker 경로에 생성합니다.

PATH="/:${PATH}"

그리고 Dockerfile 파일에서 이들을 다음처럼 컨테이너에 복사합니다.

COPY google-chrome /usr/local/bin/
COPY Renviron /.Renviron

문서의 브라우저 팝업 이슈 해결

기존 로직은 이렇게 간단합니다.

browseURL(paste(".", output_file, sep = "/"))

utils::browseURL()을 사용하는 로직을 다음처럼 Javascript로 브라우저 창을 팝업하는 로직으로 변경합니다.

먼저 Javascript 코드로 함수를 작성합니다.

# define js function for opening urls in new tab/window
js_code <- "shinyjs.browseURL = function(url) {
              window.open(url, '_blank');
           }"

그리고, 작성한 Javascript 함수를 사용할 수 있게 Shiny ui 영역에 설정합니다.

useShinyjs(),
extendShinyjs(text = js_code, functions = 'browseURL')

마지막으로 Shiny server 영역에서 해당 Javascript 함수를 호출합니다.

# Change for server side excute
file.copy(paste(tempdir(), output_file, sep = "/"),
          paste("www", output_file, sep = "/"),
          overwrite = TRUE)
URL <- paste(".", output_file, sep = "/")  

js$browseURL(URL)

이제는 server에서

앞으로 Shiny 애플리케이션을 개발할 때에는, stand-alone이 아닌 Shiny server에서 동작한다는 전제하에 로직을 구현해야할 것 같습니다. 이것이 RStudio의 개발 환경 후 Shiny server의 운영 환경으로 배포할 때 문제가 발생하는 것을 사전에 방지해주니까요.

Citation

For attribution, please cite this work as

유충현 (2022, Jan. 23). Dataholic: Shiny 애플리케이션 서버에서 구동하기. Retrieved from https://choonghyunryu.github.io/2022-01-23-serverside

BibTeX citation

@misc{유충현2022shiny,
  author = {유충현, },
  title = {Dataholic: Shiny 애플리케이션 서버에서 구동하기},
  url = {https://choonghyunryu.github.io/2022-01-23-serverside},
  year = {2022}
}