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 +}