0%

디자인 패턴

프로그램 개발에서 자주 발생하는 문제를 해결하기 위한 방법 중 하나로, 과거의 소프트웨어 개발 과정에서 발견된 설계의 노하우를 축적하여 이름을 붙여, 이후에 재사용하기 좋은 형태로 특정의 규약을 묶어서 정리한 해결책1

  • “바퀴를 다시 발명하지 마라(Don’t reinvent the wheel)”
  • = 이미 만들어져 잘 되는 것을 처음부터 다시 만들 필요가 없다는 뜻

패턴이란?

프로그램 개발에서 설계 문제와 이를 처리하는 해결책에도 공통점이 있는 데 이런 유사점을 패턴이라 한다.

디자인 패턴 종류

GoF(Gang of Fout)2가 23가지의 디자인 패턴을 정리하고 각각의 디자인 패턴을 생성(Creational), 구조(Structural), 행위(Behavioral) 3가지로 분류했다.

생성(Creational) 패턴

  • 추상 팩토리(Abstract Factory)
  • 빌더(Builder)
  • 팩토리 메서드(Factory Methods)
  • 프로토타입(Prototype)
  • 싱글턴(Singleton)

구조(Structural) 패턴

  • 어댑터(Adapter)
  • 브리지(Bridge)
  • 컴퍼지트(Composite)
  • 데커레이터(Decorator)
  • 퍼사드(Facade)
  • 플라이웨이트(Flyweight)
  • 프록시(Proxy)

행위(Behavioral) 패턴

  • 책임 연쇄(Chain of Responsibility)
  • 커맨드(Command)
  • 인터프리트(Interpreter)
  • 이터레이터(Iterator)
  • 미디에이터(Mediator)
  • 메멘토(Memento)
  • 옵저버(Observer)
  • 스테이트(State)
  • 스트래티지(Strategy)
  • 템플릿 메서드(Template Method)
  • 비지터(Visitor)

1: 디자인 패턴 정의 - 위키백과
2: 에리히 감마(Erich Gamma), 리차드 헬름(Richard Helm), 랄프 존슨(Ralph Johnsom), 존 블리시디스(John Vissides)를 말한다. 소프트웨어 개발 영역에서 디자인 패턴을 구체화하고 체계화한 사람들이다.

일정 데이터 관리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
let data = [
{
id: 1,
type: '2',
category: '2',
date: '2021-06-20',
content:
'지훈님께서 아침에 트라이(Trie) 가사 검색 문제에 대한 힌트를 잘 알려주셔서 문제를 이해하고, 푸는데 정말 큰 도움이 되었다.',
order: 1
},
{
id: 23,
type: '1',
category: '1',
date: '2021-06-23',
content: '자두 캘린더 마우스이벤트',
order: 2
}
...
]
```
위와 같은 배열을 사용했다.

## 고려했던 다른 데이터 방식
```javascript
let data = [
{
2020:{
},
2021:{
6:{
20:{
id: 1,
type: '2',
category: '2',
content:
'지훈님께서 아침에 트라이(Trie) 가사 검색 문제에 대한 힌트를 잘 알려주셔서 문제를 이해하고, 푸는데 정말 큰 도움이 되었다.',
order: 1
}
23:{
id: 23,
type: '1',
category: '1',
content: '자두 캘린더 마우스이벤트',
order: 2
}
}
}
}
]

달력이니 날짜를 기준으로 데이터를 관리하면 편할 거란 생각이 들어 위와 같은 데이터 관리 방식을 고민해봤었다.
하지만 이러면 데이터의 깊이가 너무 깊어지고 일저의 날짜가 바뀌면 옮기기도 어려워져 다른 방식을 택했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// v.0.1 app.js 1407줄
// 드래그 앤 드롭
$calendarDates.addEventListener('mousedown', e => {
const initialMousePos = {
x: e.clientX,
y: e.clientY
};

const $selectedMoveBtn = closest(e.target, 'item-move-btn', 'calendar-dates');

if (!$selectedMoveBtn) return;

const draggable = closest(e.target, 'item', 'calendar-dates');
draggable.classList.add('dragging');

e.target.style['pointer-events'] = 'none';
e.target.parentElement.style['pointer-events'] = 'none';
e.target.parentElement.parentElement.style['pointer-events'] = 'none';

[...document.querySelectorAll('.item-control-btn')].forEach(
$itemControlBtn => {
$itemControlBtn.classList.toggle('--invisible');
}
);

const eventFunctions = (() => {
let $container = null;
let $prevContainer = null;
let $nextElement = null;
let $prevNextElement = null;

return {
mouseMoveEvent(e) {
draggable.style.transform = `translate3d(${
e.clientX - initialMousePos.x
}px, ${e.clientY - initialMousePos.y}px, 0)`;
},
mouseOverEvent(e) {
if ($prevNextElement) $prevNextElement.style.border = 'none';
if ($prevContainer) $prevContainer.style.border = 'none';

$container = e.target.closest('.items');
if (!$container) return;

$nextElement = e.target.closest('li');

if ($nextElement) {
$nextElement.style['border-top'] = 'solid 5px gray';
$prevNextElement = $nextElement;
return;
}

const $lastElement = $container.lastElementChild;

// 이동할 위치에 아무 일정도 없는 경우
if (!$lastElement) {
$container.style['border-top'] = 'solid 5px gray';
$prevContainer = $container;
return;
}

// 이동할 위치에 현재 이동중인 자신의 일정이 있는데
// 그게 이동할 위치의 마지막이고 자신보다 위에 일정이 있는 경우
const $prevElement = draggable.previousElementSibling;
if ($lastElement === draggable && $prevElement) {
$prevElement.style['border-bottom'] = 'solid 5px gray';
$prevNextElement = $prevElement;
return;
}

// 이동할 위치에 현재 이동중인 자신의 일정만 있는 경우
if ($lastElement === draggable) {
$container.style['border-top'] = 'solid 5px gray';
$prevContainer = $container;
}

// 이동할 위치에 현재 이동중인 자신의 일정이 있는데
// 그게 이동할 위치의 마지막이 아니고 들어가려고 하는 위치는 마지막인 경우
if ($lastElement !== draggable) {
$lastElement.style['border-bottom'] = 'solid 5px gray';
$prevNextElement = $lastElement;
}
},
mouseUpEvent() {
draggable.classList.remove('dragging');
draggable.style.transform = 'none';

if ($prevNextElement) $prevNextElement.style.border = 'none';
if ($prevContainer) $prevContainer.style.border = 'none';

e.target.style['pointer-events'] = 'auto';
e.target.parentElement.style['pointer-events'] = 'auto';
e.target.parentElement.parentElement.style['pointer-events'] = 'auto';

[...document.querySelectorAll('.item-control-btn')].forEach(
$itemControlBtn => {
$itemControlBtn.classList.toggle('--invisible');
}
);

$calendarDates.removeEventListener(
'mousemove',
eventFunctions.mouseMoveEvent
);
$calendarDates.removeEventListener(
'mouseover',
eventFunctions.mouseOverEvent
);
document.removeEventListener('mouseup', eventFunctions.mouseUpEvent);

if (!$container) return;

if (!$nextElement) {
$container.appendChild(draggable);
const editItem = {
id: draggable.dataset.id,
date: $container.parentElement.dataset.date,
order:
$container.lastElementChild === draggable
? null
: data.filter(
item =>
item.category === currentCategory &&
item.date === $container.parentElement.dataset.date
).length + 1
};
modifyDataArray(editItem);

return;
}

$container.insertBefore(draggable, $nextElement);
[...$container.children].forEach(($li, idx) => {
data.find(item => item.id === +$li.dataset.id).order = idx + 1;
});
data.find(item => item.id === +draggable.dataset.id).date =
$container.parentElement.dataset.date;
}
};
})();

$calendarDates.addEventListener('mousemove', eventFunctions.mouseMoveEvent);
$calendarDates.addEventListener('mouseover', eventFunctions.mouseOverEvent);
document.addEventListener('mouseup', eventFunctions.mouseUpEvent);
});

Element.getBoundingClientRect() VS Element.closest()

두 메서드 중 Element.closest() 선택

Element.getBoundingClientRect()

What forces layout / reflow 문서를 보면 Element APIs > Getting box metrics > elem.getBoundingClientRect() 가 있음을 볼 수 있다.
즉, 이 메서드를 호출할 때마다 브라우저는 요소의 크기와 위치값을 최신 정보로 가져오기 위해 문서의 일부 혹은 전체를 다시 그리는 리플로우(reflow) 현상이 발생한다.
이 메서드를 일정을 드래그할 때마다 호출하게 되면 성능에 문제를 줄 수 있다.
요소의 크기와 위치값이 캐시될 수 있지만, JADOO 프로젝트의 경우, 일정이 새로 생성, 삭제, 이동되면서 getBoundingClientRect() 메서드로 계산할 요소의 크기와 위치값이 계속 변동되어 새로 계산될 확률이 높으므로 사용하지 않았다.
//(MutationObserver를 사용해 호출 수를 줄이는 방법도 있긴 하다.)

Element.closest()

레이아웃 계산이 필요없이 HTML의 부모자식관계를 이용하는 메서드를 사용했다.
해당 요소부터 시작해서 document까지 비교하게 되는데 조금이라도 덜 비교하기 위해 아래와 같은 커스텀 함수를 만들어 사용했다.

1
2
3
4
5
6
7
8
9
10
11
// closest 커스텀 함수
const closest = ($startElem, targetClass, endClass) => {
let elem = $startElem;
while (!elem.classList.contains(targetClass)) {
if (elem.classList.contains(endClass)) {
return null;
}
elem = elem.parentNode;
}
return elem;
};

closest()를 사용해서 발생하는 문제점

  • translatd3d동작원리

정리 이유

HTMLCollection 객체와 NodeList 객체를 배열로 변환해서 사용하는 이유를 기억하기 위해

결론

노드 객체의 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection 객체나 NodeList 객체 모두 배열로 변환하여 사용하는 것을 권장한다.
둘 모두 유사 배열 객체이면서 이터러블이므로 스프레드 문법이나 Array.from 메서드를 사용하면 간단히 배열로 변환할 수 있다.

HTMLCollection

  • DOM API가 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체
  • 언제나 노드 객체의 상태 변화를 실시간으로 반영하는 살아있는(live) 객체이다.
  • getElementsByTagName, getElementByClassName 메서드가 반환
  • 유사 배열 객체이면서 이터러블이다.
  • 따라서 HTMLCollection 객체를 배열로 변환하면 부작용을 발생시키지 않고, 유용한 배열의 고차함수들을 사용할 수 있다.

NodeList

  • DOM API가 여러 개의 결과값을 반환하기 위한 DOM 컬렉션 객체
  • 대부분의 경우 노드 객체의 상태 변화를 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-live 객체로 동작하지만 경우에 따라 live 객체로 동작할 때가 있다.
  • querySelectorAll 메서드가 반환하는 경우, non-live 객체로 동작한다.
  • Node.prototype.childNodes 노드 탐색 프로터티가 반환하는 경우,
  • NodeList 객체는 NodeList.prototype.forEach, item, entries, keys, values 등의 메서드를 상속받아 사용할 수 있다.

정리 이유

자바스크립트를 이용해 알고리즘 문제 풀이를 시작할 때 많이 쓰일 Math의 property와 method를 미리 mdn에서 조사하고 기억하기 위해서

Math

  • Math는 수학적인 상수와 함수를 위한 property와 method를 가지는 내장 객체이다. (함수 객체가 아니다.)
  • Math의 모든 property와 method는 정적이다.
    = Math.property로 참조, Math.method로 호출한다.
  • Math는 Number 자료형만 지원하며 BigInt와는 사용할 수 없다.

Property

Math.PI

원의 둘레와 지름의 비율. 약 3.14159

Math.E

오일러의 상수이며 자연로그의 밑. 약 2.718

Math.LN2

2의 자연 로그. 약 0.693

Math.LN10

10의 자연로그. 약 2.303

Math.LOG2E

밑이 2인 로그 E. 약 1.443

Math.SQRT1_2

1/2의 제곱근. 약 0.707

Math.SQRT2

2의 제곱근. 약 1.414


Method

Math.abs(x)

숫자 x의 절댓값을 반환

Math.ceil(x)

인수보다 크거나 같은 수 중에서 가장 작은 정수를 반환

Math.floor(x)

인수보다 작거나 같은 수중에서 가장 큰 정수를 반환

Math.max([x[, y[, …]]])

0개 이상의 인수 중에서 제일 큰 수를 반환

Math.min([x[, y[, …]]])

0개 이상의 인수 중에서 제일 작은 수를 반환

Math.pow(x,y)

x의 y 제곱을 반환

Math.random()

0과 1 사이의 난수를 반환

Math.round(x)

숫자에서 가장 가까운 정수를 반환

삼각함수

  • Math.acos(x)
  • Math.acosh(x)
  • Math.asin(x)
  • Math.asinh(x)
  • Math.atan(x)
  • Math.atanh(x)
  • Math.atan2(y,x)
  • Math.cos(x)
  • Math.cosh(x)

app.js 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fetchTodos = () => {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/todos');
xhr.send();

xhr.onload = () => {
if (xhr.status === 200) {
console.log(JSON.parse(xhr.response));
setTodos(JSON.parse(xhr.response));
} else {
console.error('Error', xhr.status, xhr.statusText);
}
};
};

server.js 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const express = require('express');
const app = express();
const PORT = 9900;

app.use(express.static('public'));

app.listen(PORT, () => {
console.log(`Server is listening on http://localhost:${PORT}`);
});

let todos = [
{ id: 3, content: 'JavaScript', completed: false },
{ id: 2, content: 'CSS', completed: true },
{ id: 1, content: 'HTML', completed: false }
];

// 1. FetchTodos
app.get('/todos', (req, res) => {
res.send(todos);
});

// 2. Add todo
app.post('/todos', (req, res) => {
todos = [req.body, ...todos];
res.send(todos);
});

1
2
3
const express = require('express');
const app = express();
const PORT = 9900;

1
2
// The app.use(express.static()) adds a middleware for serving static files to your Express app.
app.use(express.static('public'));

middleware(미들웨어)

Client로부터 request(요청)이 오고
그 request에 대한 response(응답)을 보내는
그 사이에 실행되는 함수이다.
(사진첨부 - 예정)

app.use()

app.use()는 middleware(미들웨어)를 application(app object instance)에 binding한다.

app.use() 안의 함수들은
모두 middleware(미들웨어)이며
request(요청)가 올때마다 이 middleware가 실행된 후,
Client에 response(응답)한다.

express.static(root, [options])

express.static()은 Express의 유일한 기본 제공 middleware(미들웨어) 함수이다.
이 함수는 serve-static을 기반으로 하며, Express application의 정적 자산을 제공한다.

root 인수는 정적 자산의 제공을 시작하는 위치인 root directory를 설정한다.

app.use(express.static(‘public’));

app(=Server)에 request(요청)이 올 때마다
express.static(‘public’)을 실행하여 root directory를 public으로 설정한다.

1
app.use(express.json()); // for parsing application/json

request(요청)에 넘어오는 payload(페이로드)를 json 형식으로 pharsing(파싱)한다.

1
2
3
4
// The app.listen() function is used to bind and listen the connections on the specified host and port.
app.listen(PORT, () => {
console.log(`Server is listening on http://localhost:${PORT}`);
});

app.listen()

app.listen()은 지정된 host 및 port의 connection을 bind(연결)하고 listen(수신 대기)한다.

1
2
3
4
5
6
// Stored data on a server
let todos = [
{ id: 3, content: 'JavaScript', completed: false },
{ id: 2, content: 'CSS', completed: true },
{ id: 1, content: 'HTML', completed: false }
];

서버에 저장된 데이터를 의미한다.

-1. FetchTodos

1
2
3
4
// The app.get() function define a route handler for GET requests to a given URL.
app.get('/todos', (req, res) => {
res.send(todos);
});

app.get(path, handler)

app.get(path, handler)는 주어진 URL로부터 GET request가 올 때 실행될 route handler를 정의한다.

app.get(‘/todos’, (req, res) => { res.send(todos); });

/todos에서 GET requset가 올 경우, response(res)로 todos 데이터를 보낸다.

send() 시,
send framework가 타입을 확인해서 문자열이면 그대로 body에 담아서 보내고,
그 외의 타입이라면 stringify()로 문자열로 바꿔서 보낸다.

-2. Add Todo

1
2
3
4
app.post('/todos', (req, res) => {
todos = [req.body, ...todos];
res.send(todos);
});

/todos에서 POST request가 올 경우, response(res)로 request(req).body를 payload(페이로드)로 보낸다.

오류 원인

IE 브라우저에서는 document.querySelectorAll()이 반환하는 NodeList 객체에 대해서는 forEach 메서드를 지원하지 않는다.

오류 해결

[…document.querySelectorAll()].forEach() 와 같이 NodeList 객체를 배열로 바꿔 배열의 forEach 메서드를 사용해야한다.

배열로 변환해야하는 더 중요한ㄴ 이유

HTMLCollection 객체와 NodeList 객체는 모두 배열로 변환하여 사용하는 것을 권장한다.