상태 관리는 웹 애플리케이션에서 가장 중요한 부분 중 하나입니다. 전역 변수 사용부터 React 후크, MobX, Redux 또는 XState와 같은 타사 라이브러리 사용에 이르기까지 3가지 이름만 지정하는 것은 가장 많은 토론을 불러일으키는 주제 중 하나입니다. 안정적이고 효율적인 애플리케이션입니다.
오늘은 Observable 개념을 기반으로 50줄 미만의 JavaScript로 미니 상태 관리 라이브러리를 구축할 것을 제안합니다. 이는 확실히 소규모 프로젝트의 경우 있는 그대로 사용할 수 있지만, 이 교육 활동 외에도 실제 프로젝트에서는 보다 표준화된 솔루션으로 전환하는 것이 좋습니다.
새 라이브러리 프로젝트를 시작할 때 기술 구현 세부 사항을 고려하기 전에 개념을 고정하고 개발을 안내하기 위해 처음부터 API의 모습을 정의하는 것이 중요합니다. 실제 프로젝트의 경우 TDD 접근 방식에 따라 작성된 라이브러리 구현을 검증하기 위해 이때 테스트 작성을 시작하는 것도 가능합니다.
여기서 초기 상태를 포함하는 객체로 인스턴스화될 State라고 부르는 단일 클래스와 관찰자를 통해 상태 변경 사항을 구독할 수 있는 단일 관찰 메서드를 내보내려고 합니다. 이러한 관찰자는 종속성 중 하나가 변경된 경우에만 실행되어야 합니다.
상태를 변경하려면 setState와 같은 메소드를 거치지 않고 클래스 속성을 직접 사용하고 싶습니다.
코드 조각은 천 마디 말보다 중요하므로 최종 구현은 다음과 같습니다.
const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.count = 1; state.text = 'Hello, world!'; state.count = 1; // Output: // Count changed 1 // Text changed Hello, world! // Count changed 2
생성자에서 초기 상태를 받아들이고 나중에 구현할 관찰 메서드를 노출하는 State 클래스를 만드는 것부터 시작하겠습니다.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; } observe(observer) { this.observers.push(observer); } }
여기에서는 상태 값을 유지할 수 있는 내부 중간 상태 개체를 사용하기로 선택했습니다. 또한 이 구현을 완료할 때 유용할 내부 관찰자 배열에 관찰자를 저장합니다.
이 두 속성은 이 클래스 내에서만 사용되므로 접두사 #를 붙이고 클래스에 초기 선언을 추가하여 약간의 구문 설탕을 사용하여 비공개로 선언할 수 있습니다.
class State { #state = {}; #observers = []; constructor(initialState = {}) { this.#state = initialState; this.#observers = []; } observe(observer) { this.#observers.push(observer); } }
원칙적으로 이는 좋은 방법이지만 다음 단계에서는 프록시를 사용할 예정이며 개인 자산과 호환되지 않습니다. 자세히 설명하지 않고 구현을 더 쉽게 하기 위해 지금은 공용 속성을 사용하겠습니다.
이 프로젝트의 사양을 개략적으로 설명할 때 내부 상태 개체에 대한 항목이 아닌 클래스 인스턴스에서 직접 상태 값에 액세스하고 싶었습니다.
이를 위해 클래스가 초기화될 때 반환될 프록시 객체를 사용합니다.
이름에서 알 수 있듯이 프록시를 사용하면 개체가 getter 및 setter를 포함한 특정 작업을 가로채는 중개자를 만들 수 있습니다. 우리의 경우 상태 객체의 입력이 State 인스턴스에 직접 속한 것처럼 노출할 수 있는 첫 번째 getter를 노출하는 프록시를 만듭니다.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); console.log(state.count); // 0
이제 State를 인스턴스화할 때 초기 상태 객체를 정의한 다음 해당 인스턴스에서 직접 해당 값을 검색할 수 있습니다. 이제 데이터를 조작하는 방법을 살펴보겠습니다.
getter를 추가했으므로 다음 논리적 단계는 상태 개체를 조작할 수 있는 setter를 추가하는 것입니다.
먼저 키가 이 객체에 속하는지 확인한 다음 불필요한 업데이트를 방지하기 위해 값이 실제로 변경되었는지 확인한 다음 마지막으로 객체를 새 값으로 업데이트합니다.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; } } else { target[prop] = value; } }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); console.log(state.count); // 0 state.count = 1; console.log(state.count); // 1
이제 데이터 읽기 및 쓰기 부분이 완료되었습니다. 상태 값을 변경한 다음 해당 변경 사항을 검색할 수 있습니다. 지금까지의 구현은 그다지 유용하지 않으므로 이제 관찰자를 구현해 보겠습니다.
인스턴스에 선언된 관찰자 함수를 포함하는 배열이 이미 있으므로 값이 변경될 때마다 하나씩 호출하기만 하면 됩니다.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; this.observers.forEach((observer) => { observer(this.state); }); } } else { target[prop] = value; } }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.count = 1; state.text = 'Hello, world!'; // Output: // Count changed 1 // Text changed // Count changed 1 // Text changed Hello, world!
좋습니다. 이제 데이터 변경에 대응하고 있습니다!
그래도 작은 문제입니다. 지금까지 주의를 기울였다면 원래는 종속성 중 하나가 변경된 경우에만 관찰자를 실행하려고 했습니다. 그러나 이 코드를 실행하면 상태의 일부가 변경될 때마다 각 관찰자가 실행되는 것을 볼 수 있습니다.
그런데 이러한 기능의 종속성을 어떻게 식별할 수 있습니까?
다시 한번, 프록시가 우리를 구출해 줍니다. 관찰자 함수의 종속성을 식별하기 위해 상태 객체의 프록시를 생성하고 이를 인수로 실행하고 어떤 속성에 액세스했는지 확인할 수 있습니다.
간단하지만 효과적입니다.
관찰자를 호출할 때 우리가 해야 할 일은 업데이트된 속성에 대한 종속성이 있는지 확인하고 그럴 경우에만 트리거하는 것입니다.
다음은 마지막 부분이 추가된 미니 라이브러리의 최종 구현입니다. 이제 관찰자 배열에 각 관찰자의 종속성을 유지할 수 있는 개체가 포함되어 있음을 알 수 있습니다.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; this.observers.forEach(({ observer, dependencies }) => { if (dependencies.has(prop)) { observer(this.state); } }); } } else { target[prop] = value; } }, }); } observe(observer) { const dependencies = new Set(); const proxy = new Proxy(this.state, { get: (target, prop) => { dependencies.add(prop); return target[prop]; }, }); observer(proxy); this.observers.push({ observer, dependencies }); } } const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.observe((state) => { console.log('Count or text changed', state.count, state.text); }); state.count = 1; state.text = 'Hello, world!'; state.count = 1; // Output: // Count changed 0 // Text changed // Count or text changed 0 // Count changed 1 // Count or text changed 1 // Text changed Hello, world! // Count or text changed 1 Hello, world! // Count changed 2 // Count or text changed 2 Hello, world!
그리고 45줄의 코드로 JavaScript로 미니 상태 관리 라이브러리를 구현했습니다.
더 나아가고 싶다면 JSDoc으로 유형 제안을 추가하거나 TypeScript로 이를 다시 작성하여 상태 인스턴스의 속성에 대한 제안을 얻을 수 있습니다.
State.observe에서 반환된 객체에 노출되는 unobserve 메서드를 추가할 수도 있습니다.
한 번에 여러 속성을 수정할 수 있는 setState 메서드로 setter 동작을 추상화하는 것도 유용할 수 있습니다. 현재 상태의 각 속성을 하나씩 수정해야 하며, 이로 인해 일부 관찰자가 종속성을 공유하는 경우 여러 관찰자가 트리거될 수 있습니다.
어쨌든 여러분도 저만큼 이 작은 연습을 즐겼기를 바라며, 이를 통해 JavaScript의 프록시 개념을 좀 더 깊이 탐구할 수 있었기를 바랍니다.
부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.
Copyright© 2022 湘ICP备2022001581号-3