From b10624386ee92a24fc7ee76e8210bc0f42054f87 Mon Sep 17 00:00:00 2001 From: yurim Date: Tue, 2 Nov 2021 15:23:03 +0900 Subject: [PATCH 1/3] Update step3.md --- step3/step3.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 step3/step3.md diff --git a/step3/step3.md b/step3/step3.md new file mode 100644 index 0000000..9533230 --- /dev/null +++ b/step3/step3.md @@ -0,0 +1,9 @@ +# 요구사항 체크리스트 + +- [ ] 이론 + - [ ] 옵저버 패턴에 대해 조사하기 + - [ ] Flux 패턴에 대해 조사하기 +- [ ] 실습 + - [ ] Class Component에 옵저버 패턴 적용하기 + +# 이번주 회고 From ca760a01e85f464b4dab66a05a955c3f01d6ddff Mon Sep 17 00:00:00 2001 From: yurim Date: Sun, 7 Nov 2021 14:53:57 +0900 Subject: [PATCH 2/3] Add: step3 VanillaJS TodoList with Observer Pattern --- step3/src/index.html | 13 ++++ step3/src/js/TodoApp.js | 93 +++++++++++++++++++++++++++ step3/src/js/components/TodoHeader.js | 9 +++ step3/src/js/components/TodoInput.js | 26 ++++++++ step3/src/js/components/TodoList.js | 76 ++++++++++++++++++++++ step3/src/js/core/Component.js | 36 +++++++++++ step3/src/js/core/observer.js | 29 +++++++++ step3/src/js/index.js | 3 + step3/src/js/utils/addEvent.js | 11 ++++ step3/src/js/utils/newGuid.js | 51 +++++++++++++++ 10 files changed, 347 insertions(+) create mode 100644 step3/src/index.html create mode 100644 step3/src/js/TodoApp.js create mode 100644 step3/src/js/components/TodoHeader.js create mode 100644 step3/src/js/components/TodoInput.js create mode 100644 step3/src/js/components/TodoList.js create mode 100644 step3/src/js/core/Component.js create mode 100644 step3/src/js/core/observer.js create mode 100644 step3/src/js/index.js create mode 100644 step3/src/js/utils/addEvent.js create mode 100644 step3/src/js/utils/newGuid.js diff --git a/step3/src/index.html b/step3/src/index.html new file mode 100644 index 0000000..9ba305b --- /dev/null +++ b/step3/src/index.html @@ -0,0 +1,13 @@ + + + + + + + TodoList with Observer Pattern + + +
+ + + diff --git a/step3/src/js/TodoApp.js b/step3/src/js/TodoApp.js new file mode 100644 index 0000000..968db16 --- /dev/null +++ b/step3/src/js/TodoApp.js @@ -0,0 +1,93 @@ +import Component from './core/Component.js'; +import TodoHeader from './components/TodoHeader.js'; +import TodoInput from './components/TodoInput.js'; +import TodoList from './components/TodoList.js'; +import newGuid from './utils/newGuid.js'; +import { observable } from './core/observer.js'; + +export default class TodoApp extends Component { + initState() { + const sampleState = { + todoItems: [ + observable({ + id: newGuid(), + content: '1번 투두', + isComplete: false, + createdAt: Date.now(), + }), + observable({ + id: newGuid(), + content: '2번 투두', + isComplete: true, + createdAt: Date.now(), + }), + observable({ + id: newGuid(), + content: '3번 투두', + isComplete: false, + createdAt: Date.now(), + }), + ], + selectedItem: -1, + }; + + this.state = observable(sampleState); + } + + template() { + return ` +
+
+
+ `; + } + + mounted() { + const $headerContainer = document.querySelector('.header-container'); + const $inputContainer = document.querySelector('.input-container'); + const $listContainer = document.querySelector('.list-container'); + new TodoHeader($headerContainer); + new TodoInput($inputContainer, { + onAdd: this.onAdd.bind(this), + }); + new TodoList($listContainer, { + data: this.state, + onoffEditMode: this.onoffEditMode.bind(this), + onEdit: this.onEdit.bind(this), + onDelete: this.onDelete.bind(this), + onToggle: this.onToggle.bind(this), + }); + } + + onAdd(content) { + const newItem = { + id: newGuid(), + content: content, + createdAt: Date.now(), + isComplete: false, + }; + this.state.todoItems = [...this.state.todoItems, newItem]; + } + + onoffEditMode(id) { + console.log('onEditMode click!***'); + this.state.selectedItem = id; + } + + onEdit(id, content) { + this.state.todoItems.find((todo) => todo.id === id).content = content; + this.state.selectedItem = -1; + } + + onDelete(id) { + const copiedTodos = JSON.parse(JSON.stringify(this.state.todoItems)); + const todoIndex = copiedTodos.findIndex((todo) => todo.id === id); + copiedTodos.splice(todoIndex, 1); + this.state.todoItems = copiedTodos; + } + + onToggle(id) { + const selectedTodo = this.state.todoItems.find((todo) => todo.id === id); + selectedTodo.isComplete = !selectedTodo.isComplete; + } +} diff --git a/step3/src/js/components/TodoHeader.js b/step3/src/js/components/TodoHeader.js new file mode 100644 index 0000000..1768535 --- /dev/null +++ b/step3/src/js/components/TodoHeader.js @@ -0,0 +1,9 @@ +import Component from '../core/Component.js'; + +export default class TodoHeader extends Component { + template() { + return ` +

TODOLIST

+ `; + } +} diff --git a/step3/src/js/components/TodoInput.js b/step3/src/js/components/TodoInput.js new file mode 100644 index 0000000..d88f12b --- /dev/null +++ b/step3/src/js/components/TodoInput.js @@ -0,0 +1,26 @@ +import Component from '../core/Component.js'; +import addEvent from '../utils/addEvent.js'; + +export default class TodoInput extends Component { + template() { + return ` + + `; + } + + setEvent() { + const { onAdd } = this.props; + + addEvent('keydown', '.new-todo', this.$target, (event) => { + if (event.key !== 'Enter') return; + + const content = event.target.value; + + if (!content.trim()) return alert('Todo의 내용을 입력해주세요.'); + + onAdd(content); + + event.target.value = ''; + }); + } +} diff --git a/step3/src/js/components/TodoList.js b/step3/src/js/components/TodoList.js new file mode 100644 index 0000000..6ea3284 --- /dev/null +++ b/step3/src/js/components/TodoList.js @@ -0,0 +1,76 @@ +import Component from '../core/Component.js'; +import addEvent from '../utils/addEvent.js'; + +export default class TodoList extends Component { + template() { + const { todoItems, selectedItem } = this.props.data; + + return ``; + } + + setEvent() { + const { onoffEditMode, onEdit, onDelete, onToggle } = this.props; + + // 수정 버튼 클릭 + addEvent('click', '.modify', this.$target, (event) => { + const id = event.target.closest('li').dataset.id; + onoffEditMode(id); + }); + + // 수정 취소 버튼 클릭 + addEvent('click', '.modify-cancel', this.$target, () => { + onoffEditMode(-1); + }); + + // 수정 submit + addEvent('submit', `form[name='modify-form']`, this.$target, (event) => { + event.preventDefault(); + const content = event.target.querySelector('input').value; + if (!content.trim()) return alert('Todo의 내용을 입력해주세요.'); + const id = event.target.closest('li').dataset.id; + onEdit(id, content); + }); + + // 삭제 + addEvent('click', '.destroy', this.$target, (event) => { + const id = event.target.closest('li').dataset.id; + onDelete(id); + }); + + // 토글 + addEvent('click', '.toggle', this.$target, (event) => { + const id = event.target.closest('li').dataset.id; + onToggle(id); + }); + } +} diff --git a/step3/src/js/core/Component.js b/step3/src/js/core/Component.js new file mode 100644 index 0000000..a4eec4d --- /dev/null +++ b/step3/src/js/core/Component.js @@ -0,0 +1,36 @@ +import { observe, observable } from './observer.js'; + +export default class Component { + $target; // target element + state; + props; + + constructor($target, props) { + this.$target = $target; + this.props = props; + this.setup(); + this.setEvent(); + } + + setup() { + this.initState(); + observe(() => { + this.render(); + }); + } + + initState() {} + + template() { + return ''; + } + + render() { + this.$target.innerHTML = this.template(); + this.mounted(); + } + + setEvent() {} + + mounted() {} +} diff --git a/step3/src/js/core/observer.js b/step3/src/js/core/observer.js new file mode 100644 index 0000000..2c2986e --- /dev/null +++ b/step3/src/js/core/observer.js @@ -0,0 +1,29 @@ +let currentObserver = null; + +const observe = (fn) => { + currentObserver = fn; + fn(); + currentObserver = null; +}; + +const observable = (obj) => { + Object.keys(obj).forEach((key) => { + let _value = obj[key]; + const observers = new Set(); + + Object.defineProperty(obj, key, { + get() { + if (currentObserver) observers.add(currentObserver); + return _value; + }, + set(value) { + _value = value; + observers.forEach((fn) => fn()); + }, + }); + }); + + return obj; +}; + +export { observe, observable }; diff --git a/step3/src/js/index.js b/step3/src/js/index.js new file mode 100644 index 0000000..4e02f89 --- /dev/null +++ b/step3/src/js/index.js @@ -0,0 +1,3 @@ +import TodoApp from './TodoApp.js'; + +new TodoApp(document.querySelector('#app')); diff --git a/step3/src/js/utils/addEvent.js b/step3/src/js/utils/addEvent.js new file mode 100644 index 0000000..d19778e --- /dev/null +++ b/step3/src/js/utils/addEvent.js @@ -0,0 +1,11 @@ +const addEvent = (eventType, selector, $target, callback) => { + const children = [...$target.querySelectorAll(selector)]; + const isTarget = (target) => + children.includes(target) || target.closest(selector); + $target.addEventListener(eventType, (event) => { + if (!isTarget(event.target)) return false; + callback(event); + }); +}; + +export default addEvent; diff --git a/step3/src/js/utils/newGuid.js b/step3/src/js/utils/newGuid.js new file mode 100644 index 0000000..431cfe2 --- /dev/null +++ b/step3/src/js/utils/newGuid.js @@ -0,0 +1,51 @@ +export default function newGuid() { + var hexValues = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + ]; + + // c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx) + var oct = '', + tmp; + for (var a = 0; a < 4; a++) { + tmp = (4294967296 * Math.random()) | 0; + oct += + hexValues[tmp & 0xf] + + hexValues[(tmp >> 4) & 0xf] + + hexValues[(tmp >> 8) & 0xf] + + hexValues[(tmp >> 12) & 0xf] + + hexValues[(tmp >> 16) & 0xf] + + hexValues[(tmp >> 20) & 0xf] + + hexValues[(tmp >> 24) & 0xf] + + hexValues[(tmp >> 28) & 0xf]; + } + + // "Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively" + var clockSequenceHi = hexValues[(8 + Math.random() * 4) | 0]; + return ( + oct.substr(0, 8) + + '-' + + oct.substr(9, 4) + + '-4' + + oct.substr(13, 3) + + '-' + + clockSequenceHi + + oct.substr(16, 3) + + '-' + + oct.substr(19, 12) + ); +} From 8a1ccce7021695144c4f9918df14fe5aec37dd43 Mon Sep 17 00:00:00 2001 From: yurim Date: Sun, 7 Nov 2021 14:56:00 +0900 Subject: [PATCH 3/3] Add: step3.md --- step3/step3.md | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/step3/step3.md b/step3/step3.md index 9533230..beea9d0 100644 --- a/step3/step3.md +++ b/step3/step3.md @@ -1,9 +1,39 @@ # 요구사항 체크리스트 -- [ ] 이론 - - [ ] 옵저버 패턴에 대해 조사하기 - - [ ] Flux 패턴에 대해 조사하기 -- [ ] 실습 - - [ ] Class Component에 옵저버 패턴 적용하기 +- [x] 이론 + - [x] 옵저버 패턴에 대해 조사하기 + - [x] Flux 패턴에 대해 조사하기 +- [x] 실습 + - [x] Class Component에 옵저버 패턴 적용하기 -# 이번주 회고 +# Observer 패턴 + +Observer 패턴(옵저버 패턴)은 객체의 상태 변화를 관찰하는 **관찰자들(Observer)**을 객체에 등록해놓고 객체의 상태가 변경될 때마다 객체가 직접 각 Observer들에게 변경됐다는 것을 **알리는** 디자인 패턴이다. (데이터가 바뀌었을때 반영을 해주는 것) + +Observer 패턴을 다음과 같이 코드에 적용해볼 수 있다. + +- 객체 - Store 혹은 State +- Observer들 - Component들 + +예를 들어, Store나 State가 변경되면 그 Store를 사용하는 Component도 변경되어야 한다. Store가 변경됐다는 걸 Store를 사용하는 컴포넌트에게 알리는 코드를 작성해야한다는 것이다. + +- `observable` : 쉽게 말해 데이터(state)가 담긴다. +- `observe` : `observable`에 변화가 생기면 `observe`에 등록된 함수들(Observer들)이 실행된다. + +# Flux 패턴 + +![https://user-images.githubusercontent.com/33214449/139672751-d692667e-cd54-426c-8075-33fc8fb9cdb2.png](https://user-images.githubusercontent.com/33214449/139672751-d692667e-cd54-426c-8075-33fc8fb9cdb2.png) + +Flux 패턴의 특징은 **단방향으로 데이터가 흐른다**는 것이다. 이는 데이터 변화를 예측하기 쉽게 만든다. + +state를 바꿀 때는 dispatch, commit 등으로만 바뀔 수 있도록 하는 것이 포인트이다. + +[Redux의 Data Flow] + +![https://user-images.githubusercontent.com/33214449/139673384-91f6c1c5-d692-4b3d-9da7-91a088b5d96b.png](https://user-images.githubusercontent.com/33214449/139673384-91f6c1c5-d692-4b3d-9da7-91a088b5d96b.png) + +음식을 주문하는 상황을 예로 들면 + +- Action: 음식을 주문 (할 일을 명시) +- Dispatch: 재료창고(서버)에서 재료를 받아옴 +- Reducer: 받은 주문을 만드는 과정을 담당하는 요리사 역할