Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<!doctype html>
<html lang="en">
<!DOCTYPE html>
<html lang="ko">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋아요 👍

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
<title>Store 적용</title>
</head>
<body>
<div id="app"></div>
Expand Down
40 changes: 40 additions & 0 deletions src/App.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Component from './core/Component';
import { store, setA, setB } from './store';

const InputA = () => `
<input id="stateA" type="number" value="${store.getState().a}" size="5"/>
`;

const InputB = () => `
<input id="stateB" type="number" value="${store.getState().b}" size="5"/>
`;

const Calculator = () => `
<p>a + b = ${store.getState().a + store.getState().b}</p>
`;

export class App extends Component {
template(): string {
return `
${InputA()}
${InputB()}
${Calculator()}
`;
}

setEvent(): void {
const { $el } = this;

$el.querySelector('#stateA')?.addEventListener('change', ({ target }) => {
if (target && target instanceof HTMLInputElement) {
store.dispatch(setA(Number(target.value)));
}
});

$el.querySelector('#stateB')?.addEventListener('change', ({ target }) => {
if (target && target instanceof HTMLInputElement) {
store.dispatch(setB(Number(target.value)));
}
});
}
}
43 changes: 43 additions & 0 deletions src/core/Component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { observable, observe } from './observer';

export type TComponentData = Record<string, any>;

export interface IState {
[key: string]: number;
}

export default class Component<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IStateTComponentData가 어색해보입니다. 하나로 통일 해야하지 않을까? 라는 생각이 들어요.

요즘은 인터페이스 앞에 I를 붙이지 않는다는 네이밍 컨벤션을 본적 있어서 관련한 아티클을 봐보면 좋을것 같아요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 찾아보니 헝가리안 표기법을 지양하는 개발자들이 많군요 참고할게요

State extends TComponentData = IState,
Props = TComponentData,
> {
state!: State;
props: Props;
$el: HTMLElement;

constructor($el: HTMLElement, props: Props) {
this.$el = $el;
this.props = props;
this.setup();
}

setup() {
this.state = observable(this.initState()) as State;
observe(() => {
this.render();
this.setEvent();
this.mounted();
});
}

initState() {
return {};
}
template() {
return '';
}
render() {
this.$el.innerHTML = this.template();
}
setEvent() {}
mounted() {}
}
46 changes: 46 additions & 0 deletions src/core/ReduxStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { TAction } from '../store';
import { observable } from './observer';

interface IStateA {
a: number;
b: number;
}

interface IStateB {
b: any;
a: number;
}

type IReducerState =
| (IStateA & { [key: string]: any })
| (IStateB & { [key: string]: any });

interface IReducer {
(state?: IReducerState, action?: TAction): IReducerState;
}

export const createStore = (reducer: IReducer) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 코드를 보면 createStore에는 상태의 키가 고정이 되어있어요. 이렇게되면 커스터마이징한 객체를 사용하지 못할것 같아요

image

즉 스토어는 어떤 타입이던, 어떤 키값이던 사용할 수 있어야하는데, 현재 코드는 객체의 키값인 { a, b } 만 사용할 수 있어요. 타입을 제네릭으로받아 해당 타입들을 추론할 수 있도록 바꿔야할 것 같아요.

추가적으로 현재 코드는 객체만 상태를 지정할 수 있도록 하였는데, Redux나 Zustand는 문자나 넘버타입도 가능하도록 되어있어요.

const initialState: IReducerState = reducer();
const state = observable(initialState);

const frozenState: { [key: string]: any } = {};
Object.keys(state).forEach((key) => {
Object.defineProperty(frozenState, key, {
get: () => state[key],
});
});

const dispatch = (action: TAction) => {
const newState = reducer(state, action);

for (const [key, value] of Object.entries(newState)) {
if (key in state) {
state[key] = value;
}
}
};

const getState = () => frozenState;

return { getState, dispatch };
};
29 changes: 29 additions & 0 deletions src/core/observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
type TObserver = () => void;

let currentObserver: TObserver | null = null;

export const observe = (fn: TObserver): void => {
currentObserver = fn;
fn();
currentObserver = null;
};
export const observable = <T extends Record<string, any>>(obj: T): T => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

옵저버 패턴에는 subscribe, unsubscribe, notify가 보통은 필수적으로 들어가는데, 현재 코드에서는 unsubscribe가 없네요. 이런 부분도 고민해보면 좋을것 같아요.

Object.keys(obj).forEach((key) => {
let _value = obj[key];
const observers: Set<TObserver> = new Set();

Object.defineProperty(obj, key, {
get() {
if (currentObserver) observers.add(currentObserver);
return _value;
},

set(value) {
_value = value;
observers.forEach((fn) => fn());
},
});
});

return obj;
};
15 changes: 14 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
// your code
import { App } from './App';

class Main {
constructor() {
const $app: Element | null = document.querySelector('#app');
if ($app instanceof HTMLElement) {
new App($app, {});
} else {
console.error('Element with ID "app" not found.');
}
}
}

new Main();
28 changes: 28 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createStore } from './core/ReduxStore';

export type TAction = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type은 옵셔널이 되면 안돼요. 왜냐하면 Action을 사용할 때 type은 필수여야만 합니다! 리덕스랑 타입스크립트를 사용해보시면 type이 옵셔널이 아니라는것을 알 수 있어요.

type?: string;
payload?: any;
};

const initState = {
a: 10,
b: 20,
};

export const SET_A = 'SET_A';
export const SET_B = 'SET_B';

export const store = createStore((state = initState, action: TAction = {}) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TAction = {} 부분에 기본 값으로 빈 객체가 들어가면 안될것 같아요. 결국 액션은 dispatch로 스토어의 값을 변경하는 목적이지만, 작성 규칙에서 엄격하다고 볼 수 없을것 같아요.

switch (action.type) {
case 'SET_A':
return { ...state, a: action.payload };
case 'SET_B':
return { ...state, b: action.payload };
default:
return state;
}
});

export const setA = (payload: number) => ({ type: SET_A, payload });
export const setB = (payload: number) => ({ type: SET_B, payload });
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
Loading