dynamic UI로 shiny module 보여주기
크큭 이제 개발자다
이번 글은 정말 X 3 오랜만의 Shiny 관련 글로, 최근 개발 작업 중 만났고 이틀 정도를 태운 끝에 풀어냈던 문제의 대한 글을 작성하려고 한다.
최소한의 설명
⭐ Shiny
- R에서 사용되는 웹페이지 / 대시보드 구현 패키지
- Python의 Flask / Django를 생각하면 비슷하다고 알고 있다.
- 관심이 있다면, 2020년에 작성했던 글을 참고해도 좋다.
📦 Module
- Python이 R의 data.frame과 같은 기능을 참조한 것처럼, R에서도 Python의 엔지니어링적인 요소들을 참조한 것 같다 (뇌피셜)
- R의 Package (library) 와 유사하게, Shiny application에서 특정 기능만을 담당하는 코드 모음으로 UI와 Server라는 2개의 필수 요소로 구성되어있다.
- 즉 하나의 Shiny는 (여러개의 기능으로 이루어져 있기 때문에) 여러개의 Module 로 구성 될 수 있다.
- 코드를 모듈화 하는 것의 장점은 꼭 R이 아니더라도 이미 많은 개발자들이 작성한 글이 있으니 참조하면 좋다. (내 설명보다 훨씬 낫다 ㅎㅎ)
🔄 dynamicUI
- Shiny의 핵심 컨셉 중 하나로, interactive 한 web page 가 있는데 짧게 설명하면 변수 A를 선택하면 A의 분포를, 변수 B를 선택하면 B의 분포를 그리는 웹페이지를 만들 수 있다고 생각하면 된다.
- dynamicUI는 조금 더 나아가서, + 버튼을 누르면 선택 할 수 있는 영역을 A / B 를 넘어 새로운 C / D를 추가하여 그들의 분포도 추가적으로 그려 낼 수 있다 라고 생각하면 된다.
- 앞과의 차이는 처음에 application 설계시 개발자가 분포를 그리는 코드를 심었는지, 사용자의 입력에 반응해서 application에서 자체적으로 코드를 추가해서 심었는지 이다.
예를 들어, 사용자에게 변수, 연산, 기준 (쿼리 조건)을 입력을 받아서 실행시키는 페이지가 있다고 해보자. (A > 1
이라는 코드가 서버로 전달된다)
dynamic UI는 사용자가 1개의 조건이 아니라 추가로 조건을 만들고 싶을때 + ADD FILTER
버튼을 눌러 입력을 받을 수 있는 UI를 dynamic 하게 더하고 그 결과를 작업하는 코드를 의미한다. (물론 X
를 통해 지울 수도 있으면 좋다)
이러한 dynamicUI가 필요한 이유는 사용자가 얼마만큼의 액션을 할지 모르기 때문인데, 그렇기 때문에 UI를 미리 코드로 만들어 두는 것이 아니라, UI를 코드에서 dynamic하게 더할 수 있는 코드를 작성하는 것이다.
내가 풀어야 했던 문제
- 하나의 데이터에 할 수 있는 작업이 여러개가 있으며 각자 모듈 형태로 구현 되어 있다.(W, X, Y, Z라 하자)
- 각 작업마다 필요로 하는 입력값이 다르다.
- 모든 작업을 전부 페이지에 나열 하는 것이 아닌, 선택지를 통해 해당 작업에 필요한 입력값만 받는 UI를 구현한다.
- 입력값 중 하나는 데이터의 column 인데, 이전의 다른 작업으로 인해 column의 선택지 자체가 바뀔 수 있어서 새롭게 load를 해야한다.
이를 위해 제일 처음 선택했던 방법은 마법의 Action Button으로 (최애 😻 ), 기능을 선택한 다음, Input 의 선택지를 불러오기 위해 Load !
버튼을 누르게 하는 것이었다.
가장 구현이 쉽고 빠르고 확실했지만 각 모듈별로 Load !
버튼에 해당하는 UI와 Server를 넣어야했고, 사용자의 입장에서는 매번 버튼을 눌러줘야 하는 귀찮음도 있었다. 그래서 기능을 선택할때 Load !
까지 자동으로 할 수 있게 구현을 해야했다.
난관 & 헤딩
shiny module은 namespace
라는 방법을 사용하여 각 모듈들끼리 커뮤니케이션을 할 수 있게 한다. (서로 입력값을 주고, 결과물을 뱉는다라고 생각하면 좋다)
문제는, namespace
는 모듈끼리는 커뮤니케이션을 할 수 있게 하지만 Main application 과는 커뮤니케이션을 할 수 없었다.
어거지로 그림을 그려보면 아래와 같다.
- google과 stackoverflow에서 내가 풀고 싶었던 것은
shiny module 내부의 UI를 dynamic하게 업데이트 하는 방법
이었지만 아쉽게도shiny module을 dynamicUI로 구현하는 방법
들이 많이 나왔다. - Main application의 module을 선택하는 UI에
observeEvent
를 통해서 module의 UI 를 업데이트 하려고도 해봤지만namespace
에 들어가지 않아서 불가능했다. - Main application의 데이터를 입력받는 기능까지 모듈로 만들기에는 뭔가 이상했다 (파일이 얽혀 있다보니 이 부분에 대한 고민이 정말 어려웠다)
솔루션
결국 풀어낸 방법은 해들리 위컴 선생님의 방법인데, module server에 입력파라미터로 main Application 의 모듈 선택 UI를 reactive를 씌워서 넘기는 것이다.
설명은 복잡하고 방법도 뭔가 우아하지 않은데, 그림으로 표현하면 대충 이런 느낌이다.
물론 2020년 기준의 답변이라 어쩌면 지금은 새로운 기능으로 연결 하는 방법이 있을 수도 있다. (있었다면 검색했을때 나왔지 않을까)
그리고 결국 해들리 선생님도 이 부분도 모듈로 구현하라
라고 답변하지 않았는데, 그 이유는 아직도 잘 모르겠다. (다만 본능적으로 저렇게 하면 안된다 라는 생각이 들긴 하는데 나중에 시간 나면 한번 해봐야겠다.)
결국 사용했던 코드의 형태는 이러하다. (R 개발자가 아니면 쓸 일이 없을 거라 믿기 때문에 텍스트 대신, 설명에 용이한 색깔이 들어가는 이미지로 붙여넣었다)
즉 기본 module server에 추가로 inputData와 opened라는 값을 전달했고 각자 reactive ( ()
를 사용하여 interactive 하게 값이 바뀌는 것을 추적) 형태로 모듈 안에서 사용했다.
추가로 updateSelectizeInput
에서는 module 내부가 아닌, main application (외부) 에 작업하는 것이기 때문에 UI를 구현하는 것임에도 ns
를 씌워서는 안된다 ☠️
결과
- 각 기능 (
Filter
,Subset
,Mutate
…)은 모듈로 구현되어 있다. - 기능마다 담고 있는 기능이 다르기 때문에 각자 독립적으로 구현하는 것이 좋은편 ( ex)
Reshape
는sortable.js
를 긁어서 사용) - 첫번째 선택지(
SelectLabel
)가 데이터의 Column으로 업데이트가 되어야 하는 부분 - 데이터는 모듈에 담겨 있지 않고 메인 어플에 들어가있다.
결론
돌아만 가면 됐지 뭐
라는 생각을 하는 걸 보니 개발자 된 것 같다.