Declarative React, and Inversion of Control

2022. 9. 3. 21:51Frontend

 

 

 

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의 설계원칙과 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)

The question is: "what aspect of control are they inverting?" When I first ran into inversion of control, it was in the main control of a user interface. Early user interfaces were controlled by the application program. You would have a sequence of commands like "Enter name", "enter address"; 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. -martinfowler

 

이렇게 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."

 

개발자는 React에게 렌더링할 Element를 "선언"하기만 하면 됩니다

 

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

 

문제 해결을 위한 근본적인 추상화

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 등의 작업을 "React renders my component descriptions" 한마디로 끝낼 수 있다는 것입니다. 이 추상화로 인해 우리는 "어떤 화면"을 렌더링할 것인지를 고민하는 데에 더 많은 시간을 사용할 수 있으며, 이 화면을 "어떻게" 렌더링해야 할 지에 대한 고민은 React 에게 온전히 맡길 수 있게 됩니다.

 

 

이는 Clean Architecture에서 이야기하는 Usecase Layer와도 비슷하다고 할 수 있는데, 실제로 "렌더"라는 usecase interface에 의존해서 UI를 선언하고, 이 "렌더"라는 기능이 어떻게 구현되어 있는지(Infrastructure)에 대해서는 개발자가 신경쓰지 않기 때문입니다. ("렌더"라는 기능이 어떻게 구현되어 있는지는 React에서 신경쓰게 됩니다.)

 

 

clean architecture는 추상화 레이어들을 통해 의존성을 관리하는 방법을 제시합니다

 

 

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가 자체적으로 수행할 수 있음을 의미합니다.

 

 

 

 

 

또한 브라우저가 매 프레임마다 작업을 수행하는 것을 가로막지 않기 위해 브라우저가 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

https://en.wikipedia.org/wiki/Declarative_programming

반응형