React 와 Flux

그니·2023년 8월 3일
0
post-thumbnail

출처


  • 페이스북은 2011년 가장 대중적인 아키텍처 중 하나 였던 MVC 를 포기하고 새로운 아키텍처를 도입.
  • 이전부터 UI 개발 하면서 2가지 큰 문제를 직면.
  1. 쉽게 잘 망가진다.
  2. 빠르게 수정하는 것이 어렵다.
    -> "예측 불가능한 코드가 문제의 근원"이라는 결론.

이미지1
1. Controller 에 너무 많은 제어권이 존재
2. 그 모든 제어를 개발자가 직접 주관적인 판단하에 진행
-> 시스템이 아닌 개인에 의존하는 이러한 방식은 예측 가능성을 깨지기 쉬운것으로 만듬!

이미지2
1. 그렇기 때문에 Data, View 에 시스템을 도입하는 것이 "예측 가능성"을 지켜주리라 생각함.
-> 예측 가능한 '데이터' 를 위해 'Flux' 라는 아키텍처를
-> 예측 가능한 'UI' 를 위해 'React' 라는 시스템을 도입.

모든 어플리케이션은 'Data', 'UI' 를 어떻게 제어할 것인지에 대한것으로 구성된다.
MVC 는 Controller 가 Model, View의 제어권을 갖는 것을 의한다.

동적인 프로세스와 정적인 프로그램의 갭

“우리의 지적 능력은 정적인 관계를 정복하는데에는 준비가 되어 있으나, 시간에 따라 진화하는 프로세스를 시각화 할 수 있는 힘은 상대적으로 뛰어나지 않다. 이러한 이유로 정적인 프로그램과 동적인 프로세스 사이의 개념적인 격차를 단축하여 프로그램과 프로세스 사이의 대응 및 차이를 가능한 만큼 사소한 것으로 만들기 위해 최선을 다해야 한다.”

  1. Alice가 오프라인으로 변경되었습니다.
  2. Charles가 모바일로 재접속했습니다.
  3. Carol이 PC로 재접속 했습니다.
  4. Charles이 오프라인으로 변경되었습니다.
  5. Alice가 모바일로 접속했습니다.
  6. Dan이 모바일로 접속했습니다.

위 프로세스에서 더 복잡해질수록 결과를 예측하는 것은 어려울 것이다.
-> 이와 같은 상황을 '동적인 프로세스'

현대 웹 프레임워크로 개발 시 위와 같은 프로세스에 대해서 고민을 하나?
-> 현재 상태에 대한 Render 를 데이터에 맞춰서 할 뿐.
-> '어떠한 프로세스'를 거쳤는지 중요하지 않다.
-> 현재 데이터만 중요할 뿐.

정적인 프로그램이 가지는 하나의 상태에만 집중하는 것.

-> 이는 너무나도 당연해 보이지만 리액트가 탄생하기 전 동적인 프로세스와 정적인 어플리케이션의 시각화는 갭 차이가 컸다. -> 예측 불가능한 UI를 만든다고 생각.

멱등성

  • 요청이 같다면 항상 결과도 같다는 특징.
    -> 1 + 1 = 2 의 대해 예외가 없는것처럼.
    -> 멱등성은 React의 발전에 있어 많은 영향을 줌.
    또한 멱등성은 함수형 프로그래밍의 참조 투명성 과 같은 여러 개념에 기본이 된다.
    (1, 2) => 3 일 경우 (3) => 1 + 2 로 바꿔도 무관하다.
    즉, 함수의 표현식에 대한 결과를 표현식에 추가해도 함수의 결과값은 똑같다.

리액트 팀은 멱등성을 유지하는 것만이 예측 가능성을 지키는 일이라 생각했다.
허나,
데이터가 바뀔 때마다 DOM 요소를 수정하는 것은 멱등성을 해친다. 하나의 데이터에 여러 수정을 통해서 변경하는 점은 옳지 않다.
-> 대신 데이터가 바뀔 때마다 완전히 새로운 DOM 요소를 그리는 방법을 택한다.
1. 변경점이 발생
2. 매번 새로운 데이터를 요청받고
3. 매번 새로운 UI를 그림.
-> 데이터마다 결과 스냅샷을 가지고 있는 것 처럼

이러한 것들이
1. 설명 가능한 순간적인 UI
2. 주어진 입출력에 관해 결과를 예측
3. 쉬운 테스트
와 같은 특징을 만들어낸다고 믿음.

Stateful Browser 와 Stateless한 Rendering

  • 브라우저는 이미 동적인 프로세스(Stateful 혹은 상태)가 있는데, 어떻게 UI 를 그리는 일은 상태가 없는 정적인 화면을 출력하는게 가능할까?
buttonAClick() {
	const buttonA = document.querySelector(".buttonA");
	if(buttonA.isOff) {
		buttonA.style.backgroundColor = "red";
		buttonA.style.innerText = "on";
	}
	else {
		buttonA.style.backgroundColor = "gray";
		buttonA.style.innerText = "off";
	}
}

// 이를 매번 스크린 샷을 찍듯 매번 새로운 화면을 만들기 위해선 다음과 같이 변경, stateless 렌더링 방식

buttonAClick(color, text) {
	const buttonParent = document.querySelector(".buttonParent");
	buttonParent.innerHTML = `
		<button style="color:${color};">${text}</button>
	`
}

더 이상 이전 버튼이 상태가 무엇인지에 따라 화면이 달라지는 것이 아닌, 순수하게 현재 데이터를 기반으로 작성.
-> 이전 상태가 무엇이었는지, 어떤 과정을 거쳤는지 더 이상 알 필요가 없기 때문에 예측 가능성을 지킴!

2가지 문제점
1. 모든 것을 매번 새로 그리는 것은 성능에 대한 효율이 매우 떨어진다.
2. DOM을 새로 그리기 위해서는 전부 지워야 한다. 그려야 할 DOM이 복잡할 수록 유저들은 흰 화면을 오래 보게 됨.

개념적인 격차의 단축

  • 정적인 프로그램 <-> 동적인 프로세스 의 차이를 물리적으로 줄인다는 의미가 아니다.
    -> 대신 개념적으로 이 격차를 줄이면?
    -> DOM을 매번 새로 만드는 것이 아니라, '작성 방법' 과 '사고 방식'은 DOM을 새로 만들 듯이 생각
    -> 실제로는 DOM을 여전히 수정하는 것은 변함이 없는 것.
    REACT IS LIKE DOOM3

Virtual DOM 불리우는 추상화는 DOOM 3와 매우 유사하다.
우리는 '서버의 이벤트' 혹은 '브라우저의 이벤트' 에 의해 동작하는 '어플리케이션'을 가짐.

그리고 '비지니스 로직' 을 설명하기 위한 React Component 를 선언적으로 작성한다.

이러한 컴포넌트에 담긴 정보를 취합하여 논리적으로 표현가능한 가상의 DOM을 생성한다.

물리적인 렌더링 대신 논리적인 렌더링 계층을 추가 -> 개념적인 갭의 격차를 줄일 수 있는 아이디어
-> 실제로 그리지 않아도 Front End 에서는 마치 새로 그리듯이 Component를 만들면 된다.
-> 나머지는 Virtual DOM 과 Back End 에서 물리적인 변경을 시도할 것.

Virtual DOM 은 태생 자체가 퍼포먼스 개선이 목표가 아닌 개념적으로 멱등성을 가지는 View를 만들어나가는 과정에서 생긴 문제를 해결하기 위해 나온 아이디어.

멱등성

  • 첫 번째 수행을 한 뒤 여러 차례 적용해도 결과를 변경시키지 않는 작업 또는 기능의 속성.
  • 멱등한 작업의 결과는 한 번 수행하든 여러 번 수행하든 같다.

예측 가능한 데이터

"안읽은 메시지" 기능의 대한 처리에 대해서 작업을 해놔도 이후 이미 메시지를 다 읽었는데도 여전히 1 메시지가 사라지지 않은 문제가 발생함.

새로운 메시지 컨트롤러

  1. 메시지 수신
function newMessageHandler(message) {
	var chatTab = ChatTabs.getChatTab(message.threadID);
	chatTab.appendMessage(message);
}
  • 단순히 메시지에 해당하는 채팅방을 가져온 후에 채팅방에 메시지를 추가해주기만 하면 됬음.
  1. 읽지 않은 메시지 기능 추가!

업로드중..

  • 기존 기능에 읽지 않은 메시지 UI가 추가됨. -> 컨트롤러 수정 ( 읽지 않은 메시지의 대한 처리 추가 )
function newMessageHandler(message) {
	UnseenCount.incrementUnSeen(); // 읽지 않은 메시지를 가진 스레드를 증가 시킨다.
	
	var chatTab = ChatTabs.getChatTab(message.threadID);
	chatTab.appendMessage(message);

	// 만약 Tabs이 Focus 되어 있는 중이라면 읽지 않은 스레드를 감소 시킨다.
	if(chatTab.hasFocus()) {   
		UnseenCount.decrementUnseen();
	}
}
  1. 메인 채팅 화면 추가

  • 그러다 채팅 기능이 메인 기능으로 추가됨. -> 이에 대한 수정 역시 변경됨.
function newMessageHandler(message) {
	UnseenCount.incrementUnSeen(); // 읽지 않은 메시지를 가진 스레드를 증가 시킨다.
	
	var chatTab = ChatTabs.getChatTab(message.threadID);
	chatTab.appendMessage(message);

	// 만약 메인 채팅에 현재 메시지에 해당하는 스레드가 열려있다면 해당 스레드에 메시지를 추가한다.
	var messageView = Messages.getOpenView();
	var threadID = MessagesView.getThreadID();
	if ( threadID == message.threadID) {
		messagesView.appendMessage(message);
	}

	// 만약 Tabs이 Focus 되어 있는 중이라면 읽지 않은 스레드를 감소 시킨다.
	// 만약 새 메시지가 메인 채팅의 스레드라면 읽지 않은 스레드를 감소 시킨다.
	if(chatTab.hasFocus() || threadID == message.threadID) {   
		UnseenCount.decrementUnseen();
	}
}

이러한 과정을 추적하며 구조적인 결함을 발견할 수 있다.
위 어플리케이션은 깨지기 쉬운 구조처럼 보임.

  1. 새로운 기능이 생길 때마다 특정 개발자가 시스템이 기준이 아닌 주관적인 방식으로 개발 및 수정을 가한다.
    -> 이는 다른 개발자가 이해하고 수정하기 어려워 진다.
  2. Controller 에 역할이 비대해지고, Model과 View 간의 복잡한 의존성이 내부에 존재하게 된다.

2번 같은 경우에는 Flux 혹은 Redux 에서 항상 나오는 MVC 혹은 양방향 바인딩의 문제점을 설명하는 내용과 동일하다.

  • Chat
  • Thread
  • Message
  • UnseenMessage
    -> 를 Controller 에서 취합해서 관리하고 그에 해당하는 View 들의 의존성을 서로 참조하는 것은 그 끝을 예측하기 어렵다.
    -> 프로세스가 복잡해질수록 위 각 도메인들의 관계성이 깊어짐.

새로운 아키텍처

  • 첫 번째 수술 대상이었던 Controller
  • Controller 가 Model의 제어권을 가지고 개발자에게 직접 맡기는 방법을 고칠 필요가 있다.
    -> Controller 내에서 Model의 제어권을 보유하며 Model끼리의 관계성이나 Model과 View의 대한 관계성을 개발자가 덜 신경 쓰도록?

구체적으로
1. 외부에 제어권(External Control)이 있는 상황을 문제라고 여김.
2. 각각의 데이터들이 일관적으로 데이터를 유지해야 하는데 외부(Handler)에 제어권이 있다면
3. 데이터들은 스스로 일관성을 유지할 방법이 없다.
-> Model의 대한 제어권이 외부에 있어서 해당 Model 이 원치 않게 바뀔 수 있고 이는 다른 Model과도 연관될 수 있다?

  1. UnseenCounter 의 개수
  2. Chat Tab의 메시지 수
    = 모두 전적으로 Handler 가 통제하며,
    각각의 데이터들은 내부 일관성을 유지할 수 있는 기능이 없으므로 언제든지 데이터는 잘못될 수 있으며 스스로 고칠 방법도 없다는 것.

  • 외부 제어권을 없애고 내부에 제어권을 부여, 문제를 해결할 수 있으리라 생각.
  • Controller 가 데이터를 제어하는 것이 아닌, 각각의 데이터가 본인 스스로의 데이터를 관리, 내부에서 일관성을 유지할 수 있는 기능을 제공.
  1. 왜 기존에 이런 변화를 주지 않았나

  2. Controller는 왜 필요하지?

  3. 모든 도메인은 서로 간의 의존성

  4. 읽지 않은 메시지 -> Chat Tab, Main Message 의 읽음 여부에 의존적이다.

  • 서로가 서로를 모른다면 충분하지 않은 정보로는 표현할 수 없는 한계에 직면.
    -> 서로 간 의존성 통제 및 관리할 Controller 역할이 필수적인 것처럼 보인다.

위 구조에서 확실한 점
1. Unseen Cotuner 는 정보가 불충분하다
2. 의존성이 있는 도메인으로부터 Unseen Counter 만을 위한 더 명확한 데이터 (more explicit data) 가 필요하다.

이를 해결하기 위해서
1. Chat Tab 과 같은 도메인의 View 에서 이미 읽었음(Mark Seen)과 같은 추가적인 트리거를 실행
2. 새로운 사이클을 실행하여 문제를 해결.
-> 이미 읽었음 이라는 추가적인 트리거를 추가하고, 읽지 않은 메시지 와 채팅창, 메인 채팅창과, 이미 읽었음 의 대한 도메인 간 관계도를 더 명확하게 만듦?

하나의 Controller 에서 처리해야하는 비지니스를
1. 도메인 별 분리와
2. 더 구체적이고 많은 액션으로 대체하면서
3. 내부의 일관성과 도메인 간의 정보 부족 이라는 문제를 해결.

기존에 파생된 데이터를 사용했던 방식에서 -> 더 명시적인 데이터를 사용해야 했음.

파생된 데이터는 존재하는 또 다른 데이터과 합성되었거나 추정된 데이터
명시적인 데이터는 오로지 해당 도메인만을 위한 다른 데이터와 독립적인 성질을 지닌 데이터

해당 시스템에서 중요한 핵심 전략

  • Data Layer 의 프로세스가 완전히 처리되기 전에 추가적인 Action 은 일체 전달 되서는 안됨.
  1. Message, Thread 와 Unseen Threads 가 데이터 저장소에서 View 에게 알려줄 대이터를 모두 처리한다.
  2. 그러고 나면 View 는 Mark Seen, Action 을 호출할 수 있게 된다.
    = 이러한 순서를 잘 지키는 것이 위 시스템에 핵심이다.

기존의 MVC 와 달리 한 방향으로 순서를 지키며 진행한다.

  • 다이어그램을 더 일반화 할 수 있게 만든다.
  • 시스템에서 Model, View 가 증가 하더라도 그들을 하나의 Sotres 와 Views 로 압축하여 표기할 수 있다.
  • 하나의 방향과 순서만 신경.

이러한 단순한 시스템과 하나의 방향에만 집중

  • 하나의 기능 추가 -> 전체 시스템을 건들일 필요 사라짐.
  • 버그를 발견했더라도 어떤 액션이 시작된 위치만 알면 downstream 을 파악할 수 있기 때문.

하나의 방향에만 집중하기 때문에 다양한 모델과 뷰를 압축할 수 있고
-> "간단한 멘탈 모델" 을 형성할 수 있음.
그리고 이것은 중첩된 업데이트를 방지하는 것으로 계단식 or 전파 효과 방지 를 도움.

FLUX

  • FLUX의 가장 핵심적 아이디어는 '흐름'
  • Dispatcher 가 다이어그램에 추가, 흐름을 제어하는 역할을 담당
  • traffic controller 라 볼 수 있으며, 발생하는 모든 이벤트들(Action)을 제어함.
    -> 순서를 지키는 것이 매우 중요하기 때문에 Store 에서 데이터를 처리하기 전까지 -> 다른 Action이 추가적으로 요청되더라도 저장 후 순차적 처리가 될 수 있게 도움.

FLUX 라는 시스템은 다음과 같은 변화를 만들어 냄.
1. 향샹된 데이터 일관성
2. 버그의 근원을 발견하는 것이 쉬워짐
3. 더욱 의미 있는 유닛 테스트

데이터가 일관성을 유지 -> 결과를 예측하는 것이 수월해짐 -> 상태와 상태를 업데이트하는 로직이 한 곳에 생겼고 -> 그 뜻은 의존성이 줄어든다는 의미 -> 유닛 테스트 작성에 도움.

일관성을 유지하기 위해 내부로 제어권을 이동 -> 이를 통해 저장소 사이에 의존성이 사라짐.
= 이는 꽤 큰 변경점을 시사 -> 코드의 작성 방법.

의존성이 생긴다는 것은 기능이 변경 할 시에 여파 범위가 넓어진다는 것을 의미함.

MVC는 여파를 해결하기 위해서 OOP를 기본적인 전략으로 사용함.
-> 모든 값들을 상대적으로 만들어서 기능 간의 의존성을 약하게 만드는 전략

function printHelloMessage(name) {
	const defaultMessage = "Hello!"; // 데이터
	console.log(defaultMessage + name); // 뷰
}

기본 메시지가 변경된다면 또는 더 이상 서버에서 터미널에 출력하는 것이 아니라면

-> printHelloMessage 의 값들을 상대적인 참조 값으로

printHelloMessage(name) {
	const result = this.Message.buildMessage(name);
	this.View.print(result); 
}

printHelloMessage 는 메시지와 뷰에 변경 사항이 있더라도 buildMessage 만 실행하고 print 만 실행하면 된다. 혹은 메시지나 뷰가 아니여도 상관없다. 그저 필요한 메서드를 가진 무언가가 존재하기만 하면 됨.

FLUX 시스템 내에서는 이러한 의존성은 없으며, 위와 같이 작성하지 않아도 된다.
-> 각각의 데이터끼리의 의존성이 아닌. 데이터 자체가 자신을 관리하며, 전달만?
printHelloMessage 액션(name) -> dispatcher -> message -> view
printHelloMessage -> Message.buildMessage(name) -> printHelloMessage -> View.print(name)


출처

0개의 댓글