리액트 이전의 프론트엔드 상태관리 역사 정리
전문가를 위한 리액트 스터디를 진행하면서 "리액트가 왜 등장했나"를 이해하려면 그 이전에 어떤 시도들이 있었는지부터 짚어야 한다는 걸 알게 됐다. 라이브러리 하나하나가 그 시점의 한계를 풀기 위해 나왔고, 리액트도 그 연장선에 있었다. 공부하면서 정리한 내용을 남긴다.
리액트가 풀려던 문제, 즉각적인 업데이트
리액트의 핵심 목적 중 하나는 즉각적인 업데이트였다. 새 페이지가 렌더링되고 로딩되기까지 기다릴 필요 없이 즉시 화면이 갱신되어 내용을 바로 확인할 수 있는 것.
그 이전 정적 페이지 환경에서 즉각적인 업데이트가 어려웠던 이유는 세 가지로 정리할 수 있었다.
- 성능: 리렌더링 시마다 페이지 레이아웃을 다시 계산(리플로우, reflow — 요소의 위치·크기를 다시 계산하는 단계)하고 그리기 때문에 성능 병목이 자주 발생했다.
- 신뢰성: 주로 전역 상태로 관리하다 보니 상태를 추적하기가 어려웠다.
- 보안: 크로스 사이트 스크립팅(XSS), 크로스 사이트 요청 위조(CSRF) 같은 악용을 막기 위해 페이지에 삽입하는 HTML과 자바스크립트를 모두 sanitize(사용자 입력 등에서 문제가 될 만한 부분을 제거하는 작업) 해야 했다. 이건 정적·동적을 가리지 않는 모든 웹 페이지의 보편적인 요구사항이지만, 직접 DOM을 만지는 환경에서는 sanitize를 빼먹기 쉬워 위험이 더 컸다.
순수 JavaScript로 상태를 다루던 시절
가장 원시적인 형태. DOM 객체를 찾고, 이벤트 리스너를 붙이고, data-* 속성으로 데이터를 추적하면서 상태를 관리했다. 서버 통신은 XMLHttpRequest로 직접 호출했다.
const button = document.querySelector('#submit');
button.dataset.count = '0';
button.addEventListener('click', () => {
button.dataset.count = String(Number(button.dataset.count) + 1);
});문제는 두 가지였다. 첫째, 작업량이 많고 확장성이 떨어졌다. 버튼이 많아질수록 그만큼 핸들러를 일일이 다 붙여야 했다. 둘째, DOM 요소가 HTML과 자바스크립트 양쪽에서 동시에 관리되다 보니 DOM에 대한 CRUD에서 발생하는 에러 케이스가 많았다.
jQuery, DOM 자체가 상태였던 시기
jQuery는 코드의 어느 곳에서든 페이지 구조를 직접적·전역적으로 수정할 수 있게 해줬다. 편리한 만큼 사이드 이펙트 가능성도 높았다. DOM 자체가 곧 상태였기 때문에, 상태의 단일 출처(single source of truth)나 단방향 데이터 흐름 같은 패러다임이 자연스럽게 강제되지 않았다.
문제점은 다음과 같았다.
- jQuery가 추가하는 동작은 격리하기 어렵기 때문에 테스트가 어렵다. 현재 상태가 어떤 값인지 추적하기 힘들었다 — 단방향 데이터 흐름이 없는 패러다임의 한계였다.
- 전체 jQuery 라이브러리를 웹 프로젝트에 통합해야 해서 파싱할 파일 크기가 늘어났다. 로딩 시간 단축이 중요한 사이트에는 부담이었다.
Backbone, 구조를 도입한 초기 프레임워크
Backbone은 상태 불일치, 코드 재사용, 테스트 가능성 같은 문제를 모델·뷰·컬렉션·라우터라는 구조로 분리해 다룬 초기 프레임워크 중 하나다. Model, View, Collection, Router 네 구성 요소를 제공한다. MVC를 자체적으로 해석해 컨트롤러를 명시적으로 두지 않고, View가 컨트롤러 역할 일부를 겸하며 Router가 URL을 함수에 매핑하는 방식이었다.
각 뷰 객체마다 모델 프로퍼티에 상태값·API를 바인딩해 구조화된 방식으로 다룰 수 있게 됐다.
MVC 패턴 자체를 짚고 가기
Backbone을 이해하려면 MVC(모델-뷰-컨트롤러) 패턴을 먼저 알아야 했다. 상태 관리와 UI를 요소별로 분리하는 디자인 패턴이다.
| 구성 요소 | 역할 |
|---|---|
| 모델 | 애플리케이션의 데이터와 비즈니스 규칙 담당. UI와 분리되어 있다. |
| 뷰 | UI 담당. 모델이 제공한 데이터를 사용자에게 표시하고, 사용자 명령을 컨트롤러로 전달한다. 자체적으로 상호작용을 처리하지는 않는다. |
| 컨트롤러 | 모델과 뷰 사이의 인터페이스. 뷰에서 사용자 입력을 받아 처리한 뒤 표시할 결과를 뷰에 돌려준다. |
인터랙티브한 동적 인터페이스에 대한 수요가 늘어나면서 MVC 패턴은 한계에 부딪혔다.
- 사용자 입력이 많아질수록 컨트롤러 수가 증가했다. 화면에 표시되지 않는 뷰를 제어하는 컨트롤러가 생기거나 다른 컨트롤러와 충돌하는 경우도 발생했다.
- 양방향 데이터 흐름 때문에 뷰가 모델과 동기화되지 않거나 모델이 뷰와 동기화되지 않는 경우가 생겼다.
- 양방향 흐름에서는 모델·뷰 중 어느 쪽이 진실의 출처인지 흐려져 동기화 문제가 생기기 쉬웠다.
이 한계에 리액트는 어떻게 답했나
여기서부터가 리액트의 차별점이었다. 리액트는 컴포넌트 기반 아키텍처로, UI 컴포넌트가 입력(prop)을 받고 그 입력에 따라 출력(엘리먼트)을 돌려주는 함수처럼 동작한다. 그래서 상태 변화와 그 영향을 쉽게 파악할 수 있었다. MVC의 양방향 데이터 흐름과 반대되는 단방향 데이터 흐름을 채택한 것도 같은 맥락이었다.
Knockout, MVVM의 데이터 바인딩
Knockout은 MVVM 패턴을 채택한 라이브러리였다. 뷰 모델은 객체로, data-bind 속성을 사용해 페이지의 다양한 요소에 바인딩한다. Knockout의 핵심은 뷰 모델을 HTML 요소에 선언적으로 바인딩하는 방식이다. 이후 버전에서 컴포넌트와 템플릿 기능도 추가됐다.
function ViewModel() {
this.name = ko.observable('Bob');
}
ko.applyBindings(new ViewModel());ko.observable로 관찰 가능한 상태를 만들고, ko.applyBindings(viewModel)로 뷰 모델을 활성화해 data-bind 속성이 붙은 DOM 요소와 연결한다. 뷰 모델이 HTML 요소에 바인딩되면 사이트가 사용자와 상호작용할 수 있게 된다.
MVVM 패턴이 뭐였나
Microsoft의 WPF(2006년 출시)를 위해 고안된 아키텍처 디자인 패턴이다. 이후 Silverlight, Xamarin 등에서 채택되면서 자리잡았다. 데이터 바인딩이 중요해진 최신 UI 개발 플랫폼에 맞춰 기존 MVC 패턴을 발전시킨 형태였다.
| 구성 요소 | 역할 |
|---|---|
| 모델 | 데이터 및 비즈니스 로직, 데이터 검색·저장·처리 담당. 일반적으로 데이터베이스, 서비스, 다른 데이터 출처와 통신한다. 뷰 및 뷰 모델을 인식하지 못한다. |
| 뷰 | UI 담당. 사용자에게 정보를 표시하고 사용자 입력을 수신한다. |
| 뷰 모델 | 모델과 뷰 사이의 브릿지. 뷰에 바인딩할 데이터와 명령을 제공한다. 명령 패턴을 통해 사용자 입력을 처리하고, 모델의 데이터를 뷰에서 쉽게 표시할 수 있는 형식으로 변환한다. |
AngularJS, 양방향 바인딩과 모듈식 아키텍처
AngularJS도 양방향 데이터 바인딩을 핵심으로 삼았다. 모듈식 아키텍처를 도입해 사용자가 컨트롤러를 직접 제작하지 않아도 모듈을 가져다 쓰는 것만으로 서비스를 사용할 수 있게 했다. 서비스 생성 및 주입은 AngularJS가 알아서 처리했다.
양방향 데이터 바인딩과 모듈화된 구조는 작업 능률을 크게 끌어올렸지만, 그만큼 한계도 있었다.
- 데이터 바인딩이 복잡한 대규모 애플리케이션에서 양방향 데이터 바인딩 패턴은 성능에 취약했다.
- 모듈을 학습해야 해서 러닝 커브가 높아졌다.
- 템플릿 속성 안에서 복잡한 자바스크립트 표현식을 허용한 탓에 HTML 안에 프레젠테이션과 비즈니스 로직이 뒤섞였다. 인라인 표현식에서 발생하는 오류는 디버깅이 어렵고 유지보수가 힘들었다.
ReactJS, 단방향 흐름과 가상 DOM
리액트는 JSX와 훨씬 더 간단한 컴포넌트 모델을 도입했다. 그리고 결정적으로 단방향 데이터 흐름을 채택했다. 덕분에 개발자가 애플리케이션을 더 잘 제어하고 시간에 따라 데이터가 어떻게 변하는지 더 쉽게 이해할 수 있게 됐다. 웹 페이지 전체가 아니라 일부만 교체하는 혁신적인 구상이 이 흐름 위에서 가능해졌다.
가상 DOM(Virtual DOM, UI의 이상적인 표현을 메모리에 두고 실제 DOM과 동기화하는 방식)도 같이 들어왔다. 개발자는 상태만 선언하면 React가 DOM을 맞춰준다. 직접적인 DOM 조작이 줄어드는 것은 그 부수 효과다. 한편 jQuery·AngularJS에서 보이던 일관성 없는 상태 문제는 가상 DOM이 아니라 단방향 데이터 흐름이 다뤘다. 이 둘은 자주 같이 묶여 설명되지만 각각이 푸는 문제가 다르다는 걸 정리하면서 새로 알게 됐다.
데이터 흐름만 떼어놓고 봐도 차이가 명확했다.
리액트가 갑자기 튀어나온 게 아니라 이전 라이브러리들이 부딪힌 문제 — 양방향 바인딩의 동기화 문제, DOM 직접 조작의 사이드 이펙트, 진실의 출처가 흐려지는 문제 — 를 단방향 흐름과 컴포넌트로 정리한 결과물이라는 게 이번 정리의 핵심이었다.