Declarative React, and Inversion of Control

선언형 UI 라이브러리로서의 React에 대해 살펴봅니다.

Yeoul Kim
Team QANDA

--

Lin Clark — A Cartoon Intro to Fiber — React Conf 2017

Overview

React 공식문서의 Design Principles 부분을 보면, 다음과 같은 문구가 있습니다.

Even when your components are described as functions, when you use React you don’t call them directly. Every component returns a description of what needs to be rendered, and that description may include both user-written components like <LikeButton> and platform-specific components like <div>. It is up to React to “unroll” <LikeButton> at some point in the future and actually apply changes to the UI tree according to the render results of the components recursively.

요약하면 “컴포넌트가 함수(Functional Component)로 작성되어 있더라도, 해당 컴포넌트를 렌더하고 DOM에 적용하는 것은 React의 책임이므로, 개발자가 리액트를 사용해서 개발할 때 직접 함수를 호출하지 않는다”는 의미인데, 이는 React의 설계원칙과 React가 지향하는 패러다임과 깊은 연관이 있습니다. 이번 포스팅에서는 이에 대해 살펴보도록 하겠습니다.

콴다 프론트엔드 팀에서는 대부분의 웹 프로젝트와 웹뷰 프로젝트를 ReactJs + Typescript의 기술 스택(SEO대응, 저사양 디바이스에서의 성능 개선등을 위해 서버사이드 렌더링이 필요한 프로젝트의 경우에는 +NextJs)으로 개발하고 있기 때문에 React(이하 리액트)의 동작 원리에 대해서 많은 관심을 가지고 정리하고 공유하는 것을 중요하게 생각하고 있습니다.

Declarative React

In computer science, declarative programming is a programming paradigm that expresses the logic of a computation without describing its control flow. -wiki(en)

선언형 프로그래밍은 두 가지 뜻으로 통용되고 있다. 한 정의에 따르면, 프로그램이 어떤 방법으로 해야 하는지를 나타내기보다 무엇과 같은지를 설명하는 경우에 “선언형”이라고 한다. 또 다른 정의에 따르면, 프로그램이 함수형 프로그래밍 언어, 논리형 프로그래밍 언어, 혹은 제한형 프로그래밍 언어로 쓰인 경우에 “선언형”이라고 한다 -wiki(ko)

React가 “선언적”(Declarative)인가에 대해 논하기 전에 먼저 선언적(Declarative)인 것이 무엇인가에 대해서 살펴보는 것이 좋습니다. 위에서 언급된 정의에 따르면 “선언형” 프로그래밍이란, 프로그램을 작성할 때 프로그램이 “어떤 방식으로” 목적지까지 도착할 것인지를 나타내는 것(Imperative. 명령형)이 아닌, 프로그램(혹은 상태)이 “무엇과 같은가”를 설명하는 것이라고 말합니다.

선언형과 명령형의 차이점에 대해 답변한 stackoverflow 글을 보면, Collection에서 odd numbers를 필터링하는 예제를 통해 이를 설명하고 있습니다. 명령형(Imperative) 프로그래밍의 경우 프로그램이 목적지까지 도착하기 위해 거쳐야 할 모든 단계들을 다음과 같이 컴파일러에게 알려줌으로써, 문제를 해결합니다.

  1. Create a result collection
  2. Step through each number in the collection
  3. Check the number, if it’s odd, add it to the results
List<int> results = new List<int>();
foreach(var num in collection)
{
if (num % 2 != 0) results.Add(num);
}

반면, 선언형 프로그래밍의 경우 프로그램이 목적지까지 도착하기 위해 어떻게 해야할지를 하나씩 알려주는 것이 아니라, "내가 원하는 동작"을 명시하는 코드를 작성합니다. (그 동작을 어떻게 수행할지에 대해서는 자세하게 명시할 필요가 없습니다.)

var results = collection.Where( num => num % 2 != 0);

Here, we're saying "Give us everything where it's odd", not "Step through the collection. Check this item, if it's odd, add it to a result collection."

그렇다면 선언형 프로그래밍과 명령형 프로그래밍의 이러한 차이가 User Interface의 관점에서는 어떤 것을 의미할까요? <div> 태그 안에 <h1> 태그가 있고, 그 안에 “Chocolate Cookie”라는 문구가 적힌 HTML 문서를 렌더링하고 싶다고 생각해보겠습니다. 명령형(Imperative) 프로그래밍 방식으로 이 UI를 렌더링하는 경우, 다음과 같이 DOM을 어떤 순서로 조작할지에 대해 하나씩 순차적으로 알려주어야 할 것입니다. (For every interaction, we provide step-by-step DOM mutations to reach the desired state of UI.)

The imperative approach is when you provide step-by-step DOM mutations until you reach desired UI

function addCookieToBody() {
const bodyTag = document.querySelector('body')
const divTag = document.createElement('div')
let h1Tag = document.createElement('h1')
h1Tag.innerText = "Chocolate Cookie"
divTag.append(h1Tag)
bodyTag.append(divTag)
}

반면, 선언형(Declarative) 프로그래밍 방식으로 렌더링하는 경우, 해당 화면을 렌더링하기 위한 각각의 단계를 알려주는 것이 아니라, 렌더링이 끝난 후에 보기를 원하는 최종 결과물을 “선언”하고, 해당 결과물을 렌더링하는 것은 UI 라이브러리의 책임으로 넘기게 됩니다. 그리고 바로 이것이 우리가 React를 사용해서 컴포넌트를 렌더링하는 방식입니다.

React를 사용해서 렌더링되어야 하는 컴포넌트를 개발할 때, 우리는 특정 상태에서 그려져야 하는 화면의 최종 모습을 React에게 전달하는 방식으로 개발합니다. 그리고 이 최종 모습을 렌더링하기 위해 task들을 스케줄링하고, DOM에 반영하는 것은 React의 책임이 됩니다. (We don’t provide step-by-step instructions to reach the desired UI. Instead, we describe the final UI we want for each scene.)

The declarative approach is when you describe the final state of the desired UI

function RenderCookie() {
return (
<div>
<h1>Chocolate Cookie</h1>
</div>
)
}

React는 이렇게 “선언형” 방식으로 컴포넌트를 개발하도록 안내하고 있으며, 이에 따라 React 개발자는 매 렌더마다 DOM을 어떻게 조작할지에 대해 신경 쓸 필요없이, 특정 상태에 따른 최종 UI의 결과물을 “선언”해서 React Element의 형태로 React에 전달하고, 이를 DOM에 반영하는 것은 React의 책임으로 넘기게 됩니다.

A declarative style, like what react has, allows you to control flow and state in your application by saying “It should look like this”. An imperative style turns that around and allows you to control your application by saying “This is what you should do”. — stackoverflow

Inversion of Control (IoC)

your program would drive the prompts and pick up a response to each one. With graphical (or even screen based) UIs the UI framework would contain this main loop and your program instead provided event handlers for the various fields on the screen. The main control of the program was inverted, moved away from you to the framework. — Martin Fowler

이렇게 React에게 내가 그리고 싶은 UI를 React Element들의 묶음들로 “선언”해서 React에게 전달하면, React는 실제로 이 “선언”을 화면에 그리기 위한 작업들을 수행합니다. 이 과정을 통해 자연스럽게 DOM에 화면을 렌더할 책임이 개발자에서 React로 넘어오게 되며 이는 DOM을 조작해서 UI를 화면에 그리는 작업에 대한 제어권이 역전되었음(Inversion of Control)을 의미합니다. (The main control of the program was inverted, moved away from you to react.)

Lin Clark — A Cartoon Intro to Fiber — React Conf 2017

이렇게 “렌더링”에 대한 책임이 개발자에서 React로 넘어오게 됨으로써 얻게 되는 주요한 이점들이 있습니다.

Fundamental Abstraction for Problem Solving

As you can see, the React way focuses on the result and further describes it in the render block of code. Simply put, “what I want rendered on the page is going to look like this and I don’t care about how you get there.

좋은 런타임은 직면한 문제와 일치하는 근본적인 추상화를 제공해 줍니다.(A good runtime provides fundamental abstractions that match the problem at hand) 여기서 추상화라는 것은 개발자가 표현하고자 하는 UI와 실제 이를 렌더링하기 위해 처리해야 하는 Task Splitting, Task Scheduling, Reconcilation, DOM Control 등의 작업을 “Anyway, React renders!” 한마디로 끝낼 수 있다는 것입니다. 이 추상화로 인해 우리는 “어떤 화면”을 렌더링할 것인지를 고민하는 데에 더 많은 시간을 사용할 수 있으며, 이 화면을 “어떻게” 렌더링해야 할 지에 대한 고민은 React 에게 온전히 맡길 수 있게 됩니다.

이는 “Clean Architecture”에서 이야기하는 Usecase와 같은 맥락에 있다고 할 수 있는데, 실제로 개발자는 Render라는 Usecase를 사용해서 UI를 선언하고, Render라는 기능이 어떻게 구현되어 있는지(Infrastructure), DOM은 언제 어떻게 조작하고, 각각의 Task들은 어떻게 나누는지등등에 대해서는 개발자가 신경쓸 필요가 없기 때문입니다. 이는 “리액트의 책임” 입니다.

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

Support for Concurrency & Batching

This is a subtle distinction but a powerful one. Since you don’t call that component function but let React call it, it means React has the power to delay calling it if necessary. In its current implementation React walks the tree recursively and calls render functions of the whole updated tree during a single tick. However in the future it might start delaying some updates to avoid dropping frames.

React가 Component의 Description(React Element들에 대한 정보)을 가지고 DOM을 조작해서 화면을 그리는 책임을 가져가게 된다는 것은 React가 필요에 의해 렌더링을 위한 여러 작업들의 우선순위를 지정하거나, 작업을 수행하는 것을 미룰 수 있음을 의미 합니다.

이는 React 18에서 소개된 “Concurrent React” 에서 확인할 수 있는데, User Event(Mouse Click, Keyboard Input)이벤트와 같이 상대적으로 빠르게 반영되어야 하는 이벤트들에 의한 렌더링 작업들을 우선적으로 처리하고, Background Data Fetching과 같은 이벤트들에 대해서는 상대적으로 나중에 반영하는 등의 성능 최적화를 React가 자체적으로 수행할 수 있음을 의미합니다.

Lin Clark — A Cartoon Intro to Fiber — React Conf 2017

또한 브라우저가 매 프레임마다 작업을 수행하는 것을 가로막지 않기 위해 브라우저가 Call Stack을 차지하지 않는 시점에 렌더링 작업을 수행하게 할 수 있으며, 성능 최적화를 위해 개념상 동일한 작업들을 한번에 처리하는 Batching 처리들도 제공합니다. 이 모든 기능들을 React 에서 제공하고, 지속적으로 개선하며 유지보수하고 있기 때문에 개발자는 이를 신경쓰지 않아도 됩니다.

If something is offscreen, we can delay any logic related to it. If data is arriving faster than the frame rate, we can coalesce and batch updates. We can prioritize work coming from user interactions (such as an animation caused by a button click) over less important background work (such as rendering new content just loaded from the network) to avoid dropping frames.

이외에도 React가 렌더링에 대한 책임을 가져가게 됨으로써 얻을 수 있는 여러 이점들이 있는데, 추가적인 내용들은 Dan Abramov의 “React as a UI runtime” 글을 참고해주세요.

Conclusion

React는 UI 렌더링을 위한 라이브러리로, 개발자에게 런타임에 대한 좋은 추상화를 제공합니다. 이를 통해 개발자는 컴포넌트에 대한 선언만을 React Element의 형태로 React에게 제공하게 되며(Declarative), React는 개발자로부터 명세를 DOM에 반영할 책임을 넘겨받아 이를 렌더합니다.(IoC)

React가 지향하는 이러한 설계 원칙으로 인해, 개발자는 컴포넌트에 대한 선언을 유지한 채 React 버전을 업데이트하는 것 만으로도(e.g 17 to 18) 렌더링에 대한 성능을 끌어올릴 수 있으며, 사용자에게 제공해야 하는 “UI”, 즉 화면에 대해 더 심도깊은 고민을 할 수 있게 됩니다. React 공식문서의 한 부분을 인용하며 글을 마칩니다.

React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes.

Reference

https://martinfowler.com/articles/injection.html#InversionOfControl

https://overreacted.io/react-as-a-ui-runtime/

https://alexsidorenko.com/blog/react-is-declarative-what-does-it-mean/

https://reactjs.org/docs/design-principles.html#scheduling

--

--