Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[step3] 3주차: 옵저버 패턴 적용 #7

Open
wants to merge 3 commits into
base: pul8219
Choose a base branch
from
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
13 changes: 13 additions & 0 deletions step3/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TodoList with Observer Pattern</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./js/index.js"></script>
</body>
</html>
93 changes: 93 additions & 0 deletions step3/src/js/TodoApp.js
Original file line number Diff line number Diff line change
@@ -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 `
<div class="header-container"></div>
<div class="input-container"></div>
<div class="list-container"></div>
`;
}

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;
}
}
9 changes: 9 additions & 0 deletions step3/src/js/components/TodoHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Component from '../core/Component.js';

export default class TodoHeader extends Component {
template() {
return `
<h1>TODOLIST</h1>
`;
}
}
26 changes: 26 additions & 0 deletions step3/src/js/components/TodoInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Component from '../core/Component.js';
import addEvent from '../utils/addEvent.js';

export default class TodoInput extends Component {
template() {
return `
<input class="new-todo" placeholder="오늘의 할 일" autofocus />
`;
}

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 = '';
});
}
}
76 changes: 76 additions & 0 deletions step3/src/js/components/TodoList.js
Original file line number Diff line number Diff line change
@@ -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 `<ul class="todo-list">${todoItems
.map((todoItem) => {
if (selectedItem === todoItem.id) {
return `
<li class="editing" data-id=${todoItem.id}>
<div class="view">
<form name="modify-form" method="post">
<input class="modify-todo" value='${todoItem.content}' />
<button type="submit">완료</button>
<button class="modify-cancel" type="button">취소</button>
</form>
</div>
</li>
`;
}
return `
<li data-id=${todoItem.id} ${
todoItem.isComplete ? `class="completed"` : ''
}>
<div class="view">
<input class="toggle" type="checkbox" ${
todoItem.isComplete ? 'checked' : ''
}>
<label>${todoItem.content}</label>
<button class="modify" type="button">수정</button>
<button class="destroy" type="button">삭제</button>
</div>
</li>
`;
})
.join('')}</ul>`;
}

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);
});
}
}
36 changes: 36 additions & 0 deletions step3/src/js/core/Component.js
Original file line number Diff line number Diff line change
@@ -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() {}
}
29 changes: 29 additions & 0 deletions step3/src/js/core/observer.js
Original file line number Diff line number Diff line change
@@ -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 };
3 changes: 3 additions & 0 deletions step3/src/js/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import TodoApp from './TodoApp.js';

new TodoApp(document.querySelector('#app'));
11 changes: 11 additions & 0 deletions step3/src/js/utils/addEvent.js
Original file line number Diff line number Diff line change
@@ -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;
51 changes: 51 additions & 0 deletions step3/src/js/utils/newGuid.js
Original file line number Diff line number Diff line change
@@ -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)
);
}
39 changes: 39 additions & 0 deletions step3/step3.md
Original file line number Diff line number Diff line change
@@ -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: 받은 주문을 만드는 과정을 담당하는 요리사 역할