From ddabff30b502af3d3171436cf30f436c55d24f8a Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 09:49:44 +0900 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=94=A7=20Chore:=20Prettier=20settin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.prettierrc b/.prettierrc index 9ad9a45..a054ec0 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,10 +1,10 @@ { "singleQuote": true, - "semi": true, + "semi": false, "useTabs": false, "tabWidth": 2, "trailingComma": "all", - "printWidth": 80, + "printWidth": 120, "bracketSpacing": true, "arrowParens": "always", "endOfLine": "auto" From c72b4b0b05402cd4c6957e1f4b8a3ddb8bc0695a Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 09:50:42 +0900 Subject: [PATCH 02/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20index.ht?= =?UTF-8?q?ml=20lang=20en=20to=20ko?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 44a9335..63b3534 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,10 @@ - - + + - Vite + TS + 상태관리 만들기
From 8248cd0c95eef3ceb0486835bd3e5a84b40ce22c Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 10:20:27 +0900 Subject: [PATCH 03/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20Observer=20Pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 5aaaaae..9a09a53 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1 +1,58 @@ -// your code +type State = Record +type ObserverFn = (data: T) => void + +class Store { + private _state: TState + private observers: Set> + + constructor(state: TState) { + this._state = state + this.observers = new Set() + } + + get state() { + return { ...this._state } + } + + setState(newState: State) { + this._state = { ...this._state, ...newState } + this.notify() + } + + subscribe(observer: ObserverFn) { + this.observers.add(observer) + } + + notify() { + this.observers.forEach((observer) => observer(this._state)) + } +} + +interface InitialState { + a: number + b: number +} + +class Observer { + private fn: ObserverFn + + constructor(fn: ObserverFn) { + this.fn = fn + } + + subscribe(store: Store) { + store.subscribe(this.fn) + } +} + +const store = new Store({ a: 10, b: 20 }) + +const add = new Observer((data) => console.log(`a + b = ${data.a + data.b}`)) +const multiple = new Observer((data) => console.log(`a * b = ${data.a * data.b}`)) + +add.subscribe(store) +multiple.subscribe(store) + +store.notify() + +store.setState({ a: 100, b: 200 }) From c8a5ee9cbda7675810e9bb5aad4f4a04d3e142e4 Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 10:36:45 +0900 Subject: [PATCH 04/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=ED=99=94=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 80 +++++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/src/main.ts b/src/main.ts index 9a09a53..eff8704 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,58 +1,52 @@ type State = Record -type ObserverFn = (data: T) => void - -class Store { - private _state: TState - private observers: Set> - - constructor(state: TState) { - this._state = state - this.observers = new Set() - } - - get state() { - return { ...this._state } - } - - setState(newState: State) { - this._state = { ...this._state, ...newState } - this.notify() - } - - subscribe(observer: ObserverFn) { - this.observers.add(observer) - } - - notify() { - this.observers.forEach((observer) => observer(this._state)) - } -} +type ObserverFn = (...args: T[]) => void interface InitialState { a: number b: number } -class Observer { - private fn: ObserverFn +function observable(initialState: TState) { + Object.keys(initialState).forEach((key) => { + let _value = initialState[key] + const observers = new Set() as Set> + + Object.defineProperty(initialState, key, { + get() { + if (currentObserver) { + observers.add(currentObserver) + } + + return _value + }, + set(value) { + _value = value + observers.forEach((observer) => observer()) + }, + }) + }) + + return initialState +} - constructor(fn: ObserverFn) { - this.fn = fn - } +let currentObserver: null | (() => void) = null - subscribe(store: Store) { - store.subscribe(this.fn) - } +function observer(fn: () => void) { + currentObserver = fn + fn() + currentObserver = null } -const store = new Store({ a: 10, b: 20 }) +// a를 읽을 때 a가 observers에 등록해야함. -const add = new Observer((data) => console.log(`a + b = ${data.a + data.b}`)) -const multiple = new Observer((data) => console.log(`a * b = ${data.a * data.b}`)) +const store = observable({ a: 10, b: 20 }) -add.subscribe(store) -multiple.subscribe(store) +observer(() => console.log(`a = ${store.a}`)) +observer(() => console.log(`b = ${store.b}`)) +observer(() => console.log(`a + b = ${store.a} + ${store.b}`)) +observer(() => console.log(`a * b = ${store.a} + ${store.b}`)) +observer(() => console.log(`a - b = ${store.a} + ${store.b}`)) -store.notify() +store.a = 100 -store.setState({ a: 100, b: 200 }) +store.b = 200 From 44b3865e6afabe252bf60a36d6db226ae345dda0 Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 10:38:39 +0900 Subject: [PATCH 05/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20Component=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/component.ts | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/core/component.ts diff --git a/src/core/component.ts b/src/core/component.ts new file mode 100644 index 0000000..bd9af96 --- /dev/null +++ b/src/core/component.ts @@ -0,0 +1,65 @@ +import { observable, observe } from './observer' + +export default class Component< + TElement extends HTMLElement, + Props extends Record = Record, + State extends Record = Record, +> { + element: TElement + props: Props + private _state: State + + constructor(element: TElement, props?: Props, state?: State) { + this.element = element + this.props = props || ({} as Props) + this._state = state || ({} as State) + + this.setup() + } + + get state() { + return { ...this._state } + } + + set state(newState) { + this._state = { ...this._state, ...newState } + } + + setup() { + this.state = observable(this.initState()) + observe(() => { + this.render() + this.setEvent() + this.mounted() + }) + } + + initState() { + return {} as State + } + + template() { + return '' + } + + render() { + this.element.innerHTML = this.template() + } + + addEvent( + eventType: keyof HTMLElementEventMap, + selector: keyof HTMLElementTagNameMap | string, + callback: EventListener, + ) { + this.element.addEventListener(eventType, (event) => { + const target = event.target as HTMLElement + if (target.closest(selector)) { + callback(event) + } + }) + } + + setEvent() {} + + mounted() {} +} From 119f8b90104da22939f6df8ac6f79de25b39fbdd Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 10:39:03 +0900 Subject: [PATCH 06/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20DOM=EC=97=90=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/observer.ts | 35 +++++++++++++++++++++++++ src/main.ts | 61 ++++++++++++-------------------------------- 2 files changed, 52 insertions(+), 44 deletions(-) create mode 100644 src/core/observer.ts diff --git a/src/core/observer.ts b/src/core/observer.ts new file mode 100644 index 0000000..1c2336e --- /dev/null +++ b/src/core/observer.ts @@ -0,0 +1,35 @@ +type Fn = (...args: any[]) => void +type State = Record + +let currentObserver: Fn | null = null + +export function observe(fn: Fn) { + currentObserver = fn + fn() + currentObserver = null +} + +export function observable(state: T) { + const stateKeys = Object.keys(state) + + stateKeys.forEach((key) => { + let _value = state[key] + const observers = new Set() as Set + + Object.defineProperty(state, key, { + get() { + if (currentObserver) { + observers.add(currentObserver) + } + return _value + }, + + set(value) { + _value = value + observers.forEach((observer) => observer()) + }, + }) + }) + + return state +} diff --git a/src/main.ts b/src/main.ts index eff8704..def1610 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,52 +1,25 @@ -type State = Record -type ObserverFn = (...args: T[]) => void +import { observable, observe } from './core/observer.ts' -interface InitialState { - a: number - b: number -} +const state = observable({ a: 10, b: 20 }) -function observable(initialState: TState) { - Object.keys(initialState).forEach((key) => { - let _value = initialState[key] - const observers = new Set() as Set> +const rootElement = document.getElementById('app')! as HTMLDivElement - Object.defineProperty(initialState, key, { - get() { - if (currentObserver) { - observers.add(currentObserver) - } +const render = () => { + rootElement.innerHTML = ` +

a + b = ${state.a + state.b}

+ + + ` - return _value - }, - set(value) { - _value = value - observers.forEach((observer) => observer()) - }, - }) + rootElement.querySelector('#stateA')!.addEventListener('change', (event) => { + const target = event.target as HTMLInputElement + state.a = Number(target.value) }) - return initialState -} - -let currentObserver: null | (() => void) = null - -function observer(fn: () => void) { - currentObserver = fn - fn() - currentObserver = null + rootElement.querySelector('#stateB')!.addEventListener('change', (event) => { + const target = event.target as HTMLInputElement + state.b = Number(target.value) + }) } -// a를 읽을 때 a가 observers에 등록해야함. - -const store = observable({ a: 10, b: 20 }) - -observer(() => console.log(`a = ${store.a}`)) -observer(() => console.log(`b = ${store.b}`)) -observer(() => console.log(`a + b = ${store.a} + ${store.b}`)) -observer(() => console.log(`a * b = ${store.a} + ${store.b}`)) -observer(() => console.log(`a - b = ${store.a} + ${store.b}`)) - -store.a = 100 - -store.b = 200 +observe(render) From c8beed54547046c00dda83eba66b8932b68e64c7 Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 11:12:17 +0900 Subject: [PATCH 07/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=99=B8=EB=B6=80=EC=97=90=EC=84=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 9 +++++---- src/store.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 src/store.ts diff --git a/src/main.ts b/src/main.ts index def1610..d777375 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ -import { observable, observe } from './core/observer.ts' +import { observe } from './core/observer.ts' +import { store } from './store.ts' -const state = observable({ a: 10, b: 20 }) +const { state, setState } = store({ a: 10, b: 20 }) const rootElement = document.getElementById('app')! as HTMLDivElement @@ -13,12 +14,12 @@ const render = () => { rootElement.querySelector('#stateA')!.addEventListener('change', (event) => { const target = event.target as HTMLInputElement - state.a = Number(target.value) + setState({ a: Number(target.value) }) }) rootElement.querySelector('#stateB')!.addEventListener('change', (event) => { const target = event.target as HTMLInputElement - state.b = Number(target.value) + setState({ b: Number(target.value) }) }) } diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..b2960c7 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,14 @@ +import { observable } from './core/observer' + +export const store = = Record>(initialState: State) => { + const state = observable(initialState) + return { + state, + setState(newState: Partial) { + for (const [key, value] of Object.entries(newState)) { + if (!(key in state)) return + state[key as keyof typeof newState] = value + } + }, + } +} From ffb73a0ed9de64b1cb8ff1ee997997341f0d7b3b Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 12:49:35 +0900 Subject: [PATCH 08/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=B5=9C=EC=86=8C?= =?UTF-8?q?=ED=99=94=EB=90=9C=20Redux=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/component.ts | 65 ----------------------------------------- src/core/observer.ts | 35 ---------------------- src/core/redux.ts | 31 ++++++++++++++++++++ src/core/redux.type.ts | 7 +++++ src/core/redux.utils.ts | 5 ++++ src/main.ts | 18 ++++++------ src/reducer.ts | 31 ++++++++++++++++++++ src/store.ts | 14 --------- 8 files changed, 83 insertions(+), 123 deletions(-) delete mode 100644 src/core/component.ts delete mode 100644 src/core/observer.ts create mode 100644 src/core/redux.ts create mode 100644 src/core/redux.type.ts create mode 100644 src/core/redux.utils.ts create mode 100644 src/reducer.ts delete mode 100644 src/store.ts diff --git a/src/core/component.ts b/src/core/component.ts deleted file mode 100644 index bd9af96..0000000 --- a/src/core/component.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { observable, observe } from './observer' - -export default class Component< - TElement extends HTMLElement, - Props extends Record = Record, - State extends Record = Record, -> { - element: TElement - props: Props - private _state: State - - constructor(element: TElement, props?: Props, state?: State) { - this.element = element - this.props = props || ({} as Props) - this._state = state || ({} as State) - - this.setup() - } - - get state() { - return { ...this._state } - } - - set state(newState) { - this._state = { ...this._state, ...newState } - } - - setup() { - this.state = observable(this.initState()) - observe(() => { - this.render() - this.setEvent() - this.mounted() - }) - } - - initState() { - return {} as State - } - - template() { - return '' - } - - render() { - this.element.innerHTML = this.template() - } - - addEvent( - eventType: keyof HTMLElementEventMap, - selector: keyof HTMLElementTagNameMap | string, - callback: EventListener, - ) { - this.element.addEventListener(eventType, (event) => { - const target = event.target as HTMLElement - if (target.closest(selector)) { - callback(event) - } - }) - } - - setEvent() {} - - mounted() {} -} diff --git a/src/core/observer.ts b/src/core/observer.ts deleted file mode 100644 index 1c2336e..0000000 --- a/src/core/observer.ts +++ /dev/null @@ -1,35 +0,0 @@ -type Fn = (...args: any[]) => void -type State = Record - -let currentObserver: Fn | null = null - -export function observe(fn: Fn) { - currentObserver = fn - fn() - currentObserver = null -} - -export function observable(state: T) { - const stateKeys = Object.keys(state) - - stateKeys.forEach((key) => { - let _value = state[key] - const observers = new Set() as Set - - Object.defineProperty(state, key, { - get() { - if (currentObserver) { - observers.add(currentObserver) - } - return _value - }, - - set(value) { - _value = value - observers.forEach((observer) => observer()) - }, - }) - }) - - return state -} diff --git a/src/core/redux.ts b/src/core/redux.ts new file mode 100644 index 0000000..b8a14aa --- /dev/null +++ b/src/core/redux.ts @@ -0,0 +1,31 @@ +import type { Reducer, ActionType, ListenerCallback } from './redux.type' +import { ActionTypes } from './redux.utils' + +export function createStore(reducer: Reducer) { + let currentState: State + let currentListenerId = 0 + let currentListeners: Map = new Map() + + const getState = () => ({ ...currentState }) + + const dispatch = (action: Action) => { + currentState = reducer(currentState, action) + currentListeners.forEach((listener) => listener()) + return action + } + + const subscribe = (listeners: ListenerCallback) => { + const listenerId = currentListenerId++ + currentListeners.set(listenerId, listeners) + currentListeners.forEach((listener) => listener()) + return () => {} + } + + dispatch({ type: ActionTypes.INIT } as Action) + + return { + getState, + dispatch, + subscribe, + } +} diff --git a/src/core/redux.type.ts b/src/core/redux.type.ts new file mode 100644 index 0000000..dd88b9e --- /dev/null +++ b/src/core/redux.type.ts @@ -0,0 +1,7 @@ +export type Reducer = (state: State, action: Action) => State + +export type ListenerCallback = () => void + +export interface ActionType { + type: T +} diff --git a/src/core/redux.utils.ts b/src/core/redux.utils.ts new file mode 100644 index 0000000..81e6b95 --- /dev/null +++ b/src/core/redux.utils.ts @@ -0,0 +1,5 @@ +const randomString = () => Math.random().toString(36).substring(7).split('').join('.') + +export const ActionTypes = { + INIT: `@@redux/INIT${randomString()}`, +} diff --git a/src/main.ts b/src/main.ts index d777375..5d7e9ea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,26 +1,26 @@ -import { observe } from './core/observer.ts' -import { store } from './store.ts' +import { createStore } from './core/redux' +import { additionReducer, ACTION_TYPE } from './reducer' -const { state, setState } = store({ a: 10, b: 20 }) +const { getState, dispatch, subscribe } = createStore(additionReducer) const rootElement = document.getElementById('app')! as HTMLDivElement const render = () => { rootElement.innerHTML = ` -

a + b = ${state.a + state.b}

- - +

a + b = ${getState().a + getState().b}

+ + ` rootElement.querySelector('#stateA')!.addEventListener('change', (event) => { const target = event.target as HTMLInputElement - setState({ a: Number(target.value) }) + dispatch({ type: ACTION_TYPE.SET_A, payload: { a: Number(target.value) } }) }) rootElement.querySelector('#stateB')!.addEventListener('change', (event) => { const target = event.target as HTMLInputElement - setState({ b: Number(target.value) }) + dispatch({ type: ACTION_TYPE.SET_B, payload: { b: Number(target.value) } }) }) } -observe(render) +subscribe(render) diff --git a/src/reducer.ts b/src/reducer.ts new file mode 100644 index 0000000..8bb5607 --- /dev/null +++ b/src/reducer.ts @@ -0,0 +1,31 @@ +interface InitialState { + a: number + b: number +} + +const initialState = { + a: 10, + b: 20, +} + +export const ACTION_TYPE = { + SET_A: 'redux/SET_A', + SET_B: 'redux/SET_B', +} as const + +type Action = + | { type: typeof ACTION_TYPE.SET_A; payload: Pick } + | { type: typeof ACTION_TYPE.SET_B; payload: Pick } + +export const additionReducer = (state: InitialState = initialState, action: Action): InitialState => { + switch (action.type) { + case ACTION_TYPE.SET_A: + return { ...state, a: action.payload.a } + + case ACTION_TYPE.SET_B: + return { ...state, b: action.payload.b } + + default: + return state + } +} diff --git a/src/store.ts b/src/store.ts deleted file mode 100644 index b2960c7..0000000 --- a/src/store.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { observable } from './core/observer' - -export const store = = Record>(initialState: State) => { - const state = observable(initialState) - return { - state, - setState(newState: Partial) { - for (const [key, value] of Object.entries(newState)) { - if (!(key in state)) return - state[key as keyof typeof newState] = value - } - }, - } -} From cfd17fc04ac0b0cbe579887c853b8e43ba984fa9 Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 19:21:31 +0900 Subject: [PATCH 09/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20isPlainObject=20util?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/isPlainObject.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/utils/isPlainObject.ts diff --git a/src/utils/isPlainObject.ts b/src/utils/isPlainObject.ts new file mode 100644 index 0000000..1a311e6 --- /dev/null +++ b/src/utils/isPlainObject.ts @@ -0,0 +1,10 @@ +export default function isPlainObject(obj: any): obj is object { + if (typeof obj !== 'object' || obj === null) return false + + let proto = obj + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto) + } + + return Object.getPrototypeOf(obj) === proto || Object.getPrototypeOf(obj) === null +} From 07e8e456801129e02f33ebe7eb5892d6231e3214 Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 19:23:55 +0900 Subject: [PATCH 10/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20counter=20example=20a?= =?UTF-8?q?dd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/main.ts b/src/main.ts index 5d7e9ea..3b8040a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,22 +5,35 @@ const { getState, dispatch, subscribe } = createStore(additionReducer) const rootElement = document.getElementById('app')! as HTMLDivElement +// Todo: 상태가 객체일 때 +// const render = () => { +// rootElement.innerHTML = ` +//

a + b = ${getState().a + getState().b}

+// +// +// ` + +// rootElement.querySelector('#stateA')!.addEventListener('change', (event) => { +// const target = event.target as HTMLInputElement +// dispatch({ type: ACTION_TYPE.SET_A, payload: { a: Number(target.value) } }) +// }) + +// rootElement.querySelector('#stateB')!.addEventListener('change', (event) => { +// const target = event.target as HTMLInputElement +// dispatch({ type: ACTION_TYPE.SET_B, payload: { b: Number(target.value) } }) +// }) +// } + +// Todo: 상태가 number 일 때 const render = () => { rootElement.innerHTML = ` -

a + b = ${getState().a + getState().b}

- - +

${getState()}

+ + ` - rootElement.querySelector('#stateA')!.addEventListener('change', (event) => { - const target = event.target as HTMLInputElement - dispatch({ type: ACTION_TYPE.SET_A, payload: { a: Number(target.value) } }) - }) - - rootElement.querySelector('#stateB')!.addEventListener('change', (event) => { - const target = event.target as HTMLInputElement - dispatch({ type: ACTION_TYPE.SET_B, payload: { b: Number(target.value) } }) - }) + rootElement.querySelector('#increase')!.addEventListener('click', () => dispatch({ type: ACTION_TYPE.INCREASE })) + rootElement.querySelector('#decrease')!.addEventListener('click', () => dispatch({ type: ACTION_TYPE.DECREASE })) } subscribe(render) From ac28340af71e618afbc72ea2a9170dcbf7fdb9d8 Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 19:24:16 +0900 Subject: [PATCH 11/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20counter=20reducer=20e?= =?UTF-8?q?xample=20add?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/reducer.ts | 65 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/src/reducer.ts b/src/reducer.ts index 8bb5607..428e62b 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -1,29 +1,58 @@ -interface InitialState { - a: number - b: number -} +/** + * Object 사용 예시 + */ -const initialState = { - a: 10, - b: 20, -} +// interface InitialState { +// a: number +// b: number +// } + +// const initialState = { +// a: 10, +// b: 20, +// } + +// export const ACTION_TYPE = { +// SET_A: 'redux/SET_A', +// SET_B: 'redux/SET_B', +// } as const + +// type Action = +// | { type: typeof ACTION_TYPE.SET_A; payload: Pick } +// | { type: typeof ACTION_TYPE.SET_B; payload: Pick } + +// export const additionReducer = (state: InitialState = initialState, action: Action): InitialState => { +// switch (action.type) { +// case ACTION_TYPE.SET_A: +// return { ...state, a: action.payload.a } + +// case ACTION_TYPE.SET_B: +// return { ...state, b: action.payload.b } + +// default: +// return state +// } +// } + +/** + * Number 사용 예시 + */ +const initialState = 0 export const ACTION_TYPE = { - SET_A: 'redux/SET_A', - SET_B: 'redux/SET_B', + INCREASE: 'increase', + DECREASE: 'decrease', } as const -type Action = - | { type: typeof ACTION_TYPE.SET_A; payload: Pick } - | { type: typeof ACTION_TYPE.SET_B; payload: Pick } +type Action = { type: typeof ACTION_TYPE.INCREASE } | { type: typeof ACTION_TYPE.DECREASE } -export const additionReducer = (state: InitialState = initialState, action: Action): InitialState => { +export const additionReducer = (state: number = initialState, action: Action): number => { switch (action.type) { - case ACTION_TYPE.SET_A: - return { ...state, a: action.payload.a } + case ACTION_TYPE.INCREASE: + return state + 1 - case ACTION_TYPE.SET_B: - return { ...state, b: action.payload.b } + case ACTION_TYPE.DECREASE: + return state <= 0 ? 0 : state - 1 default: return state From f2996ce23f8fd4f9ae90d7a0ecf60d7cb049707a Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 14 Dec 2024 19:25:03 +0900 Subject: [PATCH 12/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20Redux=20Core=20Except?= =?UTF-8?q?ion=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/redux.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/core/redux.ts b/src/core/redux.ts index b8a14aa..3bd9bc8 100644 --- a/src/core/redux.ts +++ b/src/core/redux.ts @@ -1,3 +1,4 @@ +import isPlainObject from '../utils/isPlainObject' import type { Reducer, ActionType, ListenerCallback } from './redux.type' import { ActionTypes } from './redux.utils' @@ -6,17 +7,30 @@ export function createStore(reducer: Reducer = new Map() - const getState = () => ({ ...currentState }) + const getState = () => currentState const dispatch = (action: Action) => { + if (!isPlainObject(action)) { + throw new Error(`해당 ${action}은 순수 객체가 아니에요! action은 항상 순수 객체여야 합니다.`) + } + + if (typeof action.type !== 'string') { + throw new Error(`action type은 반드시 문자열이어야 합니다.`) + } + currentState = reducer(currentState, action) currentListeners.forEach((listener) => listener()) - return action + + return action as Action } - const subscribe = (listeners: ListenerCallback) => { + const subscribe = (listenerCallback: ListenerCallback) => { + if (typeof listenerCallback !== 'function') { + throw new Error(`subscribe 매개변수는 반드시 함수이어야 합니다.`) + } + const listenerId = currentListenerId++ - currentListeners.set(listenerId, listeners) + currentListeners.set(listenerId, listenerCallback) currentListeners.forEach((listener) => listener()) return () => {} } From 1bba98dbd159bd00b76f6553277ff6c107b1dc78 Mon Sep 17 00:00:00 2001 From: d5ng Date: Sun, 15 Dec 2024 14:26:20 +0900 Subject: [PATCH 13/13] =?UTF-8?q?=E2=9C=A8=20Feat:=20unsubscribe=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/redux.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/redux.ts b/src/core/redux.ts index 3bd9bc8..5fabfb2 100644 --- a/src/core/redux.ts +++ b/src/core/redux.ts @@ -32,7 +32,10 @@ export function createStore(reducer: Reducer listener()) - return () => {} + + return () => { + currentListeners.delete(listenerId) + } } dispatch({ type: ActionTypes.INIT } as Action)