개발자 윤찬

웹 개발자 윤찬의 프로필 사진개발자 윤찬
2025. 6. 15.

상태 일관성 및 실시간 구독

Vience최적화
"재밌는 프론트"

하나의 동작에, 관리할 여러 상태

남들이 프론트엔드가 뭐하는 직무에요? 라고 물어보면,

화면 꾸미는 직업입니다.

라고 말하고, 개발자가 프론트엔드는 뭐하냐? 물어보면

프론트엔드는 화면에 보이기 위해 관리되는 정보와, 브라우저 렌더링 간의 상태를 동기화 및 흐름을 관리하는 직무입니다.

라고 말할 것이다. 예전 상태에 대해서도 블로그를 적은적이 있었고 사실 이 공부가 결국 리액트에서 여러 상태관리 기법, 라이브러리 등이 나오고 그 기술 하나를 도입시키기 위해 많은 명분과 공부가 필요한 것 같다. 상태 라는것에 막 개발을 하다가 규모가 점점 커져 상태를 잃어버리고 다시 코드를 설계해본 경험도 많았고, 단순하게 데이터를 기록 이라고 접근만 했다가 주소값 변경여부 이슈로 원인 모를 디버깅까지 많이 해봤었다. 이때부터 프론트엔드 = 상태 라는 생각이 계속 잡혀있었다.

회사일을 하면서 제일 좋았던게 관리할 상태가 많았다는 것이다. 이 말이 결국 서비스가 점점 커지고 있다는 것이기도 하며, 그것에 필요한 구조적 고민, 기술 공부 및 도입 등이 필요한 상황이기 때문이다. 이번글에는 지금까지 해본 마이그레이션, 리팩토링 그리고 점점 커지는 서비스 규모에서 발생한 상태들의 동기화가 확실하게 필요했던 Vience Canvas의 Workspace 서비스에서의 이야기를 포스팅할 예정이다.


하나의 동작에 3가지 종류의 상태

workspace에서는 3가지 상태가 모두, 하나의 동작에 일관된 흐름을 가지고 있어야한다.

먼저 크게 관리되는 상태를 정리하면 다음과 같다.

리트(rete) 상태

  • rete엔진의 처리 단계(시작, 종료, 데이터 이동, 업데이트)
  • 백엔드에 저장해야하는 상태

전역 상태

  • 서로 다른 노드들 간의 정보, rete의 상태, UI 관리 상태

지역 상태

  • 각 노드별, react의 상태

그리고 위에서 언급한 하나의 동작이란 두가지 흐름을 생각해야한다.

rete 엔진의 가동

  • rete 엔진은 react가 마운트 될 때, 항상 시작을 한다.
  • rete 엔진이 가동되어야지 다음 노드로 데이터를 전송하는 소켓이 활성화 된다.
  • rete 엔진은 다른 상태가(전역, 지역)이 변할때 항상 시작을 한다.

브라우저 렌더링

  • 리액트 렌더링을 발생시키는 상황

백엔드 저장 시점

  • 사용자가 작업하다가, 중간에 나가거나, 백엔드에 최근에 저장했던 상황

도식화 해보면 다음과 같다.

이것이 일관된 흐름과 순서가 없는 경우에는 지역 상태 업데이트와 백엔드 저장이 Rete 엔진의 최신 상태 반영보다 먼저 일어나면, 화면과 실제 데이터, 백엔드 저장 내용 간의 불일치, 데이터 손실, 비정상적 동작이 발생할 수 있다. 이게 실제 잔 버그들이 많이 발생했었고, 그래서 캐싱문제까지 겹치게 되면, 복잡해 졌다. 예시 상황을 보면 다음과 같다.

현재 Custom Processing Node를 다루고 있는 상황에서, 코드의 진행 상태, 작성된 버전, 코드 작성 정도, import된 모듈 등 여러 동작 정보를 백엔드에 저장한다.

이때, 이전 노드인 Data Selector를 선택하면, 다음 노드(현재 노드인 Custom Processing)에 기본값 데이터를 전달하게 된다. 이 과정에서 Custom Processing Node는 이미 백엔드에서 받아온 기존 저장 데이터와, Rete 엔진 실행으로 인해 전달되는 초기화(init) 데이터가 동시에 들어오게 된다.

만약 이때 초기화 데이터가 먼저 컴포넌트에 도착하면, 백엔드는 이전까지 저장되어 있던 데이터를 해당 초기화 데이터로 덮어쓰게 된다.

즉, rete 상태와 패널의 상태가 동기화 되어있지 않아, 노드 저장시 손실/사이드 패널의 중복 데이터 발생등의 문제가 있었다.

rete 라이브러리 자체 특성으로 engine이 초기에 가동이 되어야 단방향으로 데이터 이동이 발생하고, 작업 도중 다른 소켓으로 연결을 하게 된다면, 다시 rete engine이 가동되어, 노드들이 기존의 상태를 잃어버려 정상적인 파이프라인이 구축이 되지 않는 것이다.

최종적으로 결국 rete의 초기 데이터인 경우, 먼저 수신 노드에서, 기존의 값이 있는지 체크를 한 이후에, 초기 데이터를 먼저 쓸지, 기존의 데이터를 넣을지 결정하는 것이다. 지금까지는 이렇게 처리를 한 상태로 진행이 되었지만, 하나의 문제가 있었다.


이렇게 되면, 실시간성이 없어지게 된다.

치명적인 문제가 하나 있는데, 바로 실시간성이 사라진다는 점이다. 여기서 말하는 실시간성이란, 내가 A 노드를 보고 있는 순간에 다른 노드의 output이 A 노드의 input으로 흘러와도 렌더링이 되지 않는다.

왜 이런 일이 생길까? 이 문제는, rete는 노드를 처음 생성할 때, input이 들어왔는지 여부를 entities(사전에 정의된 도메인 클래스) 레벨에서만 처리한다. 따라서, 컴포넌트의 관찰 시점에서는 즉, 리액트에서는 마운트 시점(초기 1회)에만 이전/초기 데이터를 읽어온다. 이후에는 entities 내부 상태 변화에 구독하지 않는다.

즉, 리액트 라이프사이클과의 분리되어 있었기 때문에 rete엔진이 내부 모델(entities)만 갱신하고, 그 변화가 구독되어져 있지 않으니, 항상 이전노드를 다녀오고, 다시 rete engine을 가동시켜야 컴포넌트를 마운트해, 실시간성이 떨어질 수 밖에 없다.

rete에 의해 Class가 데이터는 바꾸지만 React에게 바꼈다는 신호를 보내지 않아서 이런 현상이 발생하는 것이다.

그렇기 때문에 Class 내부 데이터 변화를 감지하고, 이를 React에 알려 렌더링을 트리거할 필요가 있었다. 이를 위해 전역 상태를 도입하여, 값이 변경될 때 선택적 렌더링을 진행해 해당 컴포넌트만 갱신되는 방식을 적용했다.

다행히 예전에 FSD 구조로 리팩터링하고, 노드의 지역 상태를 Reducer 기반 단방향 데이터 흐름으로 변경해둔 덕분에 상태 추적이 용이했고, 전역 상태 도입도 빠르게 진행할 수 있었다.

결국 Zustand를 활용하여, Rete Node Class의 data() 부분에서 노드별 이전 값과의 변경 여부를 감지하고, 이를 해당 컴포넌트가 구독하도록 하여 필요한 시점에만 렌더링이 발생하도록 구현했다. 그 결과는 workspace에서 실시간 감지가 가능하게 되었으며, 노드별 연결상태 및 다른 input에 따라서도 타노드를 다녀오고 다시 mount 시키는 동작을 없앨 수 있었다.


이 결과로 정말 관리해야할 상태가 3가지가 되었다.

  1. rete의 엔진 가동에 맞춰, 백엔드에서 받아오는 데이터가 겹쳐지지 않게 기존의 데이터를 확인하는 작업
  2. 변경된 데이터를 사전 노드 정의 class가 react에게 전달, 즉 소켓의 통신을 react에 변경을 주는 전역상태
  3. 각 노드별 지역상태 (백엔드에 저장할 상태)

이렇게 3가지 상태가 일관성 있도록 적용이 되어야했고

위와 같이, 변경된 데이터의 결과를 백엔드에 전송하면서, 전역상태로 해당 노드 key별로 값들을 저장하게 하여, 전역상태가 초기 데이터의 유무를 확인한 이후, 리액트에게 전송하는 중간 역할을 하도록 처리했다.

이로 인해서, 만약 저장을 하더라도, react 컴포넌트에서, 바로 zustand에 값을 넣어, 항상 전역상태가 최신의 상태를 유지하도록 하여 일관된 흐름을 가져가도록 처리할 수 있었다.

이후의 작업은, 너무 많은 렌더링을 발생시킬 수 있기 때문에, memo나 callback등의 기법도입과, 이 과정에서도, 주소값 변경과 같은 문제를 풀어 나가게 되면서 점점 실시간성을 갖춘 서비스로 만들어 가게 되었다.

Profile Picture

CHAN

과정은 복잡하되, 결과는 단순하게

Thank You for Visiting My Blog, Have a Good Day 😆
ⓒYoonchan Cho