실습: BehaviorSubject로 간단한 상태 저장소(state store) 만들기: 현재값 보관과 구독(subscribe) 시점 무관한 최신 상태 전파
BehaviorSubject는 현재 값을 항상 내부에 보관하고 있다가 새로운 구독자(subscriber)가 나타나면 즉시 그 값을 전달하는 Subject다. 이 실습에서는 BehaviorSubject를 활용해 초기값, 상태 변경(setState), 상태 조회(getState), 구독(subscribe)을 갖춘 최소 단위의 상태 저장소를 직접 만들어본다. R
By 송민성
1. 개념
BehaviorSubject는 Subject의 한 종류로, 일반 Subject와 달리 반드시 초기값(initial value)을 가지고 시작하며 항상 "현재 값(current value)"을 내부에 저장하고 있다.
일반 Subject는 구독(subscribe) 시점 이전에 발행(emit)된 값을 놓치지만, BehaviorSubject는 구독하는 순간 저장된 최신 값을 즉시 받아볼 수 있다. 이 특성 때문에 BehaviorSubject는 "값을 가진 스트림", 즉 상태(state)를 표현하기에 적합한 도구로 취급된다.
상태 저장소(state store)란 애플리케이션의 특정 시점 데이터를 한 곳에 모아두고, 그 데이터가 바뀔 때마다 관심 있는 쪽에 알려주는 구조를 말한다. BehaviorSubject는 이 구조를 만드는 데 필요한 최소한의 기능, 즉 "현재 값 보관"과 "변경 알림"을 기본으로 제공한다.
2. 왜 사용하는가
일반적인 Observable은 구독 이전에 어떤 일이 일어났는지 알 방법이 없다. 예를 들어 사용자 로그인 상태를 Observable로 표현했는데, 컴포넌트가 나중에 구독하면 "이미 로그인되어 있다"는 사실을 전달받지 못하는 문제가 생긴다.
BehaviorSubject를 쓰면 이 문제가 해결된다. 언제 구독하든 상관없이 항상 최신 상태를 즉시 받을 수 있기 때문에, 애플리케이션 여러 곳에서 공유해야 하는 상태(로그인 여부, 장바구니 목록, 다크모드 설정 등)를 표현하는 데 자연스럽게 들어맞는다.
또한 getValue() 메서드를 통해 구독하지 않고도 현재 값을 동기적으로 즉시 꺼내볼 수 있다는 점도 실무에서 유용하다. 이 덕분에 이벤트 핸들러 안에서 "지금 상태가 뭐지?"를 확인하는 일반적인 패턴을 그대로 구현할 수 있다.
3. 동작 원리
BehaviorSubject는 내부적으로 다음 세 가지를 관리한다. 첫째는 현재 값을 저장하는 변수, 둘째는 구독자 목록, 셋째는 값이 바뀔 때 구독자에게 전파하는 로직이다.
새로운 구독자가 subscribe를 호출하면, BehaviorSubject는 먼저 저장된 현재 값을 그 구독자에게 즉시 발행(emit)한다. 그 이후부터는 next가 호출될 때마다 새 값을 현재 값으로 갱신하고, 모든 구독자에게 동일한 값을 동시에 전파한다.
이 원리를 이용해서 "상태 저장소"라는 객체를 만들 때는 BehaviorSubject 인스턴스를 내부에 숨기고, 외부에는 상태를 읽는 함수(getState)와 상태를 변경하는 함수(setState), 그리고 변경을 구독하는 함수(subscribe)만 노출하는 방식을 쓴다. 이렇게 하면 BehaviorSubject의 next 메서드를 외부에서 직접 호출하지 못하게 막아, 상태 변경 경로를 하나로 강제할 수 있다.
4. 예제
먼저 rxjs 패키지에서 BehaviorSubject를 가져온다. const { BehaviorSubject } = require('rxjs'); 형태로 불러온다.
그다음 상태 저장소를 만드는 함수 createStore를 정의한다. 이 함수는 초기 상태값(initialState)을 인자로 받아서, 내부에 const state$ = new BehaviorSubject(initialState); 형태로 BehaviorSubject 인스턴스를 하나 생성한다.
그다음 상태를 읽는 함수를 만든다. getState는 return state$.getValue();처럼 현재 값을 동기적으로 반환한다.
그다음 상태를 변경하는 함수를 만든다. setState는 새로운 부분 상태(partial)를 인자로 받아서, 먼저 const current = state$.getValue();로 현재 상태를 꺼내고, 그다음 const next = { ...current, ...partial };처럼 기존 상태와 병합한 새 상태를 만든 뒤, state$.next(next);를 호출해서 상태를 갱신하고 구독자들에게 전파한다.
그다음 상태 변경을 구독하는 함수를 만든다. subscribe는 콜백 함수 listener를 인자로 받아서, return state$.subscribe(listener);처럼 내부 BehaviorSubject에 구독을 위임하고, 반환되는 구독(subscription) 객체를 그대로 돌려준다.
마지막으로 이 세 함수를 객체로 묶어서 반환한다. return { getState, setState, subscribe }; 형태로 정리한다.
이제 이 저장소를 사용하는 코드를 작성한다. 먼저 const store = createStore({ count: 0, user: null });처럼 초기 상태를 지정해 저장소를 만든다.
그다음 상태 변경을 구독한다. store.subscribe((state) => console.log('상태 변경:', state));를 호출하면, 구독 즉시 콘솔에 { count: 0, user: null }이 출력된다. 이는 BehaviorSubject가 구독 시점에 현재 값을 바로 전달하기 때문이다.
그다음 상태를 변경해본다. store.setState({ count: 1 });을 호출하면 구독자에게 { count: 1, user: null }이 전달되어 콘솔에 다시 출력된다.
그다음 store.setState({ user: { name: '지훈' } });을 호출하면 { count: 1, user: { name: '지훈' } }이 출력된다. 기존 count 값이 유지되는 이유는 setState 내부에서 스프레드 연산자로 기존 상태와 병합했기 때문이다.
마지막으로 구독 없이 현재 상태를 바로 확인하고 싶다면 console.log(store.getState());를 호출해서 동기적으로 현재 값을 꺼내 쓸 수 있다.
5. 실무 사용 사례
프런트엔드 프레임워크와 무관한 순수 상태 관리 계층을 만들 때 BehaviorSubject가 자주 쓰인다. React나 Vue 같은 프레임워크에 종속되지 않는 도메인 상태(예: 인증 토큰, 사용자 프로필, 테마 설정)를 별도 모듈로 분리해서 여러 화면에서 공유할 때 이런 패턴이 적합하다.
Angular 진영에서는 서비스(service) 안에 BehaviorSubject를 두고 컴포넌트가 이를 구독해서 화면을 갱신하는 방식이 널리 쓰인다. NgRx 같은 상태 관리 라이브러리도 내부적으로 Subject 계열을 활용한 스트림 기반 아키텍처를 따른다.
Node.js 백엔드에서는 서버 내부의 설정 값이나 캐시 상태처럼 여러 모듈이 동시에 참조해야 하는 값을 BehaviorSubject로 감싸두고, 값이 바뀔 때마다 관련 로직을 재실행하는 용도로도 활용한다.
6. 주의할 점
BehaviorSubject는 반드시 초기값을 넣어야 생성할 수 있다. 초기값 없이 new BehaviorSubject()를 호출하면 오류가 발생하므로, 상태의 기본값을 설계 단계에서 명확히 정해야 한다.
setState를 구현할 때 얕은 병합(shallow merge)만 이루어진다는 점을 인지해야 한다. 위 예제처럼 스프레드 연산자로 병합하면 최상위 속성만 덮어써지고, 중첩된 객체 내부 속성은 통째로 교체된다. 깊은 병합이 필요하다면 별도의 로직을 추가해야 한다.
BehaviorSubject 인스턴스를 외부에 직접 노출하면 누구나 next를 호출해서 상태를 임의로 바꿀 수 있게 된다. 예제처럼 getState, setState, subscribe라는 제한된 인터페이스로만 감싸서 노출해야 상태 변경 경로를 통제할 수 있다.
구독이 끝난 뒤에는 반드시 구독 해제(unsubscribe)를 호출해야 한다. store.subscribe(...)가 반환하는 구독 객체를 변수에 저장해두고, 더 이상 필요 없을 때 subscription.unsubscribe();를 호출하지 않으면 메모리 누수로 이어질 수 있다.
7. 핵심 정리
BehaviorSubject는 초기값을 가지고 시작하며 항상 현재 값을 내부에 보관하는 Subject로, 구독 시점과 무관하게 최신 상태를 즉시 전달한다는 특징이 있다. 이 특징을 활용하면 getState, setState, subscribe라는 세 가지 인터페이스만으로 최소 단위의 상태 저장소를 직접 구현할 수 있다. 실무에서는 프레임워크에 종속되지 않는 공유 상태를 표현하거나, 내부 BehaviorSubject를 캡슐화해 상태 변경 경로를 하나로 통제하는 패턴으로 자주 활용된다.