jsx와 관련된 이전의 포스팅들에 이어서 컴포넌트에 종속되는 상태관리가 아닌 컴포넌트 외부에서 상태를 관리하여 화면을 렌더링할 수 있는 기능을 추가하려고 한다.
기능을 추가하기 앞서서 공부해두면 좋을 키워드이다.
싱글톤 패턴
옵저버 패턴
ES6 module once evaluation
어플리케이션 시작되고 끝날 때까지 딱 한 번의 인스턴스를 유지하고 싶을 때 사용하는 패턴으로 나의 경우에는 중앙저장소 개념으로 사용하기 위해 싱글톤 패턴을 사용하였다.
기존에 만들어 놓은 컴포넌트는 상태가 업데이트가 되면 화면을 리렌더링하는 방식으로 동작된다. 그렇다면 상태관리를 컴포넌트 내부에서 아닌 외부에서 사용하게 될텐데 이 상태 업데이트에 대해서 감지를 하고 렌더링을 해줄 수 있는 방법이 필요했고 그 해결법은 옵저버 패턴을 활용하는 것이다.
es6의 모듈은 단 한번만 평가되는 사실을 기억하면 좋다. 모듈들은 자신만의 scope가 존재하기 때문에 외부에서 모듈에 정의된 함수 또는 변수등을 접근할 수 없다. 따라서 우리가 평소에 export 키워드를 사용하여 외부로 공개하여 사용할 수 있었던 것이고 해당 모듈을 호출할 때마다 평가하는 것이 아닌 최초 호출시에만 평가된다.
나는 이 특성을 이용하여 특정 상태에 특정 함수를 호출할 수 있도록 최초에 단 한번만 옵저버를 등록하는 동작을 구현할 것이다.
중앙 저장소에는 관리할 상태와 그 상태에 대해 구독을 하고 있는 observers로 이루어진 객체를 가지며 상태를 업데이트하고 가져올 수 있는 메서드가 작성되어 있다. 다만 여러 곳에서 쓰일 수 있기 때문에 unique한 key값으로 구분하게 되었다.
// Dj/store/store.ts
interface IAtomStore {
state: Record<string, any>;
observers: Set<FuncType>;
}
class Store {
globalState: Record<string, IAtomStore>;
constructor() {
this.globalState = {};
}
getGlobalState(key: string) {
return this.globalState[key].state;
}
setGlobalState(key: string, newState: Record<string, any>) {
this.globalState[key].state = {
...this.globalState[key].state,
...newState,
};
}
}
그리고 중앙 저장소가 여러개 존재하면 안되기 때문에 외부로 해당 클래스를 노출시킬 때 인스턴스를 반환하여 단 하나의 인스턴스만을 가지는 싱글톤 패턴을 적용하였다.
// Component1.tsx
// import Store from './Store';
// Component2.tsx
// import Store from './Store';
export default new Store();
이제 중앙 저장소에 저장된 상태에 대해 업데이트가 되었을 때 감지할 수 있도록 옵저버패턴을 적용해야 한다. 옵저버패턴은 어떤일이 발생했을 때, 그 일을 알려주는 방식으로 실생활에서 유튜버와 구독자라고 생각하면 이해하기 쉬울 것이다.
class Observer {
constructor() {
this.observers = [];
}
subscribe(f) {
this.observers.push(f);
}
unsubscribe(f) {
this.observers = this.observers.filter(subscriber => subscriber !== f);
}
notify(data) {
this.observers.forEach(observer => observer(data));
}
}
일반적인 옵저버패턴을 예시로 본다면 그냥 subscribe()메서드에 콜백함수를 인자로 전달(구독)하고 이 콜백함수를 observers라는 공간에 저장해두면 된다. 그리고 notify() 메서드로 모든 observers에 있는 콜백함수들을 실행(알림)시키면 된다.
이제 옵저버패턴의 느낌을 알았으니 기존의 컴포넌트에 적용해보았다.
먼저 observers들을 Store에서 저장하고 있기 때문에 구독과 알림기능을 할 수 있는 메서드들을 추가해주었다. 그 다음으로 this.setState의 역할을 하는 update 메서드를 추가하여 store의 상태가 업데이트 되면 해당 상태를 구독하고 있는 Observers의 콜백함수를 호출하도록 구현해주었다. 마찬가지로 key값을 활용하여 접근하도록 구현하면 된다. 사실 해당 메서드들을 Store에 때려넣어도 되지만 클래스명에 맞게 역할을 분담해주고자 나누었다.
// Dj/store/observe.ts
import store from './store';
class Observer {
// 구독
subscribe(key: string, observer: FuncType) {
const observers = store.getObservers(key);
observers.add(observer);
}
// 구독해제
unsubscribe(key: string, observer: FuncType) {
const observers = store.getObservers(key);
observers.delete(observer);
}
// 알림
notify(key: string) {
const observers = store.getObservers(key);
observers.forEach((observer: FuncType) => observer());
}
update(key: string, newState: Record<string, any>) {
store.setGlobalState(key, newState);
this.notify(key);
}
}
export default new Observer();
클래스의 메서드에 직접 접근하는 것을 방지하기 위해 일부 메서드들은 유틸함수로 빼두었다. 이렇게 작성해두면 클래스의 메서드명이 바뀌더라도 사용하는 곳에서도 그대로 사용할 수 있고 dot(.)을 쓰지 않고 접근할 수 있어서 코드가 더 읽기 좋았다.
// Dj/store/utils.ts
import observer from './observer';
import store from './store';
export const useGetState = (key: string) => store.getGlobalState(key);
export const useSetState = (key: string, newState: any) =>
observer.update(key, newState);
// Dj/store/index.ts
import Observer from './observer';
import { useGetState, useSetState } from './utils';
export { Observer, useGetState, useSetState };
아직 어떠한 상태 및 구독이 중앙 저장소에 저장되지 않았기 때문에 어떤 상태를 등록하고 그 상태를 구독할 콜백함수를 정의해주어야 한다.
상태를 등록할 때는 최초의 한번만 호출되어야하기 때문에 ES6 module 특성을 이용하기로 했다.
특정 state를 관리할 수 있는 unique key값을 가지며 최초상태를 등록할 수 있는 함수를 호출하는 새로운 모듈을 만들었다. 이렇게 하면 사용하는 곳에서 key값을 활용해야 하기 때문에 해당 모듈을 호출해야하며 여러번 호출하더라도 useAtom함수는 단 한번만 호출되기 때문에 딱 한번만 상태 초기화가 이루어진다.
// store/bookState.ts
import { useAtom } from '../Dj/store/utils';
export const bookStateKey = 'bookStateKey';
// 상태를 초기화하는 함수
const bookState = useAtom({
key: bookStateKey,
initState: {
books: [
{ id: 1, completed: false, content: 'star' },
{ id: 2, completed: true, content: 'rain' }
]
}
});
useAtom()함수는 중앙 저장소에 상태를 초기화하는 함수로 store.ts파일에 추가적으로 메서드를 작성해줘야 한다.
// Dj/store/store.ts
class Store {
globalState: Record<string, IAtomStore>;
constructor() {
this.globalState = {};
}
// 생략...
atom({ key, initState }: { key: string; initState: Record<string, any> }) {
if (Object.prototype.hasOwnProperty.call(this.globalState, key))
throw Error('중복 키');
this.globalState[key] = {
state: initState,
observers: new Set(),
};
}
// 생략...
}
---------------------------------------------------
// Dj/store/utils.ts
// 추가
export const useAtom = ({ key, initState }: { key: string; initState: any }) =>
store.atom({ key, initState });
// Dj/store/index.ts
import { useAtom, useGetState, useSetState } from './utils';
// 생략...
// useAtom 추가
export { Observer, useGetState, useSetState, useAtom };
이제 모든 준비단계는 끝났고 실제로 사용하고자 하는 컴포넌트에서 특정 상태에 대해 구독을 하겠다고 알려주기만 하면 된다.
나의 경우에는 todoList의 상태값들을 구독하고 상태값들이 변경되었을 때 화면을 리렌더링하려고 하고자 한다. 그러기 위해서는 컴포넌트가 생성되는 생성자함수 내부에서 Observer클래스의 subscribe메서드에 콜백함수만 전달해주면 끝이다.
import Todo from './components/Todo';
import { Observer, useGetState, useSetState } from './Dj/store';
import { bookStateKey } from './store/bookState';
class App extends Dj.Component {
constructor(props: AttributeType){
super(props);
this.state = {
books: useGetState(bookStateKey).books
};
// bookStateKey에 해당하는 상태를 구독하고 변화가 일어나면 this.setState를 호출하겠다.
Observer.subscribe(bookStateKey, () => this.setState({books: useGetState(bookStateKey).books}));
}
addItem(item: string) {
// 새로운 todoItem을 기존의 배열에 추가하는 작업
useSetState(bookStateKey, {
books: newBooks
});
}
render() {
const {books} = this.state;
return (
<div>
<h2>투두리스트</h2>
<Todo books={books} addItem={this.addItem} checkItem={this.checkItem} removeItem={this.removeItem} />
</div>
)
}
}
Dj.render(<App />, document.querySelector('#root'));
import { bookStateKey } from './store/bookState';를 import 후 bookStateKey 변수를 사용하는 순간 해당 key으로 관리되는 상태가 초기화
기존에 하나의 컴포넌트에 종속되어 있는 상태가 중앙 상태 관리개념으로 변경되어 다른 컴포넌트에서도 해당 상태를 공유할 수 있어 컴포넌트간에 의존성을 낮출 수 있게 되었다. 👍
https://github.com/Danji-ya/study-space/tree/main/JS_usingJSX%26Diff_algorithm/v4