상태관리와 반응형 프로그래밍

상태관리와 반응형 프로그래밍

☞ 상태관리란?

☞ 반응형 프로그래밍이란?

상태(State) : 변화하는 데이터

→ 사용자의 action에 따라 변경될 수 있는 컴포넌트 부분을 나타내는 JS 객체

상태(State) 관리 : 변화하는 데이터 관리

→ 여러 컴포넌트 간 데이터 전달과 event 통신을 한 곳에서 관리하는 것

변화하는 데이터를 알맞게 관리하기 위해 나온 개념

ex. 채팅방 만들고 삭제 [상태변경 → UI 변경]

상태관리는 왜 필요할까?

UI는 사용자의 끊임없이 상호작용한다.

이전 사용자는 페이지 이동이 잦은 페이지를 사용했다. 페이지가 바뀔때마다 서버에서 데이터를 가져왔기 때문에 매번 페이지가 다시 그려졌다. 하지만 현재 사용자는 데이터가 바껴도 페이지가 리렌더링되지 않고, 변화된 부분만 바뀌길 원한다. 기술이 발전하면서 사용자의 입맛 또한 바뀐 것이다.

ex. 페이스북 좋아요, 인스타그램 팔로우 [새로고침X → 실시간 반영]

또한, 상태들이 매우 복잡하다. 한 웹페이지의 상태가 하나면, 굳이 관리할 필요가 없다. 하지만 상태가 많아지고 거미줄처럼 복잡하게 얽혀있다면, 상태가 상호 간에 어떻게 의존하고, 어떻게 흐르고, 그에 따라 UI가 어떻게 변하는지 알기 어려워진다. 만약 이런 상태값들이 비동기적이라면 상태관리가 더욱 힘들어질 것이다.

이렇게 한 웹페이지 내에 다양한 상태들이 존재하고, 어떻게 변화하는지 제대로 알고 있어야 하기 때문에 이를 효과적으로 "관리"할 필요 가 생겼다.

우리는 주로, 컴포넌트 기반의 프로젝트를 만드는데, 그 프로젝트는 작은 단위로 쪼개진 여러개의 컴포넌트 로 화면을 구성한다. Naver처럼.

그런데 이런 컴포넌트들은 독립적으로 존재하지 않고, 서로 데이터를 공유한다. 예를 들어 검색 컴포넌트에서 게임을 검색하면, 그에 맞는 게임이 화면에 나타나고, 게임에 들어가서 채팅에 참여하면 채팅인원이 늘어난다.

이런 식으로 컴포넌트는 다 다르지만, 사용하는 데이터는 서로 연결된 경우가 많았다. 따라서 이렇게 컴포넌트 간 통신을 하거나 상호 간에 데이터 전달을 보다 자연스럽게 관리할 필요성이 대두되면서, 상태관리의 중요성이 높아졌다 .

특히 상태가 많고, 복잡한 웹 애플리케이션에서 빛을 발했다. 웹 규모가 커지면, data의 이동이 빈번해져 중간에 거쳐야 할 컴포넌트들이 많아진다. 그러면 컴포넌트 간 데이터의 흐름을 파악하기 힘들어지는데, 그래서 이를 해결하기 위해 " 상태"가 필요 해졌다.

반응형 프로그래밍이란?

상태는 변화하는 데이터인데, 이게 자기들끼리만 변화해서 다른 곳에서 update되지 않고, 화면에 반영되지 않아 사용자가 개빡치게 된다. 내가 페이스북에 좋아요를 눌러도 반영이 안되니까.

데이터가 변화하면 데이터를 사용하는 측도 동시에 update가 되도록 개발자가 하나하나 만들어줘야 한다. JS는 반응형 프로그래밍 언어가 아니기 때문이다. 그래서 이런 귀찮은 일을 쉽게 할 수 있도록 해주는 게 "반응형 프로그래밍"이다. 값이 변하면, 값이 변한 걸 스스로 구독한 애들에게 전파시켜주는 것이다.

즉, 데이터가 변화하면, 그 데이터가 변화함에 따라 알아서 반응하는 프로그래밍을 말한다. 컴포넌트를 구독하고, 그 변화는 감지해 변화가 일어날 때마다 알림을 받고, 변화한 데이터에 따라 본인이 해야 되는 일을 하는 것이다.

let price = 1000;

let quantity = 2;

let total = price * quantity;

price = 2000;

console.log(`총 가격은 ${total}원`);

Not Reactvie (JS) Reactive 총 가격은 2000원 총 가격은 4000원

반응형 프로그래밍은 1985년에 On the development of reactive systems란 논문에 처음 등장했는데, 외부의 자극에 반응하는 것이라고 나와 있다. 위키피디아에는 데이터 스트림(Data Stream)과 변화의 전파에 관련된 선언형 프로그래밍 패러다임(Declarative Paradigm)이라고 되어 있는데, 여기서 스트림이란 시간 순으로 발생하는 이벤트의 나열로, 반응형 프로그래밍에서는 모든 데이터를 스트림으로 본다. 데이터 생산자가 하나의 타임라인에서 event를 통해 발생한 자신의 데이터를 알처럼 낳고 지나가는 것이다. 그리고 관찰자는 최종적으로 나오는 데이터를 가지고 자신의 작업을 수행한다. 이런 스트림에는 마우스 클릭, 키보드 입력, http 요청, 알림, 변수의 변화가 해당되고, 값, 에러, 완료 신호를 만들어 낼 수 있다. 선언형 프로그래밍은 어떻게 할까가 아닌, 뭘 할지를 말해준다.

ex. 집에서 홈플러스에 가는 길

명령형 과정 하나하나 설명 1. 집에서 나와.

2. 직진해서 500m 가.

3. 우회전해서 200m 가. 선언형 목표만 명시 집에서 홈플러스 가.

즉, 반응형 프로그래밍은 데이터가 변경될 때마다, event를 발생시켜서 바뀐 데이터를 지속적으로 전달하는 목표를 가진 프로그래밍이다. 데이터가 변경됐어? 그럼, 바꿔!!!

그리고 반응형 프로그래밍의 핵심은 "프로그램이 외부 환경과 어떻게 커뮤니케이션하는지"다. 프로그램이 외부 환경과 커뮤니케이션 하는 방법으로 PULL과 PUSH가 있다.

* 이때 PULL과 PUSH는 제어권을 가진자 입장에서의 PULL과 PUSH다.

데이터 생산자 = 외부 환경

데이터 소비자 = 내가 지금 실행하는 프로그램

1) PULL : 당기는 것

함수를 실행할 때, 데이터가 필요하면, 그 때마다 데이터를 만드는 애에게 달라고 요청해서 사용한다. 프로그램이 자체적으로 외부환경에 명령해서 원하는 데이터를 획득한다.

2) PUSH : PULL을 뒤집어서, 외부에서 프로그램으로 요청을 밀어넣는 것

프로그램 입장에서 PULL 방식은 데이터가 바꼈는지 안 바꼈는지 계속 물어봐야 하기 때문에 매우 귀찮다 . 그래서 내가 물어볼 때 알려주지 말고, 너가 바뀌면 나한테 알려주라고 선언하고 제어권을 데이터 소비자에서 생산자로 넘긴다. 그럼 나는 데이터가 변했는지 안 변했는지 신경쓸 필요가 없어지고, 데이터를 만드는 쪽에서도 데이터가 바뀌면 그냥 나한테 알려주면 되니까 편해진다.

외부에서 응답이 오면, 그 때마다 반응한다. 즉, 제어권을 넘겨서 통제 권한을 외부에 넘겨, 그 놈이 프로그램에 PUSH 하는 것이다. 여기서 비동기 처리에 유리하다는 걸 유추할 수 있다. 계속 물어보지 않아도 알아서 작업을 마치면 나한테 알려주니까.

* 비동기 처리 : 특정 로직의 실행이 끝날 때까지 기다리지 않고, 나머지 코드를 먼저 실행한다.

보통 프로그램은 1 처럼 PULL로 동작한다. 외부 환경에 데이터 내놔라!!

하지만 반응형 프로그램은 2 처럼 PUSH로 동작한다. 외부 환경에 제어권을 넘기고, 그 쪽에서 나한테 데이터를 PUSH 한다.

그런데, 이런 반응형 프로그램이 항상 좋을까?

그건 아니다. 개발자들이 반응형 프로그램을 짜려면 누가 누굴 구독하고, 구독 시점은 어떻게 되는지 계속 신경을 써야 해서 매우 귀찮다 . 또, 잘못 짜면 망하고, 개념이 많아서 공부하기도 까다롭다.

그래서 리액트는 이런 반응에 관련된 내용들을 본인이 다 만들고 숨겨버렸다. 이게 리액트로 프로그래밍을 하면서 데이터 구독하는 것에 신경쓰지 않는 이유이다. 이렇게 개발자들은 리액트를 사용하면, 주어진 데이터를 어떻게 렌더링할지만 신경쓰면 돼서 아주 개이득이다.

"Is React Reactive?"

개발자들이 데이터의 구독과 퍼블리싱에 신경쓰지 않아도 되기 때문에 반응형인 것처럼 보이지만, 리액트는 반응형이 아니다. 리액트 권위자 피셜.

WHY?

리액트는 내부적으로 PULL 방식을 사용 하기 때문이다. PULL 방식을 통해서 마치 PUSH를 사용하는 것처럼 내부를 구현해놨다. 그래서 리액트는 반응형이 아니다. 우리가 내부 동작을 살펴볼 일은 없지만, 리액트를 사용하면서 "이게 왜 돼지?" 하는 순간들이 올 때 이 개념이 유용하게 다가올 수 있다.

ex. setState를 하면 리렌더링이 되는데, 왜 setState를 연달아 사용할 때, 2번 렌더링이 안되지?

상태 관리와 관련된 다양한 패턴

: 상태 관리를 어떻게 할지에 대해 천재 개발자들이 미리 어떻게 할지 생각하고 고민해 놓은 것.

1) Observer Pattern : 관찰하는 패턴

Observer Pattern은 객체의 상태 변화를 관찰하는 관찰자들이 있고, 이 관찰자를 객체에 구독해서 그 객체에 상태 변화가 있을 때마다 메소드를 통해서 객체가 목록에 등록된 관찰자들에게 통지하는 디자인 패턴이다. 객체는 등록을 요청한 관찰자들을 배열로 갖고 있다.

예를 들어 우리가 관찰자고, 관찰하고 싶은 State가 우아한Tech라고 가정한다. 이 유튜브에서 새 영상이 올라올 때, 그 알림을 받고 싶으면 구독한다. 만약 그 객체 State가 변화했으면, 새 영상이 올라온 것이다. 이 개념을 State가 퍼블리싱한다고 한다. State가 퍼블리싱되면, 관찰자들에게 새 영상이 올라왔다고 알려줘야 한다. 관찰자들에게 그 객체가 자신이 변한 값인 State를 포함해서 통보해주면 이 퍼블리싱한 행동은 구독한 관찰자들에게 전달되는데, 이걸 "Fire"라고 한다. 그럼 각 관찰자들은 객체로부터 받은 데이터로 각자 일처리를 하는 것이다.

만약 관찰자가 구독을 취소하고 싶다면, 알아서 구독 해제를 하면 된다.

그럼, 이 패턴을 언제 사용할까?

브라우저의 이벤트 핸들러에 사용된다.

event를 등록만 해줄 뿐인데. 나중에 event가 발생하면, 우리에게 알려준다. 그건 구독과 알림이 내부에 구현 되있기 때문에 가능한 것이다. 따라서 Observer Pattern은 실시간으로 한 객체의 변경 사항을 다른 객체에게 전파해서 객체 간의 의존성을 제거할 수 있다는 장점이 있다.

2) Publish/Subscribe Pattern

Pub/Sub Pattern은 Observer Pattern의 변화된 형태로, 기반은 동일하다.

차이점은 중간에 이벤트 관리자 가 있는 것이다. Pub/Sub Pattern은 구독자를 바로 객체에 구독하지 않고, 중간에 이벤트 관리자를 구독한다. 마찬가지로 객체도 변화하면, 바로 구독자에게 퍼블리싱하는 것이 아닌, 중간에 이벤트 관리자를 통해 구독자에게 변화된 내용을 전달한다. 따라서 구독자와 객체가 직접적으로 연락하지 않아 서로 간의 결합도가 낮아지고, 메시지 브로커로 메시징 큐를 많이 사용하기 때문에 비동기 워크플로우가 된다.

* 메시지 브로커 : Publisher로부터 전달받은 메시지를 Subscriber로 전달해주는 중간역할이며, 응용 소프트웨어 간 메시지를 교환할 수 있게 한다.

* 메시지 큐 : 메시지가 적재되는 공간

* 토픽 : 메시지의 그룹

서로 응답이 오면 알려줘. 나는 내 할일 하고 있을게가 돼서 반응형 프로그래밍에 딱 알맞은 패턴이다.

3) Proxy Pattern

Proxy(대리)?

VPN과 더불어서 한국에서 접근할 수 없는 외국 사이트에 접근하게 해준다. 한국에서 외국 사이트에 접근이 불가능하니까. 요청하는 클라이언트인 한국에 있는 나 대신 서버와의 중간에 껴서 나는 아니지만 나처럼 데이터를 대신 요청하고 받아주는 역할을 하기 때문에 가능하다.

Proxy Pattern은 하나의 객체가 프록시 역할을 수행해서 상황에 따라 다른 객체에 접근하게 해주거나 다른 함수를 호출하게 해준다. Proxy Pattern에 대해 알려면, Proxy 객체에 대해 알아야 하는데. Proxy 객체는 Proxy Pattern을 사용할 때, 실제 객체를 대신하는 객체를 말한다.

function Users() {

this.users = ['그루밍', '엘라', '신세한탄', '티케', '주모', '브콜']

}

Users.prototype.create = function(name, callback) {

this.users.push(name);

}

Users.prototype.delete = function(name, callback) {

this.users = this.users.filter(user => user !== name)

}

Users 객체는 create로 name을 Users 객체에 추가할 수 있다. 그런데 만약 Users가 새로 추가되거나 삭제되었을 때, 추가되고 삭제된 그 카운트를 세서 보여주고 싶다면,

1. 객체를 뜯어 고친다. (X)

→ 이걸 사용하는 다른 곳에서 문제 발생

2. Proxy 개체 만든다. (O)

function UsersProxy() {

let users = new Users();

let count = 0;

return {

create: function (name, callback) {

users.create(name);

count += 1;

},

delete: function (name, callback) {

users.delete(name);

count += 1;

},

getUsers: function (name, callback) {

return users;

},

getCount: function (name, callback) {

return count;

}

}

}

진짜 Users를 감싼 객체를 만들어서 원본 객체를 수정하지 않고 진짜 객체를 활용해 새로운 일을 하는 객체를 하나 만든다.

Proxy가 State랑 무슨 상관?

Proxy는 기존의 State를 변경하지 않고, 그 State 객체를 감싸서 사용할 수 있게 해준다.

이걸 리액트 동작에 접목시켜 보면, 우리는 상태가 변경되면, 그 상태를 UI에 반영하기 위해 리렌더링 시켜주길 원한다. 그 리렌더링 하는 역할을 상태를 뜯어고쳐서 set 해주는 함수를 넣을 수는 있지만, 그에 동반하는 사이트 이펙트가 크기 때문에 넣지 않는 것이 좋다. 이때 Proxy 객체를 사용한다. State를 감싼 Proxy 객체를 만들어서 set 메서드에 Render 메서드를 같이 넣어준다. 그럼, State를 변경하지 않으면서도 그 State가 변경되면, 그 변경된 데이터를 렌더하게 만들 수 있다.

또, 우리가 주로 사용하는 리액트의 event에서 프록시 객체를 사용하고 있는데, 왼쪽은 바닐라 자바스크립트로 event를 콘솔에 출력했을 때 나타나는 event고, 오른쪽은 리액트에서 event를 콘솔에 출력했을 때 나타나는 event다.

서로 좀 다른데, 리액트에서 event를 프록시로 감싸 사용하기 때문이다.

from http://pluviayoungforever.tistory.com/67 by ccl(A) rewrite - 2021-11-26 04:02:29