Skip to content

Commit

Permalink
Merge branch 'main' into Hanna
Browse files Browse the repository at this point in the history
  • Loading branch information
Hanna922 committed Jan 16, 2024
2 parents 317d927 + a789077 commit c6f402f
Show file tree
Hide file tree
Showing 14 changed files with 471 additions and 1 deletion.
5 changes: 4 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ module.exports = {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {},
rules: {
'import/extensions': 'off',
'no-plusplus': 'off',
},
};
56 changes: 56 additions & 0 deletions 02. 렌더링/05/README.md
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 하셔도 됩니다.
- 테스트 코드에 케이스는 추가 가능하나, 기존 테스트 코드를 변경하지는 말아주세요.
3 changes: 3 additions & 0 deletions 02. 렌더링/05/applyDiff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const applyDiff = () => {};

export default applyDiff;
73 changes: 73 additions & 0 deletions 02. 렌더링/05/applyDiff.test.js
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');
});
});
19 changes: 19 additions & 0 deletions 02. 렌더링/05/getTodos.js
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);
};
44 changes: 44 additions & 0 deletions 02. 렌더링/05/index.html
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>
14 changes: 14 additions & 0 deletions 02. 렌더링/05/index.js
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);
});
3 changes: 3 additions & 0 deletions 02. 렌더링/05/isNodeChanged.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const isNodeChanged = () => {};

export default isNodeChanged;
59 changes: 59 additions & 0 deletions 02. 렌더링/05/isNodeChanged.test.js
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);
});
});
6 changes: 6 additions & 0 deletions 02. 렌더링/05/registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const registry = {}; // export 하지 말아주세요!

const add = () => {};
const renderRoot = () => {};

export { add, renderRoot };
53 changes: 53 additions & 0 deletions 02. 렌더링/05/registry.test.js
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');
});
});
47 changes: 47 additions & 0 deletions 02. 렌더링/05/view/counter.test.js
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');
});
});
Loading

0 comments on commit c6f402f

Please sign in to comment.