diff --git a/week4/index.html b/week4/index.html
new file mode 100644
index 0000000..fb91e1e
--- /dev/null
+++ b/week4/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 4주차 JavaScript 실습
+
+
+
+
+
+
\ No newline at end of file
diff --git a/week4/src/App.js b/week4/src/App.js
new file mode 100644
index 0000000..5be2e10
--- /dev/null
+++ b/week4/src/App.js
@@ -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 `
+ 4주차 미션 - 옵저버
+
+
+
+ `;
+ }
+
+ 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"));
diff --git a/week4/src/Component.js b/week4/src/Component.js
new file mode 100644
index 0000000..de77efe
--- /dev/null
+++ b/week4/src/Component.js
@@ -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();
+ }
+
+ template() {
+ return "";
+ }
+
+ 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("렌더링");
+ });
+ }
+}
diff --git a/week4/src/Observer.js b/week4/src/Observer.js
new file mode 100644
index 0000000..a916a5e
--- /dev/null
+++ b/week4/src/Observer.js
@@ -0,0 +1,66 @@
+function debounce(func) {
+ let frameId;
+ return function () {
+ if (frameId) {
+ cancelAnimationFrame(frameId);
+ }
+ frameId = requestAnimationFrame(func);
+ };
+}
+
+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);
+
+ if (currentObserver && typeof 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";
+// });
diff --git a/week4/src/components/ItemAppender.js b/week4/src/components/ItemAppender.js
new file mode 100644
index 0000000..14e5e00
--- /dev/null
+++ b/week4/src/components/ItemAppender.js
@@ -0,0 +1,12 @@
+import Component from "../Component.js";
+
+export default class ItemAppender extends Component {
+ template() {
+ return `
+
+
+
+
+ `;
+ }
+}
diff --git a/week4/src/components/ItemsView.js b/week4/src/components/ItemsView.js
new file mode 100644
index 0000000..e53319f
--- /dev/null
+++ b/week4/src/components/ItemsView.js
@@ -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);
+ });
+ }
+
+ template() {
+ const { todoItems } = this.$props;
+
+ return `
+
+
+ `;
+ }
+}
diff --git a/week4/style.css b/week4/style.css
new file mode 100644
index 0000000..88125e8
--- /dev/null
+++ b/week4/style.css
@@ -0,0 +1,73 @@
+body{
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background-color: #c9c9c9;
+
+ padding: 0;
+ margin: 0;
+}
+
+ul{
+ list-style:none;
+ padding-left: 0;
+}
+
+li{
+ margin: 10px 0;
+}
+
+#app{
+ width: 60vw;
+ height: 80vh;
+ padding-top: 5vh;
+ margin-top: 5vh;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background-color: white;
+ color: #292929;
+
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+
+.append-input{
+ width: 200px;
+ height: 24px;
+ padding: 3px 10px;
+
+ border-radius: 3px;
+ border: 1px solid #c3c3c3;
+}
+
+.btn{
+ width: 50px;
+ height: 32px;
+ border: none;
+ background-color: #c3c3c366;
+ cursor: pointer;
+}
+
+.todo{
+ width: 150px;
+ height: 24px;
+ margin: 3px;
+
+ cursor: auto;
+
+ border: none;
+ text-align: center;
+}
+
+input:read-write{
+ border: 1px solid #c3c3c3;
+ text-align: start;
+}
+
+.checked{
+ color: #a6a6a6;
+ text-decoration: line-through;
+}
\ No newline at end of file