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"
diff --git a/index.html b/index.html
index 44a9335..63b3534 100644
--- a/index.html
+++ b/index.html
@@ -1,10 +1,10 @@
-
-
+
+
- Vite + TS
+ 상태관리 만들기
diff --git a/src/core/redux.ts b/src/core/redux.ts
new file mode 100644
index 0000000..5fabfb2
--- /dev/null
+++ b/src/core/redux.ts
@@ -0,0 +1,48 @@
+import isPlainObject from '../utils/isPlainObject'
+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) => {
+ 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 as Action
+ }
+
+ const subscribe = (listenerCallback: ListenerCallback) => {
+ if (typeof listenerCallback !== 'function') {
+ throw new Error(`subscribe 매개변수는 반드시 함수이어야 합니다.`)
+ }
+
+ const listenerId = currentListenerId++
+ currentListeners.set(listenerId, listenerCallback)
+ currentListeners.forEach((listener) => listener())
+
+ return () => {
+ currentListeners.delete(listenerId)
+ }
+ }
+
+ 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 5aaaaae..3b8040a 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1 +1,39 @@
-// your code
+import { createStore } from './core/redux'
+import { additionReducer, ACTION_TYPE } from './reducer'
+
+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 = `
+ ${getState()}
+
+
+ `
+
+ rootElement.querySelector('#increase')!.addEventListener('click', () => dispatch({ type: ACTION_TYPE.INCREASE }))
+ rootElement.querySelector('#decrease')!.addEventListener('click', () => dispatch({ type: ACTION_TYPE.DECREASE }))
+}
+
+subscribe(render)
diff --git a/src/reducer.ts b/src/reducer.ts
new file mode 100644
index 0000000..428e62b
--- /dev/null
+++ b/src/reducer.ts
@@ -0,0 +1,60 @@
+/**
+ * Object 사용 예시
+ */
+
+// 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 = {
+ INCREASE: 'increase',
+ DECREASE: 'decrease',
+} as const
+
+type Action = { type: typeof ACTION_TYPE.INCREASE } | { type: typeof ACTION_TYPE.DECREASE }
+
+export const additionReducer = (state: number = initialState, action: Action): number => {
+ switch (action.type) {
+ case ACTION_TYPE.INCREASE:
+ return state + 1
+
+ case ACTION_TYPE.DECREASE:
+ return state <= 0 ? 0 : state - 1
+
+ default:
+ return state
+ }
+}
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
+}