바닐라 JS 상태 관리 시스템 만들기: Proxy 객체로 구현하는 리액티브 프로그래밍

안녕하세요!

최근 웹 프론트엔드 개발의 중심에는 React, Vue와 같은 강력한 프레임워크들이 자리 잡고 있습니다. 하지만 이들의 핵심 원리인 ‘데이터가 변하면 화면이 자동으로 업데이트되는 마법’을 라이브러리 없이 직접 구현해 본다면 어떨까요? 오늘은 외부 도구의 도움 없이 순수 자바스크립트의 최신 문법을 활용하여 바닐라 JS 상태 관리 시스템을 구축해 보려고 합니다. 특히 단순히 상태를 바꾸는 수준을 넘어, 데이터의 변화를 실시간으로 감지하고 UI를 즉각적으로 갱신하는 ‘리액티브(Reactive)’한 구조를 설계하며 우리가 프레임워크를 쓰면서 놓쳤던 핵심 원리들을 깊이 있게 파헤쳐 보겠습니다.

바닐라 JS 상태 관리 시스템 메인 화면

왜 지금 ‘바닐라 JS 상태 관리’를 공부해야 할까?

프레임워크가 모든 것을 해결해 주는 시대에 왜 굳이 바닐라 JS 상태 관리 시스템을 직접 만들어야 하는지 의문이 드실 수 있습니다. 하지만 실제 복잡한 프로젝트를 진행하다 보면 라이브러리의 무거운 용량이 부담되거나, 프레임워크의 생명 주기(Lifecycle)에 갇혀 성능 최적화에 한계를 느끼는 순간이 찾아옵니다.

저는 이전에 [2026 연봉 실수령액 계산기][인피니트 스크롤 구현하기] 같은 프로젝트를 진행하면서, 데이터 하나가 바뀔 때마다 수많은 DOM 요소를 일일이 수정해야 하는 번거로움을 느꼈습니다. 만약 데이터(state)를 수정하기만 해도 화면이 알아서 바뀐다면 코드가 얼마나 깔끔해질까요? 이 질문에 대한 해답이 바로 오늘 다룰 상태 관리 시스템에 있습니다. 원리를 이해하고 나면 프레임워크의 내부 동작을 꿰뚫어 볼 수 있는 통찰력이 생깁니다.


기존 방식의 한계와 Proxy 객체의 등장

기존에 상태 변화를 감지하기 위해 흔히 사용하던 방식은 Object.defineProperty였습니다. 하지만 이 방식은 객체에 새로운 속성을 추가하거나 배열의 변화를 감지하는 데 치명적인 한계가 있었죠.

이러한 갈증을 해결해 준 것이 바로 ES6에서 등장한 Proxy 객체입니다. Proxy는 말 그대로 대리인 역할을 하여, 객체에 접근하거나 값을 수정하는 모든 동작을 중간에서 가로채(Trap) 우리가 원하는 로직을 끼워 넣을 수 있게 해줍니다. 저는 이번 바닐라 JS 상태 관리 시스템의 심장부로 이 Proxy를 선택했습니다. 데이터가 수정되는 찰나를 감지해 “자, 데이터가 바뀌었으니 화면을 다시 그려!”라고 명령을 내리는 구조를 만든 것이죠.

Proxy 객체의 가장 큰 매력은 객체의 속성에 접근하거나 값을 수정할 때 우리가 원하는 로직을 ‘가로챌’ 수 있다는 점입니다. 제가 이번 시스템에서 가장 중요하게 생각한 set 트랩의 기본 구조는 다음과 같습니다.

이 짧은 코드가 바로 바닐라 JS 상태 관리 시스템의 심장부입니다. 이전에 다뤘던 [자바스크립트 스톱워치 만들기]에서는 매번 수동으로 화면을 갱신해야 했지만, 이제는 데이터를 바꾸기만 하면 이 트랩이 알아서 감지하고 행동하게 됩니다.


리액티브 시스템의 핵심 로직: Observer 패턴의 결합

진정한 의미의 바닐라 JS 상태 관리 시스템이 되려면, 데이터의 변화를 감시하는 ‘관찰자(Observer)’가 필요합니다. 상태가 변경되었을 때 어떤 컴포넌트나 UI가 업데이트되어야 하는지 명확히 알고 있어야 하니까요.

저는 이 과정을 위해 Subscriber 목록을 관리하는 클래스를 설계했습니다. 상태가 변경되는 순간, Proxy의 set 트랩이 발동하고, 등록된 모든 구독자에게 알림을 보내 화면을 갱신하는 흐름입니다. 이 구조를 적용하면 이전에 다뤘던 [객체 매핑 설계로 코드 줄이기: 자바스크립트 단위 변환기]의 로직을 훨씬 더 선언적이고 세련되게 바꿀 수 있습니다. 이제는 “이 값을 바꿔라”라고 명령하는 게 아니라, “값은 이렇게 바뀌었으니 화면은 알아서 해라”라는 수준으로 코드가 발전하는 것이죠.

데이터가 바뀌었을 때 “어떤 함수를 실행할지” 관리하는 로직도 필수적입니다. 저는 여러 개의 UI 요소가 동시에 하나의 데이터를 감시할 수 있도록 구독자 명단을 관리하는 클래스를 설계했습니다.

이 구조를 활용하면, 상태가 바뀔 때마다 굳이 모든 DOM을 뒤질 필요가 없습니다. 각 UI 컴포넌트가 subscribe를 통해 자기 자신을 등록해두면, 상태 관리자가 notify를 호출하는 순간 등록된 모든 UI가 일제히 최신 데이터를 반영하게 되죠. 이는 마치 [이벤트 위임 패턴으로 최적화한 자바스크립트 OX 퀴즈 게임]에서 하나의 리스너로 여러 요소를 관리했던 효율성과 닮아 있습니다.


[실전] 한 걸음 더 나아간 코드 설계

제가 이번 시스템을 구현하며 가장 신경 쓴 부분은 ‘DOM 접근의 최소화’입니다. 매번 상태가 바뀔 때마다 전체 페이지를 다시 그리는 것은 [인피니트 스크롤] 포스팅에서 경계했던 성능 저하를 초래합니다.

따라서 각 컴포넌트가 자신이 감시해야 할 상태 조각(Slice)에만 반응하도록 로직을 쪼개는 작업이 중요했습니다. 마치 스톱워치 만들기에서 0.01초의 정밀도를 유지하기 위해 필요한 연산만 수행했던 것처럼, 상태 관리에서도 필요한 UI만 골라 업데이트하는 ‘타게팅 렌더링’이 핵심입니다. 이 과정에서 브라우저의 렌더링 파이프라인을 이해하게 되었고, 라이브러리가 뒤에서 얼마나 많은 일을 해주고 있었는지 다시금 깨닫게 되었습니다.


상태 관리 시스템 사용해보기

마크다운이나 계산기 예제보다 더 근본적인 ‘상태 변화’의 흐름을 보실 수 있도록 준비했습니다. 아래는 제가 직접 고민하며 짠 바닐라 JS 상태 관리 시스템의 최소 단위 예제입니다. ‘이름 변경’이나 ‘숫자 증가’ 버튼을 누르면, 어떤 DOM 조작 코드 없이도 화면이 자동으로 바뀌는 것을 확인해 보세요.

리액티브 상태 관리 실습

현재 상태 데이터

방문자

0


[심화] 완성도를 높이는 디테일: 비동기 처리와 불변성

실무 레벨의 바닐라 JS 상태 관리 시스템이라면 비동기 통신 이후의 상태 업데이트도 매끄러워야 합니다. Fetch API로 가져온 데이터가 상태에 반영될 때 UI가 자연스럽게 반응해야 하죠.

또한, 상태를 직접 수정하기보다는 새로운 객체를 생성해 교체하는 ‘불변성’의 개념도 도입해 보았습니다. 이는 나중에 ‘뒤로 가기’나 ‘상태 추적(Time Travel Debugging)’ 기능을 넣을 때 필수적인 토대가 됩니다. 제가 [배열 고차 함수로 만드는 가계부]를 만들 때 원본 데이터를 보존하며 새로운 배열을 반환했던 논리를 떠올리니 한결 이해가 쉬웠습니다. 결국 좋은 코드는 데이터의 흐름을 얼마나 예측 가능하게 만드느냐에 달려 있다는 것을 다시금 느꼈습니다.


오늘 구현해 본 바닐라 JS 상태 관리 시스템은 단순히 기능을 하나 만드는 것 이상의 의미가 있었습니다. 우리가 편하게 사용하던 도구들의 본질을 만져보고, 자바스크립트라는 언어가 가진 강력한 확장성을 직접 확인하는 시간이었으니까요.

프레임워크는 유행에 따라 변하지만, 그 뿌리에 있는 바닐라 자바스크립트의 원리는 변하지 않습니다. 이번 포스팅을 통해 여러분도 라이브러리의 마법 뒤에 숨겨진 논리적인 아름다움을 발견하셨기를 바랍니다. 여러분의 프로젝트에도 오늘 배운 Proxy 기반의 상태 관리 로직을 가볍게 이식해 보세요. 코드가 한층 더 단단하고 우아해지는 것을 경험하실 수 있을 겁니다.

구현 과정에서 Proxy 트랩이 꼬이거나 상태 동기화가 잘 안 되는 지점이 있다면 언제든 말씀해 주세요!

댓글 남기기