Fleet
More Than a Code Editor
Fleet의 내부 구조, 파트 VI – UI와 Noria
JetBrains의 차세대 IDE인 Fleet 빌드에 대해 알아보는 연재 게시물입니다.
이 시리즈의 파트 V에서는 Fleet의 서비스 중 하나인 코드 완성에 대해 설명드렸습니다. 이제 JVM을 위한 고유한 선언적 UI 프레임워크인 Noria에 대해 알려 드릴 시간입니다. Fleet은 Noria로 빌드되었습니다. Noria의 기반이 되는 아이디어, 주요 개념 및 기타 멋진 기능을 살펴보세요.
모든 것이 시작되는 곳: Noria 창
UI는 어떻게 빌드되나요? 우선 다들 애플리케이션의 상태를 나타낼 수 있고 UI를 GUI로 만드는 역할도 하는 그래픽 기능을 갖춘 디스플레이가 있을 겁니다. 명령을 전달하고 애플리케이션의 동작을 제어하기 위한 키보드, 마우스 또는 터치패드 등 하나 이상의 입력 기기가 있을 수도 있습니다. 이러한 환경에서 애플리케이션은 사실상 사용자 및 기타 컴퓨터 시스템 구성 요소(타이머, 파일 시스템, 네트워크 등)가 시작한 이벤트에 반응하는 이벤트 루프입니다. 기본적으로 반응은 화면에 표시되는 내용의 가시적 변화입니다.
이벤트 루프라는 특징 외에 GUI 애플리케이션은 일반적으로 일종의 창과 담당하는 화면 영역, 그리고 해당 영역에서 그리기 기능을 갖습니다. 창은 보통 기본 운영 체제 또는 그를 기반으로 한 창 관리자에 의해 제공됩니다. 운영 체제는 그래픽 API를 제공하거나 그리기 기능을 그래픽 프레임워크에 위임할 수 있습니다.
Fleet은 GUI 애플리케이션이면서 JVM 애플리케이션입니다. Windows, macOS 및 Linux를 포함한 모든 주요 운영 체제에서 실행됩니다. Fleet은 운영 체제에서 창을 가져오기 위해 Java AWT/Swing 프레임워크에 의존하지만, GUI 구성 요소를 관리하기 위해 해당 프레임워크를 기반으로 한 하나의 JFrame 및 JPanel을 사용할 뿐, Java 플랫폼을 사용하지는 않습니다. Fleet은 JVM의 화면 그리기 기능도 사용하지 않습니다. 대신 JetBrains가 개발하는 skiko-awt 바인딩 라이브러리를 통해 JVM 애플리케이션에서 사용할 수 있는 네이티브 2D 그래픽 라이브러리인 Skia를 사용합니다.
다음 다이어그램은 앞서 알려 드린 프레임워크와 라이브러리의 조합을 보여주며, 이 조합으로 자체 제작된 UI 프레임워크인 Noria 창에 의해 완벽히 관리되는 화면 영역이 생성됩니다.
Fleet 창에 표시되는 모든 항목은 Noria 구성 요소입니다. 패널, 탭, 버튼, 툴팁, 텍스트 에디터, 터미널, diff 뷰 및 docker 뷰는 Noria에 의해 관리되며 Noria의 이벤트 루프 반응 결과에 따라 지속적으로 변경됩니다.
“Noria여야 하는 이유가 뭔가요?”라고 질문하실 수도 있습니다. 이미 훌륭한 UI 프레임워크는 너무나 많습니다. 이러한 프레임워크는 모든 주요 데스크톱 플랫폼에 사용할 수 있고, 이를 이용해 세련된 디자인에 반응 속도가 매우 우수한 GUI 애플리케이션을 바로 만들 수 있는데 새 UI 프레임워크를 만들어내야 했던 이유는 무엇일까요? 이유는 상황이 달랐기 때문입니다. 지금부터 기존 UI 프레임워크의 역사를 알아보고, Fleet으로 탄생한 제품을 개발하기 시작했을 때 그러한 선택지가 있었는지 살펴보겠습니다.
UI 접근 방식의 간략한 역사
GUI 프레임워크(GUI 자체가 아님)에 대한 아이디어와 접근 방식은 Alan Kay를 비롯한 Xerox PARC의 연구자 팀이 Smalltalk 프로그래밍 언어용 그래픽 환경을 개발한 70년대로 거슬러 올라갑니다. UI 프레임워크 초기부터 개발자들은 아키텍처 문제에 명확히 초점을 맞췄습니다. 예측할 수 없는 사용자의 행위와 그에 따른 UI 반응을 고려하고, 아키텍처 면에서 엉망인 애플리케이션이 나오는 것을 방지하면서, 화면에 여러 UI 구성 요소가 있는 이벤트 루프상에 애플리케이션을 구성하기란 쉬운 일이 아니었습니다. 이러한 접근 방식을 자세히 확인할 수 있는 한 가지 예는 Trygve M. H. Reenskaug의 메모입니다. 여기서 Trygve는 GUI 애플리케이션에 구조를 제공하는 일련의 아이디어인, 주목할 만한 Model-View-Controller 아키텍처 스타일, MVC의 인셉션에 대해 곰곰이 검토합니다.
MVC 스타일을 적용하는 명확한 방법이 없었으므로, 다른 용어(예: MVP – Model-View-Presenter)를 사용하여 이를 재구성하려는 수많은 시도가 있었습니다. 이를 지원한다던 UI 프레임워크는 저마다 특정한 구현 방법을 제공했고, 다음과 같은 몇 가지 일반적인 사항 외에는 서로 공통점이 많지 않았습니다.
- 프로그래밍 언어의 객체 지향 기능에 의존.
- 프레젠테이션에서 비즈니스 로직 분리(안타깝게도 분리한 후에 프레젠테이션에 또 특정 로직이 필요할 수 있음).
- 옵서버 패턴을 통해 변경 사항 관찰(이 패턴을 과하게 이용하는 소스를 읽으면 프로그램에서 진행되고 있는 사항을 파악하기가 너무 어려워져 관련 코드에서 가독성 문제 발생).
또 다른 개발의 방향도 있었습니다. 아키텍처를 양식으로 구조화하여 단순화하고, 양식에 일련의 제어 기능을 적용하며, 애플리케이션 상태를 양식의 구성 요소에 연결하는 로직을 사용하려는 시도였습니다. 1990년대에 Borland Delphi 및 Visual Basic이 대중화한 이 아이디어를 통해 Button1Click 세대의 개발자는 아키텍처에 대해 생각하지 않고 편하게 대규모 애플리케이션을 개발할 수 있었습니다. 객체 지향 성향의 연구자와 실무자는 이 접근 방식을 줄곧 비판했지만, 솔직히 아키텍처가 없든, 아키텍처 지침이 엄격하든, 양쪽에서 발생되는 큰 혼란을 보면 어느 것이 낫다고 하기가 어려울 때가 많았습니다.
Martin Fowler는 자신의 저서(안타깝게도 미완성), Further Patterns of Enterprise Application Architecture(엔터프라이즈 애플리케이션 아키텍처의 패턴 탐구)의 통찰력이 빛나는 부분(발췌)에서 UI 프레임워크 개발에 대한 이 두 가지 방향을 살펴보았습니다.
한편, 당시에는 PHP도 있었습니다. 90년대 중반에 시작된 PHP로 웹 애플리케이션을 개발하면 무척 편리했습니다. 시작하기가 너무나 쉬웠으며, 프로덕션으로 바로 실행할 수 있었습니다. PHP는 템플릿 처리를 대중화했습니다. 하나의 코드에 로직(프로그래밍 언어 지침)과 프레젠테이션(HTML 태그)을 혼합하는 접근 방식으로, MVC 순수주의자의 눈에는 끔찍한 범죄였습니다. 어떤 프로그래밍 언어도 개발자들로부터 이렇게 많은 반발을 받은 적이 없습니다. ‘PHP hate’를 구글에서 검색해보기만 해도 어땠는지 잘 알 수 있습니다. ‘깔끔한 코드 천국의 수호자들’은 여전히 이러한 이유로 PHP를 싫어합니다(또한 PHP가 소프트웨어를 개발하는 유일한 ‘올바른 방법’을 모르고 자란 많은 신입 개발자를 업계에 들여놓았기 때문일 수 있습니다).
하지만 프로그래밍 언어에 대해 판단하지 말고 이 새로운 아이디어가 무엇이며 어떠한 방향으로 발전하게 될지를 정확히 보면 좋겠습니다. 데이터를 보는 방식은 데이터를 조작하는 방식과 밀접하게 관련되어 있습니다. PHP를 사용하면 더 이상 로직과 프레젠테이션을 구분할 필요가 없습니다. 결국 대부분의 로직은 프레젠테이션에 있습니다. 프레젠테이션을 조작하는 주체는 보거나 손댈 수 없는 추상 데이터가 아니라 사용자이기 때문입니다. PHP는 고유한 디자인, 사용자의 행위에 대한 반응 및 기타 동작이 있는 활성 엔티티인 구성 요소가 필요함을 분명히 보여주었습니다. 구성 요소는 다른 구성 요소로 구성될 수 있으며, 일부 공유 상태를 통해 인접 구성 요소와 효과적으로 상호작용할 수 있습니다. 예를 들어, 한 구성 요소의 항목 목록은 첫 번째 구성 요소에서 선택된 항목에 따라 내용이 달라지는 다른 구성 요소로 변경 시그널을 전달할 수 있습니다.
Ajax(비동기 JavaScript 및 XML)는 웹 구성 요소를 웹 애플리케이션의 주요 빌드용 블록으로 사용하여 이 아이디어를 더욱 발전시켰습니다. 이러한 웹 구성 요소는 백엔드 및 프런트엔드 로직을 결합하고, 이를 프레젠테이션과 혼합했습니다. 좋든 나쁘든 이 아이디어는 현재의 웹 및 웹 개발 환경을 구현해주었습니다.
2010년대 초, React는 완전히 새로운 UI 개발 세상을 열었습니다. 프런트엔드 개발자 사이에서는 React 또는 Angular 중 어느 것이 더 나은지, 어떤 상태 관리자를 사용해야 하는지에 대해 의견이 분분할 수 있지만, 활성 UI 구성 요소 관련 아이디어에는 현재 다들 익숙해진 상태입니다. 심지어 데스크톱 애플리케이션 개발에 웹 기술을 적용하기도 합니다(결과물이 항상 완벽한 응답성과 디자인을 제공하지는 않지만 말이죠).
React가 선언적 프로그래밍 및 반응형 프로그래밍을 대세로 만든 공로는 인정해야 합니다. 이러한 기술은 이전에는 빛을 보지 못하여 프로덕션 프로그래밍 언어에서 거의 사용되지 않았지만, 가장 주목할 만한 요소인 UI 프레임워크를 포함하여, React와 함께 소프트웨어 개발의 많은 영역에 퍼져 나갔습니다.
2010년대 말에는 모바일 시스템용 UI 프레임워크 전반에 보급된 동일한 UI 접근 방식이 관찰되기 시작했습니다. 가장 두드러진 예로는 Apple의 SwiftUI 및 Google의 Jetpack Compose가 있습니다. 곧이어 데스크톱 시스템용 UI 프레임워크에서도 동일한 동향이 확인되었습니다. 2020년대 초에는 Google의 Jetpack Compose를 기반으로 JetBrains에서 제공하는 멋진 Compose for Desktop이 나왔습니다. 하지만 Fleet을 작업하기 시작했을 때 Jetpack Compose는 없었기 때문에 Noria가 탄생했습니다.
증분 계산
흥미롭게도, Noria의 핵심은 UI 프레임워크가 아닙니다. 증분 계산용 플랫폼입니다. 상호 종속된 구성 요소가 있는 여러 부분으로 구성된 긴 수학 공식이 있다고 가정해 보겠습니다. 이러한 공식은 스프레드시트와 유사하게 때때로 재계산이 필요하거나 종속된 구성 요소의 재계산을 트리거해야 합니다. Noria는 이러한 유형의 공식을 계산하고 다시 계산하는 데 탁월합니다. 여기서 기본 원칙은 불필요한 계산을 피하고 최종 결과를 생성하는 데 필요한 부분만 다시 계산하는 것입니다.
UI는 어떨까요? 이것도 그러한 종류의 공식으로 되어 있습니다. 구성 요소 및 하위 구성 요소로 구성된 트리 같은 구조입니다. 이러한 구성 요소는 다른 구성 요소의 일부인 동시에 다른 종속성을 가질 수도 있습니다. 이는 종속성에 대한 방향성 있는 비순환 그래프(DAG)를 만듭니다. 구성 요소 중 하나의 상태가 변경되면 직간접적으로 여기에 종속된 다른 구성 요소의 변경도 트리거됩니다.
일반적으로 증분 계산은 예상 결과가 변경되지 않을 것이라고 확인할 수 있는 경우 일부 계산 건너뛰기를 지원합니다. UI의 경우, 변경이 필요하지 않은 구성 요소는 손대거나 다시 그리지 않습니다.
이 간단한 Tic-Tac-Toe 게임에서 한 수 둔다고 가정해 보겠습니다.
전 | 후 |
셀이 있는 그리드가 있습니다. 오른쪽 상단 모서리를 클릭(X를 추가)할 때 어떤 셀이 변경되어야 할까요? 첫 번째 행의 셀이 승리했으므로 해당 셀을 다시 그려야 합니다. 다른 셀은 변경되지 않았으므로, 다시 그리지 않는 편이 좋습니다. 필요한 로직을 구현하기 위해 다음과 같이 구성 요소 및 계산을 구성합니다.
- 2D 그리드는 셀을 포함하며 행, 열, 패딩 등으로 레이아웃을 설명하는 방법을 제공합니다.
- 각 셀에는 내용에 대한 정보(X 또는 O가 그 안에 있는지 여부) 및 승리 행, 열 또는 대각선의 일부가 될 가능성에 대한 정보가 있으며 이러한 X 및 O의 상태가 변경될 때 다시 그릴 수 있습니다.
- 오른쪽 상단 셀을 클릭하면 내용이 변경되고 다시 그리기가 트리거되지만, 게임 종료 여부 확인 및 승리 셀의 해당 설정 변경도 트리거해야 합니다.
- 승리 셀의 설정을 변경하면 노란색 긋기 선으로 다시 그리기가 트리거됩니다.
따라서 사용자의 행위(셀 클릭)에 대한 반응으로 그리드가 부분적으로 다시 계산되고 일부 셀이 다시 그려지지만 다른 셀은 그대로 유지됩니다. Noria는 다음을 포함하여 이러한 동작을 처리하는 데 필요한 모든 것을 제공합니다.
- 이벤트 전달을 활성화하는 트리 구조 종속성과 함께 구성 요소의 레이아웃을 트리 구조로 설명하는 선언적 Kotlin DSL.
- 즉각적 반응을 구현하는 구성 요소에 대한 onClick-hook.
- 인접 노드의 재계산 트리거를 담당하는 트리 구조 외부의 종속성을 표현하기 위한
StateCell
.
내부적으로, Noria는 필요하지 않은 경우 재계산을 트리거하지 않습니다. 이는 모든 증분 계산 플랫폼의 표준 동작입니다. Noria 사용자는 전체 UI를 설명하는 함수를 선언적 방식으로 작성합니다. 이 함수는 무언가가 발생할 때마다 호출됩니다. 그러나 Noria는 이전 실행에서 변경된 사항과 변경되지 않은 사항을 정확히 파악하며, 재계산이 필요한 UI 트리 부분을 결정합니다.
UI 구성 요소 선언
몇 가지 코드 예시를 살펴보면서 Noria로 작업하면 어떤 느낌이 드는지 확인해봅시다.
이전 섹션에서 언급한 Tic-Tac-Toe 구성 요소는 Noria에서 다음과 같이 나타낼 수 있습니다.
val grid = Grid(3) { state { GridCell() } } val gameState = state { GameState() } clickable(onClick = { grid.checkEndOfGame(gameState) }) { vbox { grid.gridRows.forEach { hbox { it.forEach { cell(it, gameState) } } } } }
셀이 있는 그리드, 다음 차례와 게임 종료 상태를 담당하는 게임 상태가 있습니다. 처음 두 줄에 있는 2개의 state
함수를 주목하세요. 이러한 함수는 추가적인 종속성 생성을 담당합니다. 내용을 업데이트하면 내용을 읽는 구성 요소의 재계산이 트리거됩니다.
각 셀은 콘텐츠 렌더링을 담당하고 사용자 클릭에 반응합니다.
private fun UIContext.cell(gridCell: StateCell<GridCell>, gameState: StateCell<GameState>, ...) { val gs = gameState.read() val cell = gridCell.read() clickable(onClick = { ... }, propagate = Propagate.CONTINUE) { decorate(backgroundColor = ...) { layout { render(Rect(Point.ZERO, Size(size, size))) { ... } } } } }
사실상 cell
은 UIContext
클래스의 확장 함수로, Noria를 뒷받침하는 메커니즘을 구현합니다. 셀은 상태를 읽으므로 해당 정보에 대한 의존성을 표현합니다. Noria는 이 셀을 다시 렌더링해야 하는지 여부를 결정하는 동안 이 종속성을 사용합니다.
Noria 구성 요소는 툴팁이 있는 다음 텍스트 라벨처럼 간단할 수 있습니다.
withTooltip("Tooltip text") { uiText("Point on me") }
또한 Fleet 인스턴스에서 어김없이 볼 수 있는 텍스트 에디터나 기타 패널 및 창과 같이 매우 복잡할 수도 있습니다.
Noria는 다음을 포함하여 데스크톱 UI 및 기타 명백한 UI 프레임워크 기능을 구현하기 위한 모든 필수적 구성 요소를 제공합니다.
- 구성 요소 배치.
- 경계 제한 및 설정.
- 표시되는 부분 렌더링 및 스크롤 지원 제공.
- 초점 이동 정의.
- 오버레이 구현(예: 위 예제의 툴팁 또는 잘못된 코드 조각 옆의 오류 메시지).
사용자가 Fleet을 실행할 때마다 Noria 메커니즘은 가능한 최상의 UI 환경을 제공하기 위해 열심히 작동합니다.
요약
Fleet은 JVM용으로 자체 제작된 UI 프레임워크인 Noria로 구현됩니다. Noria에서는 Kotlin 기능을 사용하여 현대적인 선언적 방식으로 UI를 설명할 수 있습니다. Noria는 UI 구성 요소 간의 종속성을 표현하고 불필요한 다시 렌더링하는 것을 최소화하는 역할을 하는 증분 계산 코어를 기반으로 합니다.
Noria는 결코 혁신적이지 않습니다. 현대적 디자인 아이디어만 가져와서 UI 프레임워크에 적용하고 Kotlin 감각을 더할 뿐입니다. 하지만 지금까지 저희가 깨달은 바에 따르면 고유한 기본 UI 프레임워크를 개발하는 것은 유용하고 실제로 엄청나게 재미있을 수 있습니다!
Fleet의 내부 구조 시리즈는 끝나지 않았습니다. Fleet 내부에 대해 공유할 세부 정보가 여전히 많이 있습니다. 새로운 소식을 기다려 주세요!
게시물 원문 작성자