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

[Step 4 실습] 옵저버 패턴 학습 (전진우) #29

Open
wants to merge 3 commits into
base: Jun-Jinu
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
12 changes: 12 additions & 0 deletions week4/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
<title>4주차 JavaScript 실습</title>
</head>
<body>
<div id="app"></div>
<script src="./src/App.js" type="module"></script>
</body>
</html>
135 changes: 135 additions & 0 deletions week4/src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import Component from "./Component.js";

import ItemAppender from "./components/ItemAppender.js";
import ItemsView from "./components/ItemsView.js";

import { observable } from "./Observer.js";

class App extends Component {
init() {
this.state = observable({
todoItems: [
{ name: "코딩하기", done: false, updateState: false },
{ name: "밥먹기", done: true, updateState: false },
{ name: "양치하기", done: false, updateState: false },
],
});
}

template() {
return `
<h1>4주차 미션 - 옵저버 </h1>

<div id="item-appender"></div>
<div id="items-view"></div>
`;
}

mount() {
//투두리스트 state를 불러옴
const { todoItems } = this.state;
const $itemAppender = this.$component.querySelector("#item-appender");
const $itemsView = this.$component.querySelector("#items-view");

// state를 props로 전달
new ItemAppender($itemAppender);
new ItemsView($itemsView, { todoItems });
}

setEvents() {
this.appendTodoItem();
this.deleteTodoItem();
this.toggleTodoItem();
this.updateTodoItem();
}

appendTodoItem() {
const { todoItems } = this.state;
const appendBtn = this.$component.querySelector("#append-btn");

if (appendBtn) {
appendBtn.addEventListener("click", () => {
const newTodo =
this.$component.querySelector("#append-input").value;

this.setState({
todoItems: [
...todoItems,
observable({
name: newTodo,
done: false,
updateState: false,
}),
],
});
});
}
}

deleteTodoItem() {
const { todoItems } = this.state;
const $itemsView = this.$component.querySelector("#items-view");

$itemsView.addEventListener("click", (event) => {
if (event.target.id === "delete-btn") {
const todoIndex = parseInt(
event.target.closest("li").getAttribute("data-id")
);
console.log(todoIndex);

const deletedTodoItems = todoItems.filter(
(item, index) => index !== todoIndex
);
this.setState({ todoItems: deletedTodoItems });
}
});
}

toggleTodoItem() {
const { todoItems } = this.state;
const $itemsView = this.$component.querySelector("#items-view");

$itemsView.addEventListener("change", (event) => {
if (event.target.id === "toggle-btn") {
const todoIndex = parseInt(
event.target.closest("li").getAttribute("data-id")
);
const toggledTodoItems = [...todoItems];
toggledTodoItems[todoIndex] = {
...toggledTodoItems[todoIndex],
done: !toggledTodoItems[todoIndex].done,
};
this.setState({ todoItems: toggledTodoItems });
}
});
}

updateTodoItem() {
const { todoItems } = this.state;
const $itemsView = this.$component.querySelector("#items-view");

$itemsView.addEventListener("click", (event) => {
if (event.target.id === "update-btn") {
const todoIndex = parseInt(
event.target.closest("li").getAttribute("data-id")
);

const updatedTodoItems = [...todoItems];
updatedTodoItems[todoIndex].updateState =
!updatedTodoItems[todoIndex].updateState;

if (!updatedTodoItems[todoIndex].updateState) {
const $itemsTitle = this.$component.querySelector(
`#title-${todoIndex}`
);

updatedTodoItems[todoIndex].name = $itemsTitle.value;
}

this.setState({ todoItems: updatedTodoItems });
}
});
}
}

new App(document.querySelector("#app"));
54 changes: 54 additions & 0 deletions week4/src/Component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { observable, observe } from "./Observer.js";

export default class Component {
$component;
$props;
state;

constructor($component, $props) {
this.$component = $component;
this.$props = $props;
this.init();
this.updateState();
this.render();
Comment on lines +12 to +13
Copy link
Member

Choose a reason for hiding this comment

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

updateState에서도 render를 실행하고 있기 때문에, 컴포넌트가 생성되는 시점에 render 함수가 총 두 번 실행될 것 같아요!

}

template() {
return "<div></div>";
}

mount() {
/* 하위 컴포넌트 마운트 */
}

render() {
this.$component.innerHTML = this.template();
this.mount();
this.setEvents();
}

setState(newState) {
this.state = { ...this.state, ...newState };
this.render();
}

init() {
//state 초기화
// this.state = observable({
// todoItems: [
// { name: "코딩하기", done: false, updateState: false },
// { name: "밥먹기", done: true, updateState: false },
// { name: "양치하기", done: false, updateState: false },
// ],
// });
}

setEvents() {}

updateState() {
observe(() => {
this.render();
console.log("렌더링");
});
}
}
66 changes: 66 additions & 0 deletions week4/src/Observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
function debounce(func) {
let frameId;
return function () {
if (frameId) {
cancelAnimationFrame(frameId);
}
frameId = requestAnimationFrame(func);
};
}
Comment on lines +1 to +9
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
function debounce(func) {
let frameId;
return function () {
if (frameId) {
cancelAnimationFrame(frameId);
}
frameId = requestAnimationFrame(func);
};
}
function debounce(func) {
let frameId;
return () => {
cancelAnimationFrame(frameId);
frameId = requestAnimationFrame(func);
};
}

바로 cancel 해버려도 되지 않을까요!?
이름은 debounceOneFrame 처럼 조금 더 명시적으로 지어주면 좋을 것 같아요!


let currentObserver = null;

export const observe = (func) => {
currentObserver = debounce(func);
func();
currentObserver = null;
};

export const observable = (obj) => {
const observers = new Map();

return new Proxy(obj, {
get(target, key) {
const value = target[key];

if (value !== null && typeof value === "object")
return observable(value);
Comment on lines +26 to +27
Copy link
Member

Choose a reason for hiding this comment

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

오... 객체면 다시 한 번 씌워주는군요 ㅎㅎ 좋습니다 👏👏👏


if (currentObserver && typeof observers !== "undefined") {
Copy link
Member

Choose a reason for hiding this comment

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

observers가 undefined일 수가 있나요?

if (!observers.has(key)) observers.set(key, new Set());

observers.get(key).add(currentObserver);
}

return value;
},
set(target, key, value) {
target[key] = value;

if (observers.has(key))
observers.get(key).forEach((observer) => observer());

return true;
},
});
};

// const state = observable({
// todoItems: [
// { name: "코딩하기", done: false, updateState: false },
// { name: "밥먹기", done: true, updateState: false },
// { name: "양치하기", done: false, updateState: false },
// ],
// });

// observe(() =>
// console.log(state.todoItems[0].name + " 로그가 실행이 됐습니다.")
// );

// state.todoItems[0].name = "todo";
// state.todoItems[0].name = "todo1";
// state.todoItems[0].name = "todo2";

// requestAnimationFrame(() => {
// state.todoItems[0].name = "todo3";
// });
12 changes: 12 additions & 0 deletions week4/src/components/ItemAppender.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Component from "../Component.js";

export default class ItemAppender extends Component {
template() {
return `
<div class="input-container">
<input type="text" placeholder="새로운 할 일을 입력해주세요" id="append-input" class="append-input"/>
<button class="btn" id="append-btn">추가</button>
</div>
`;
}
}
Comment on lines +3 to +12
Copy link
Member

Choose a reason for hiding this comment

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

앞선 내용과 동일합니다!
state를 사용하고 있지도 않고,
props를 사용하고 있지도 않기 때문에 그냥 함수로 만들어서 사용해도 무방할 것 같아요~

41 changes: 41 additions & 0 deletions week4/src/components/ItemsView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Component from "../Component.js";

import { observe } from "../Observer.js";

export default class ItemsView extends Component {
updateState() {
observe(() => {
console.log("ItemsView 컴포넌트에서 옵저버...");
console.log(this.$props);
});
Comment on lines +7 to +10
Copy link
Member

Choose a reason for hiding this comment

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

  • observable을 통해서 만든 객체가 observe내에서 사용 되고,
  • observable의 값이 변경 되면 observe를 실행한다

이런 흐름인데 지금은 observe에 observable로 씌워진 친구가 없어서, updateState는 최초에 1회만 실행되겠네요!
다만 컴포넌트가 생성되는 시점에 updateState를 실행하기 때문에, 이 컴포넌트의 상위 컴포넌트가 렌더링되는 경우에만 다시 생성되지 않을까 싶어요.

}

template() {
const { todoItems } = this.$props;

return `
<ul>
${todoItems
.map(
({ done, name, updateState }, index) => `
<li data-id="${index}">
<input type="checkbox" ${
done ? "checked" : ""
} id="toggle-btn" ${updateState ? "class='updated'" : ""}/>
<input type="text" ${
done ? "class='todo checked'" : "class='todo'"
} id="title-${index}" value="${name}" ${
updateState ? "" : "readOnly"
} />
<button class="btn" id="update-btn">${
updateState ? "완료" : "수정"
}</button>
<button class="btn deleteBtn" id="delete-btn">삭제</button>
</li>`
)
.join("")}
</ul>

`;
}
Comment on lines +13 to +40
Copy link
Member

Choose a reason for hiding this comment

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

곰곰이 생각해보면, ItemsViews 컴포넌트는 클래스로 만들 필요 자체가 없는거죠..!
단순하게 함수로 표현해도 되지 않을까요?

}
Loading