본문 링크 (Original Link)

단방향 사용자 인터페이스 아키텍쳐

2018.12.12

# • #

by André Staltz, translated by pilgwon

이 글은 “단방향 데이터 플로우” 아키텍쳐라고 불리는 것들에 대해 빠르게 살펴보는 글이며 초심자들의 튜토리얼을 위해 작성된 글이 아니라 각자의 차이점과 특징을 알아보는 목적의 글입니다. 마지막 부분에는 다른 아키텍쳐들과 확연하게 차이가 있는 새로운 아키텍쳐를 소개할 예정입니다. 이 글에선 클라이언트 사이드 웹 UI 프레임워크만을 다뤘습니다.

용어 정리

시작하기전에 공통으로 쓰이는 용어를 정리하고 가지 않으면 혼란이 생길테니 한 번 정리하고 가겠습니다.

사용자 이벤트(User events) 는 사용자가 직접적으로 조작하는 입력 기기에 의해 발생하는 이벤트를 의미합니다. 예를 들면 마우스 클릭, 스크롤, 키보드 입력, 터치스크린에 터치 등이 있습니다.

앞으로 설명할 아키텍쳐들은 “View”라는 용어를 완전히 다른 의미로 사용할 것입니다. 그러니 우리는 “View”를 이해하기 위해 “rendering”을 사용하겠습니다.

사용자 인터페이스 렌더링(User interface rendering) 은 화면에 그려지는 시각적인 출력결과를 의미합니다. 보통 HTML이나 JSX처럼 상대적으로 높은 단계의 선언형 코드로 표현됩니다.

사용자 인터페이스 (UI) 프로그램 은 사용자 이벤트를 입력으로 받거나 출력으로 렌더링하는 프로그램입니다. 프로세스는 단 한 번만 작동하는게 아니라 계속해서 작동합니다.

DOM과 다른 레이어(프레임워크와 라이브러리)는 사용자와 아키텍쳐 사이에 있다고 가정합니다.

내부 모듈의 소유권은 화살표로 나타낼 수 있습니다. A--> BA -->B 는 다르다는 뜻입니다. 후자는 반응형 프로그래밍(Reactive programming)인 반면, 전자는 수동형 프로그래밍(Passive programming)입니다. 더 자세한 사항은 여기서 확인해보세요.

단방향 아키텍쳐의 서브 컴포넌트들이 생긴 모양과 전체 모양이 같은 방식일 떄 프랙탈(fractal) 이라고 합니다.

프랙탈 아키텍쳐에서는 전체가 다른 더 큰 어플리케이션의 컴포넌트로 패키징될 수도 있습니다.

비 프랙탈 아키텍쳐에서는 반복되지 않는 부분들을 orchestrators 라고 부르며 이들은 계층적 구성을 가지고 있습니다.

FLUX

필수로 언급해야 할 첫 번째가 바로 Flux입니다. Flux가 선구자는 아니지만 적어도 인기도 면에서는 가장 유명한 단방향 아키텍쳐라고 생각합니다.

구성 요소:

image-flux

특징:

Dispatcher. 이벤트 버스이기 때문에 싱글톤입니다. 많은 Flux 변종들이 dispatcher에 대한 필요성을 삭제하고, 다른 단방향 아키텍쳐들은 dispatcher와 같은 역할을 하는 것을 가지고 있지 않습니다.

오직 View만이 구성 가능한 컴포넌트를 가지고 있습니다. 계층적인 구조는 Stores나 Actions가 아닌 오직 리액트 컴포넌트에서만 일어납니다. UI 프로그램인 리액트 컴포넌트는 종종 내부적으로는 Flux 아키텍쳐로 쓰이지 않습니다. 그런 이유는 Flux가 비 프랙탈일지라도 orchestrator가 되는 부분은 Dispatcher와 Stores가 됩니다.

사용자 이벤트 핸들러들은 렌더링 단계에서 정의됩니다. 다른 말로 하자면 리액트 컴포넌트의 render() 함수는 사용자와의 상호작용(렌더링과 사용자 이벤트 핸들러)의 방향을 조종합니다. 예: onClick={this.clickHandler}.

REDUX

Redux는 싱글톤 Dispatcher가 싱글톤 Store로 적응된 Flux의 변종 중 하나입니다. Store는 더 이상 바닥부터 다 만드는 것이 아니라 주어진 store factory의 reducer 함수에 의해 생성됩니다.

구성 요소:

image-redux

특징:

Store를 위한 Factory. Store는 reducer 함수들의 구성을 인자로 받는 createStore() 팩토리 함수를 사용해서 생성됩니다. 또한 미들웨어 함수를 인자로 받는 메타 팩토리 함수 applyMiddleware()도 존재합니다. 미들웨어(Middleware)는 store의 dispatch() 함수와 추가적으로 연결된 기능들을 오버라이딩하는 매커니즘입니다.

Provider. Redux는 UI 프로그램을 만드는데에 사용된 “View” 프레임워크에 대한 고집이 없습니다. 그 프레임워크는 React, Angular 등 무엇이든 될 수 있습니다. 이 아키텍쳐의 문맥을 읽어보면 “View”는 UI 프로그램을 의미합니다. Flux처럼 Redux도 디자인적으론 프랙탈이지 않고 Store가 orchestrator가 됩니다.

사용자 이벤트 핸들러는 렌더링 단계에서 정의될 수도 아닐수도 있습니다. 그 여부는 Provider의 손에 달려있습니다.

BEST

Famous Framework가 Behavior-Event-State-Tree(BEST)를 Controller가 Behavior와 Event라는 두 개의 단방향 요소로 분리되는 특징으로 MVC의 변형으로 소개했습니다.

구성 요소:

image-best

특징:

멀티 패러다임. State와 Tree는 완전히 선언적입니다. Event는 명령형입니다. Behavior는 함수형입니다. 어떤 부분은 반응형이고 어떤 부분은 수동형입니다. 예. Behavior는 State에 대해 반응적이고 Tree는 Behavior에 대해 수동적입니다.

Behavior. 이 글의 다른 아키텍쳐에선 볼 수 없는 특징입니다. Behavior는 UI 렌더링 (Tree)을 다이나믹 속성에서 분리시킵니다. Tree는 HTML, Behavior는 CSS라고 비교할 수 있을 정도로 둘은 다른 존재입니다.

사용자 이벤트 핸들러는 렌더링에서 독립적으로 정의됩니다. BEST는 사용자 이벤트 핸들러를 렌더링에 붙이지 않는 몇 안되는 단방향 아키텍쳐입니다. 사용자 이벤트 핸들러는 Tree가 아니라 Event에 붙어있습니다.

이 아키텍쳐의 컨텍스트에 의하면 “View”는 Tree이고 “Component”는 Behavior-Event-Tree-State 튜플입니다. Component는 UI 프로그램을 의미하니 BEST는 프랙탈 아키텍쳐라고 할 수 있습니다.

MODEL-VIEW-UPDATE

보통 “Elm 아키텍쳐“로 알려져있습니다. Model-View-Update는 Redux와 비슷하다고 생각하실 수 있는데, 이는 Redux가 Model-View-Update에 영감을 받았기 때문입니다. Model-View-Update는 웹에서 사용되는 함수형 프로그래밍 언어인 Elm을 주로 사용하는 순수한 함수형 아키텍쳐입니다.

구성 요소:

image-model-view-update

특징:

어디서든지 계층적인 구조를 가집니다. 이전의 아키텍쳐들은 오로지 “View”에서만 계층적인 구조를 가지고 있었습니다. 그러나 MVU(Model-View-Update) 아키텍쳐에선 Model과 Update에서도 구조를 찾을 수 있습니다. 심지어 Action들도 중첩 Action을 가질 수 있습니다.

컴포넌트들이 조각으로 추출될 수 있습니다. 어디서든지 계층적인 구조를 가지는 특징때문에 Elm 아키텍쳐의 “컴포넌트”는 Model 타입, 초기 Model 인스턴스, View 함수, Action 타입 그리고 Update 함수로 이루어진 튜플을 의미합니다. 이 중 하나라도 없다면 아키텍쳐가 컴포넌트를 만드는 것을 허락하지 않을 것입니다. 각 컴포넌트는 UI 프로그램이고 이 아키텍쳐도 프랙탈입니다.

MODEL-VIEW-INTENT

RxJS의 Observable을 기반으로 한 완전한 반응형 단방향 아키텍쳐로 소개된 적이 있는 Model-View-Intent는 Cycle.js 프레임워크의 초기 아키텍쳐 패턴입니다. Observable 이벤트 스트림은 기본적으로 어디서든지 사용되고 Observable에 올려진 함수들은 아키텍쳐의 조각입니다.

구성 요소:

image-model-view-intent

특징:

Observable에 심하게 기반하고 있습니다. 아키텍쳐의 각 부분들의 결과는 Observable 이벤트 스트림으로 표현됩니다. 이러한 이유로 Observable 없이는 “데이터 플로우” 또는 “변경”을 표현할 수 있는 방법이 존재하지 않습니다.

Intent. BEST의 Event와 대략적으로 비교해보면, 사용자 이벤트 핸들러는 렌더링에서 분리되어 Intent에서 정의됩니다. BEST와는 다르게 Intent는 Flux, Redux, Elm과 같이 action의 Observable 스트림을 생성합니다. 또 Flux와 다르게 MVI(Model-View-Intent)의 action은 Dispatcher나 Store에 직접적으로 보내지진 않습니다. 그것들은 Model에서 받으려고하면 간단하게 사용가능합니다.

완전히 반응형입니다. 사용자의 렌더링은 View의 출력에 대해 반응형이고, Model의 출력에도 반응형이며, Intent의 출력인 action에도 반응형이고 사용자 이벤트에게도 반응형입니다.

MVI 튜플은 UI 프로그램입니다. 이 아키텍쳐는 모든 custom element가 MVI로 구현되어있다면 프랙탈의 형태를 가집니다.

NESTED DIALOGUES

이 블로그 글은 Nested Dialogues 를 Cycle.js를 위한 새로운 단방향 아키텍쳐로 소개하기 위해 작성되었으며 다른 접근법은 오직 Observable에 기반합니다. 이것은 Model-View-Intent 아키텍쳐에 있어서 혁명입니다.

Model-View-Intent 시퀀스가 하나의 함수인 “Dialogue”로 작성될 수 있다는 사실에서 시작하겠습니다.

image-nested-dialogues-1

위 그림이 제안하듯이 Dialogue 는 사용자 이벤트의 Observable을 입력(Intent의 입력)으로 받는 함수이고 렌더링(View의 출력)의 Observable을 출력합니다. 그러니 Dialogue는 UI 프로그램이라고 할 수 있습니다.

우리는 Dialogue의 정의를 일반화해서 사용자를 넘어서 다른 타겟을 허용할 것입니다. 물론 각 타겟에 대해 입력과 출력 모두 Observable을 제공하면서요. 예를 들어 Dialogue가 사용자와 HTTP위의 서버에 접속되면 Dialogue는 사용자 이벤트의 Observable 그리고 HTTP 리스폰스의 Observable, 이 두 가지의 Observable을 입력으로 받을 것입니다. 그 다음엔 렌더링의 Observable과 HTTP 리퀘스트의 Observable, 이 두 가지의 Observable을 출력할 것입니다. 이게 바로 Cycle.js의 Driver에 대한 개념입니다.

다음은 Model-View-Intent가 Dialogue로 재구성됐을 경우를 그림으로 나타낸 것입니다.

image-nested-dialogues-2

더 큰 프로그램의 서브 컴포넌트 UI 프로그램으로 Dialogue 함수를 재사용하는 것은 Dialogue를 중첩할 수 있느냐에 대한 문제입니다.

image-nested-dialogues-3

Dialogue의 레이어 사이에서 Observable을 선으로 이어놓은 것은 데이터 플로우 그래프입니다. 비순환 그래프일 필요는 없습니다. 데이터 플로우 그래프에서 순환이 필요한 서브 컴포넌트의 다이나믹 리스트의 경우도 있습니다. 그러한 경우는 이 블로그 글의 범위를 벗어납니다.

Nested Dialogues는 사실은 메타 아키텍쳐입니다. 컴포넌트의 내부 구조에 대한 컨벤션은 없기 때문에 앞에서 설명한 어떤 아키텍쳐든 Nested Dialogue 컴포넌트에 임베드할 수 있습니다. Dialogue에서 유일하게 꼭 지켜야 하는 컨벤션은 입력은 반드시 Observable 또는 Observable의 컬렉션이어야 하고, 출력 또한 Observable 또는 Observable의 컬렉션이어야 한다는 것입니다. 만약 UI 프로그램이 Flux 또는 Model-View-Update 등으로 구성되었다면 입력과 출력을 Observable로 표현할 수 있게 되는데 그러면 그 UI 프로그램은 Nested Dialogues 프로그램의 Dialogue 함수에 임베드 될 수 있습니다.

그러면 이 아키텍쳐는 (Dialogue 인터페이스와 관련되어 있을때만) 프랙탈의 형태를 가지며 일반적인 아키텍쳐가 됩니다.

TodoMVC에서 구현 방식을 보실 수 있으며 이 작은 앱에서는 Cycle.js의 Nested Dialogue의 예시를 볼 수 있습니다.

편향된 결론

Nested Dialogue의 보편성과 정밀함 덕분에 이론적으로 다른 아키텍쳐를 서브 컴포넌트로 임베드할 수 있는 반면에 저는 주로 Cycle.js 어플리케이션을 구성하기 위한 용도에 더 흥미를 가지고 있습니다. 저는 똑같은 구조 를 제공하더라도 자연스럽고 유연하게 느껴지는 UI 아키텍쳐를 찾아왔었습니다.

전 Nested Dialogue가 자연스럽다고 믿습니다. 왜냐하면 이 아키텍쳐가 직접적으로 전형적인 UI 프로그램이 하는 것(사용자 이벤트를 입력(input Observable)으로 받고 렌더링을 출력(output Observable)으로 생산하는 진행중인 프로세스(Observable이 진행중인 프로세스입니다))을 표현하기 때문입니다.

Dialogue의 내부 구조는 어떤 패턴으로도 자유롭게 채울 수 있다는 것을 우리 모두 봤기 때문에 유연하다는 사실에도 동의하실 것입니다. 이는 Model-View-Update가 엄격한 컨벤션의 구조를 가지고 있는 것과는 대조됩니다. 프랙탈의 형태를 가지는 아키텍쳐는 비프랙탈 아키텍쳐에 비해 더 재사용하기 쉽다는 측면에서 저는 Nested Dialogue가 프랙탈 속성을 가진다는 것이 아주 좋습니다.

그러나 구조에 대한 기본적인 구조를 제시하는 것은 초기 개발시에 도움이 되는 것이 사실입니다. 저는 Dialogue의 내부 구조가 Flux 일 수 있다고 믿고 있지만 Model-View-Intent가 Observable 입력/출력 인터페이스에 더 자연스럽게 어울린다고 생각합니다. 그래서 Dialogue를 MVI로 구현하지 않으려고 해도 대부분의 경우 MVI로 구조화할 것입니다.

저는 이 Nested Dialogue가 사용자 인터페이스 아키텍쳐 중 최고라고 허세부리고 싶지 않습니다. 왜냐하면 저는 이걸 이제 막 발견했고 야생에서 직접 써보며 장점과 단점을 알아봐야하기 때문입니다. 그럼에도 불구하고 Nested Dialogue는 지금 저에게 가장 좋은 패라고 생각합니다.