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)
+ );
+}
diff --git a/step3/step3.md b/step3/step3.md
new file mode 100644
index 0000000..beea9d0
--- /dev/null
+++ b/step3/step3.md
@@ -0,0 +1,39 @@
+# 요구사항 체크리스트
+
+- [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: 받은 주문을 만드는 과정을 담당하는 요리사 역할