-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
471 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# 02. 렌더링 | ||
|
||
2장 렌더링의 섹션 03 ~ 05 부분을 구현하는 가이드입니다. | ||
|
||
구현에 앞서, 이전 회차에 작성했던 `view/counter.js`, `view/filters.js`, `view/todos.js` (혹은 추가로 작성한 코드들)을 가져와서 진행해주세요. | ||
|
||
## 테스트 | ||
|
||
``` | ||
npm test 02.\ 렌더링/05 | ||
``` | ||
|
||
또는 | ||
|
||
``` | ||
npm test | ||
``` | ||
|
||
## 구현 가이드 | ||
|
||
- [ ] `index.js` 에 일정 시간마다 상태를 무작위로 변경하고, 다시 렌더링을 하는 로직을 작성해보세요. | ||
|
||
- 무작위로 변경된 상태의 값은 자유롭되, 타입은 | ||
```ts | ||
{ | ||
todos: Array<{ text: string; completed: boolean }>; | ||
currentFilter: string; | ||
} | ||
``` | ||
을 지켜주세요. | ||
|
||
- [ ] `registry.js` 코드를 작성하고, 테스트를 통과해보세요. | ||
|
||
- 캡슐화를 위하여 `registry` 변수는 export 하지 말아주세요. | ||
|
||
- `index.html` 안에 렌더링 될 구성요소를 특정할 수 있도록 `data-component` 속성을 할당해보세요. | ||
|
||
- [ ] `isNodeChange.js` 코드를 작성하고, 테스트를 통과해보세요. | ||
|
||
- [ ] `applyDiff.js` 코드를 작성하고, 테스트를 통과해보세요. | ||
|
||
- `isNodeChange.js` 에 작성한 모듈을 활용해보세요. | ||
|
||
- [ ] 프로그램 내부에서 가상 DOM을 렌더링해보세요. | ||
|
||
- `registry.js` 의 `add` 함수를 호출하여 렌더링할 컴포넌트들을 미리 할당해주세요. | ||
|
||
- `registry.js` 의 `renderRoot` 함수를 호출하여 컴포넌트들을 가상 DOM에 렌더링하세요. | ||
|
||
- `applyDiff.js` 에 작성한 코드를 호출하여 가상 DOM을 렌더링하세요. | ||
|
||
## 덧붙임 | ||
|
||
- 로컬 환경에서 의도한대로 작동하는지 확인해보세요. | ||
- 코드 맥락만 맞다면 자유롭게 import / export 하셔도 됩니다. | ||
- 테스트 코드에 케이스는 추가 가능하나, 기존 테스트 코드를 변경하지는 말아주세요. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const applyDiff = () => {}; | ||
|
||
export default applyDiff; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import applyDiff from './applyDiff'; | ||
|
||
describe('applyDiff', () => { | ||
let realContainer; | ||
let virtualContainer; | ||
|
||
beforeEach(() => { | ||
realContainer = document.createElement('div'); | ||
virtualContainer = document.createElement('div'); | ||
realContainer.innerHTML = ` | ||
<div class="target"> | ||
<span class="maintained"> | ||
<div>maintain</div> | ||
</span> | ||
<div class="text">i am text</div> | ||
<ul> | ||
<li></li> | ||
</ul> | ||
</div> | ||
`; | ||
virtualContainer.innerHTML = ` | ||
<div class="target"> | ||
<a href="#" alt=""></a> | ||
<span class="removed"> | ||
<div class="inner">remove</div> | ||
</span> | ||
<div class="text">hello</div> | ||
<input type="text" /> | ||
</div> | ||
`; | ||
}); | ||
|
||
test('applyDiff가 삭제된 노드를 실제 DOM에 반영하여야 한다.', () => { | ||
applyDiff(realContainer, realContainer.children[0], virtualContainer.children[0]); | ||
|
||
expect(realContainer.querySelector('ul')).toBe(null); | ||
}); | ||
|
||
test('applyDiff가 추가된 노드를 실제 DOM에 반영하여야 한다.', () => { | ||
applyDiff(realContainer, realContainer.children[0], virtualContainer.children[0]); | ||
|
||
expect(realContainer.querySelector('a')).not.toBe(null); | ||
expect(realContainer.querySelector('input')).not.toBe(null); | ||
}); | ||
|
||
test('applyDiff가 기존 노드의 변경사항들을 실제 DOM에 반영하여야 한다.', () => { | ||
applyDiff(realContainer, realContainer.children[0], virtualContainer.children[0]); | ||
|
||
const span = realContainer.querySelector('span'); | ||
const textDiv = realContainer.querySelector('div.text'); | ||
|
||
expect(span.classList.contains('removed')).toBe(true); | ||
expect(span.classList.contains('maintained')).toBe(false); | ||
expect(textDiv.textContent).toBe('hello'); | ||
}); | ||
|
||
test('applyDiff의 결과로 나온 자식들의 개수가 정확해야한다.', () => { | ||
applyDiff(realContainer, realContainer.children[0], virtualContainer.children[0]); | ||
|
||
const targetNode = realContainer.querySelector('.target'); | ||
|
||
expect(targetNode.children.length).toBe(4); | ||
}); | ||
|
||
test('applyDiff가 DOM의 깊은 수준까지 변경사항을 반영해야한다.', () => { | ||
applyDiff(realContainer, realContainer.children[0], virtualContainer.children[0]); | ||
|
||
const innerNode = realContainer.querySelector('.inner'); | ||
|
||
expect(innerNode).not.toBe(null); | ||
expect(innerNode.textContent).toBe('remove'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
const { faker } = window; | ||
|
||
const createElement = () => ({ | ||
text: faker.random.words(2), | ||
completed: faker.random.boolean(), | ||
}); | ||
|
||
const repeat = (elementFactory, number) => { | ||
const array = []; | ||
for (let index = 0; index < number; index++) { | ||
array.push(elementFactory()); | ||
} | ||
return array; | ||
}; | ||
|
||
export default () => { | ||
const howMany = faker.random.number(10); | ||
return repeat(createElement, howMany); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<html> | ||
<head> | ||
<link rel="shortcut icon" href="../favicon.ico" /> | ||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/base.css" /> | ||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/index.css" /> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/faker.js"></script> | ||
<title>Frameworkless Frontend Development: Rendering</title> | ||
</head> | ||
|
||
<body> | ||
<section class="todoapp"> | ||
<header class="header"> | ||
<h1>todos</h1> | ||
<input class="new-todo" placeholder="What needs to be done?" autofocus /> | ||
</header> | ||
<section class="main"> | ||
<input id="toggle-all" class="toggle-all" type="checkbox" /> | ||
<label for="toggle-all">Mark all as complete</label> | ||
<ul class="todo-list"></ul> | ||
</section> | ||
<footer class="footer"> | ||
<span class="todo-count">1 Item Left</span> | ||
<ul class="filters"> | ||
<li> | ||
<a href="#/">All</a> | ||
</li> | ||
<li> | ||
<a href="#/active">Active</a> | ||
</li> | ||
<li> | ||
<a href="#/completed">Completed</a> | ||
</li> | ||
</ul> | ||
<button class="clear-completed">Clear completed</button> | ||
</footer> | ||
</section> | ||
<footer class="info"> | ||
<p>Double-click to edit a todo</p> | ||
<p>Created by <a href="http://twitter.com/thestrazz86">Francesco Strazzullo</a></p> | ||
<p>Thanks to <a href="http://todomvc.com">TodoMVC</a></p> | ||
</footer> | ||
<script type="module" src="index.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import getTodos from './getTodos.js'; | ||
import appView from './view/app.js'; | ||
|
||
const state = { | ||
todos: getTodos(), | ||
currentFilter: 'All', | ||
}; | ||
|
||
const main = document.querySelector('.todoapp'); | ||
|
||
window.requestAnimationFrame(() => { | ||
const newMain = appView(main, state); | ||
main.replaceWith(newMain); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const isNodeChanged = () => {}; | ||
|
||
export default isNodeChanged; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import isNodeChanged from './isNodeChanged'; | ||
|
||
describe('isNodeChanged', () => { | ||
test('두 노드의 어트리뷰트, 텍스트 컨텐츠가 동일하다면 false 반환', () => { | ||
const node1 = document.createElement('div'); | ||
node1.setAttribute('class', 'container'); | ||
node1.textContent = 'Hello, world!'; | ||
|
||
const node2 = document.createElement('div'); | ||
node2.setAttribute('class', 'container'); | ||
node2.textContent = 'Hello, world!'; | ||
|
||
expect(isNodeChanged(node1, node2)).toBe(false); | ||
}); | ||
|
||
test('두 노드가 다른 어트리뷰트를 가지면 true 반환', () => { | ||
const node1 = document.createElement('div'); | ||
node1.setAttribute('class', 'container'); | ||
node1.textContent = 'Hello, world!'; | ||
|
||
const node2 = document.createElement('div'); | ||
node2.setAttribute('class', 'wrapper'); | ||
node2.setAttribute('id', 'myDiv'); | ||
node2.textContent = 'Hello, world!'; | ||
|
||
expect(isNodeChanged(node1, node2)).toBe(true); | ||
}); | ||
|
||
test('두 노드가 같은 어트리뷰트를 갖지만 어트리뷰트의 값이 다르면 true 반환', () => { | ||
const node1 = document.createElement('div'); | ||
node1.setAttribute('class', 'container'); | ||
node1.textContent = 'Hello, world!'; | ||
|
||
const node2 = document.createElement('div'); | ||
node2.setAttribute('class', 'wrapper'); | ||
node2.textContent = 'Hello, world!'; | ||
|
||
expect(isNodeChanged(node1, node2)).toBe(true); | ||
}); | ||
|
||
test('두 노드가 다른 텍스트 컨텐츠를 가지면 true 반환', () => { | ||
const node1 = document.createElement('div'); | ||
node1.setAttribute('class', 'container'); | ||
node1.textContent = 'Hello, world!'; | ||
|
||
const node2 = document.createElement('div'); | ||
node2.setAttribute('class', 'container'); | ||
node2.textContent = 'Hello, GitHub Copilot!'; | ||
|
||
expect(isNodeChanged(node1, node2)).toBe(true); | ||
}); | ||
|
||
test('두 노드가 어트리뷰트, 텍스트 컨텐츠를 가지지 않으면 false 반환', () => { | ||
const node1 = document.createElement('div'); | ||
const node2 = document.createElement('div'); | ||
|
||
expect(isNodeChanged(node1, node2)).toBe(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
const registry = {}; // export 하지 말아주세요! | ||
|
||
const add = () => {}; | ||
const renderRoot = () => {}; | ||
|
||
export { add, renderRoot }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { add, renderRoot } from './registry'; | ||
|
||
describe('registry 테스트', () => { | ||
it('renderRoot를 이용하여 컴포넌트를 렌더링할 수 있어야한다.', () => { | ||
const dummyElement = (target, className, innerHTML = '') => { | ||
const element = target.cloneNode(true); | ||
element.className = className; | ||
element.innerHTML = innerHTML; | ||
return element; | ||
}; | ||
|
||
/* | ||
렌더링할 가상 컴포넌트를 정의한다. | ||
아래와 같은 DOM을 렌더링한다. | ||
<div> | ||
<div data-component="A" class="aa"></div> | ||
<span data-component="B" class="bbb"> | ||
<span class="test" /> | ||
</span> | ||
<ul data-component="C" class="cccccc"></ul> | ||
</div> | ||
*/ | ||
const virtualComponents = [ | ||
{ name: 'A', component: (target) => dummyElement(target, 'aa') }, | ||
{ | ||
name: 'B', | ||
component: (target) => dummyElement(target, 'bbb', '<span class="test" />'), | ||
}, | ||
{ name: 'C', component: (target) => dummyElement(target, 'cccccc') }, | ||
]; | ||
|
||
// 1. registry에 렌더링할 컴포넌트를 추가한다. | ||
virtualComponents.forEach(({ name, component }) => { | ||
add(name, component); | ||
}); | ||
|
||
// 2. data-component 속성이 추가된 DOM을 생성한다. | ||
const root = document.createElement('div'); | ||
root.innerHTML = ` | ||
<div data-component="A"></div> | ||
<span data-component="B"></span> | ||
<ul data-component="C"></ul> | ||
`; | ||
|
||
// 3. renderRoot를 이용하여 root를 렌더링한다. | ||
const resultRoot = renderRoot(root, {}); | ||
|
||
expect(resultRoot.querySelector('[data-component="A"]').className).toBe('aa'); | ||
expect(resultRoot.querySelector('[data-component="B"]').className).toBe('bbb'); | ||
expect(resultRoot.querySelector('[data-component="B"] > span.test')).not.toBe(null); | ||
expect(resultRoot.querySelector('[data-component="C"]').className).toBe('cccccc'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import counterView from './counter'; | ||
|
||
let targetElement; | ||
|
||
describe('counterView', () => { | ||
beforeEach(() => { | ||
targetElement = document.createElement('div'); | ||
}); | ||
|
||
test('새로운 DOM 요소는 완료되지 않은 todo의 수를 가지고 있어야 한다.', () => { | ||
const newCounter = counterView(targetElement, { | ||
todos: [ | ||
{ | ||
text: 'First', | ||
completed: true, | ||
}, | ||
{ | ||
text: 'Second', | ||
completed: false, | ||
}, | ||
{ | ||
text: 'Third', | ||
completed: false, | ||
}, | ||
], | ||
}); | ||
|
||
expect(newCounter.textContent).toBe('2 Items left'); | ||
}); | ||
|
||
test('완료하지 않은 todo가 1개일 경우를 고려해야 한다.', () => { | ||
const newCounter = counterView(targetElement, { | ||
todos: [ | ||
{ | ||
text: 'First', | ||
completed: true, | ||
}, | ||
{ | ||
text: 'Third', | ||
completed: false, | ||
}, | ||
], | ||
}); | ||
|
||
expect(newCounter.textContent).toBe('1 Item left'); | ||
}); | ||
}); |
Oops, something went wrong.