개요

학부 시절 만들고자 하는 서비스는 많았는데 나의 서비스 개발 기술은 없는 수준이었다.
언어 기본기 공부, 프레임워크 공부를 최우선으로 생각해서, 솔직히 디자인패턴은 눈에 잘 안 들어왔다.

개발자로 2년을 근무하다 오랜만에 디자인패턴 글을 읽어보니 상당히 익숙해서 놀라웠다.
현업에서 자주 보았던 패턴이었으며, 나 역시도 무의식적으로 비슷하게 사용한 경우가 많았다.

방향성

  1. 프론트엔드 개발에서 자주 사용하는 JS + 취미에 있어 관심있는 메이플스토리의 조합으로 글을 새로 쓸 계획
  2. 외국 개발자의 저널을 https://dev.to/twinfred/design-patterns-in-javascript-1l2l 참고

내가 생각하는 디자인패턴이란

워낙에 추상적인 개념이다보니, 정답을 단정짓기는 그렇고 내 생각만 적어보자면,
디자인패턴의 주 목적은 결국 효율성을 최대화하기 위함이다. (의사소통, 개발 속도, 프로덕트 퀄리티, 유지보수 용이성)

  • ex) 변수 이름에도 camelCase, PascalCase, snake_case, pothole_case, kebab-case를 채택하면 여러 사람 사이에 직관적으로 표준이 생김

  • ex) 워크맨을 보다 알았는데 편의점에서 물건이 정면을 보도록, 가격표가 물건 왼쪽에 위치하도록 정렬하는 작업을 페이스업이라고 지칭한다.
    Video Label

    “누구님 우유 코너 물건이 정면을 보도록, 가격표가 물건 왼쪽에 위치하도록 정렬해주세요” 이 타령을 하는 것보다
    “누구님 우유 코너 페이스업 해주세요” 가 효율적임

  • 이처럼 변수 표기법이나 편의점 물건 진열에도 사용하는데 몇 만줄이 되는 프로그램에서는 당연히 필요로 하는 것

Constructor(생성자), Builder(빌더) 패턴

JS에는 인스턴스를 쉽게 생성할 수 있는 도구(생성자함수, class)가 있다.

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
class Character {
constructor(name, level, classes, guild) {
this.name = name;
this.level = level;
this.classes = classes;
this.guild = guild;
}
}

class Guild {
constructor(name, master) {
this.name = name;
this.master = master;
}
}

// 캐릭터 2명 생성
// 생성과 동시에 길드를 가질 수는 없다
const hero_1 = new Character("히어로짱짱", 10, "hero", null);
const deven_1 = new Character("데벤짱짱", 10, "demon avenger", null);
console.log(hero_1, deven_1);

// 길드 생성
// 길드 생성에는 길드마스터가 반드시 1명 필요하다
const iron = new Guild("iron", deven_1);
console.log(iron);

// 히어로짱짱이 길드에 가입
hero_1.guild = iron;
console.log(hero_1);

// 히어로짱짱이 길드 탈퇴
hero_1.guild = null;
console.log(hero_1);

Factory(팩토리) Pattern

Constructor(생성자), Builder(빌더) 패턴에서 보듯이
JS는 객체를 생성하기 위해 생성자함수, class를 사용한다.

이런 객체 생성 도구들을 한 곳에 모아놓고
[도구1, 도구2, 도구3, 도구4, …]
인풋을 받아 한 도구만을 사용해 하나의 객체를 return하는 것이 팩토리 패턴이다.

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
//각각의 캐릭터가 가지고 있는 스킬 수치는 투자한 만큼 높다
//3차 스킬 이하 생략
const mySkillSet = {
level_5: { frenzy: 30, blood_fist: 20, dimension_sword: 30 },
level_4: { execution: 30, shield_chasing: 29, armor_break: 20 },
};

// 생성자 함수나 class가 아닌 일반 함수이다.
// case에서 분기하여 객체를 상황에 맞게 반환한다.
function skillFactory({ skillSet }) {
this.getSkill = function ({ skill_level }) {
let skill;
switch (skill_level) {
case "level_5":
skill = new Skill5({ skillSet });
break;
case "level_4":
skill = new Skill4({ skillSet });
break;
}
return skill;
};
}

//생성자 함수는 모듈로 만들어 import하는 것이 깔끔함
const Skill5 = function ({ skillSet }) {
const { frenzy = 1, blood_fist = 1, dimension_sword = 1 } = skillSet.level_5;

this.frenzy = frenzy;
this.blood_fist = blood_fist;
this.dimension_sword = dimension_sword;

this.attack1 = () => {
console.log("frenzy", "level" + frenzy);
};
this.attack2 = () => {
console.log("blood_fist", "level" + blood_fist);
};
this.attack3 = () => {
console.log("dimension_sword", "level" + dimension_sword);
};
};

//생성자 함수는 모듈로 만들어 import하는 것이 깔끔함
const Skill4 = function ({ skillSet }) {
const {
execution = 1,
shield_chasing = 1,
armor_break = 1,
} = skillSet.level_4;

this.execution = execution;
this.shield_chasing = shield_chasing;
this.armor_break = armor_break;

this.attack1 = () => {
console.log("execution", "level" + execution);
};
this.attack2 = () => {
console.log("shield_chasing", "level" + shield_chasing);
};
this.attack3 = () => {
console.log("armor_break", "level" + armor_break);
};
};

const factory = new skillFactory({ skillSet: mySkillSet });

const skill_level_5 = factory.getSkill({ skill_level: "level_5" });
skill_level_5.attack1();
skill_level_5.attack2();
skill_level_5.attack3();

const skill_level_4 = factory.getSkill({ skill_level: "level_4" });
skill_level_4.attack1();
skill_level_4.attack2();
skill_level_4.attack3();

Prototype(프로토타입) Pattern

객체마다 너무나도 동일한 특성을 띄고 있는 속성/메서드가 있다면,
그 속성/메서드는 굳이 각각의 객체마다 메모리를 잡고 있을 이유가 없다.
JS에서는 생성자 함수에 .prototype을 사용해 미래에 생성될 객체들의 공통 속성/메서드를 메모리 하나에 정의할 수 있고
object에도 .__proto__ 속성을 사용해 공통으로 속성을 받아올 부모 object를 가리킬 수 있다.
(참고로 JS에서 이렇게 object가 부모 참조를 시작하면 최종적으로 올라가는 곳은 Object 객체이다)

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
//생성자 패턴에서 사용한 캐릭터 class
class Character {
constructor(name, level, classes, guild) {
this.name = name;
this.level = level;
this.classes = classes;
this.guild = guild;
}
}

//공통으로 참조될 전사 캐릭터 객체
const warrior = {
classes_base: "warrior",
aura_weapon() {
console.log("aura weapon");
},
body_of_steel() {
console.log("body of steel");
},
};

// 생성자 패턴에서 생성한 두 캐릭터를 다시 가져옴
const hero_1 = new Character("히어로짱짱", 10, "hero", null);
const deven_1 = new Character("데벤짱짱", 10, "demon avenger", null);

//히어로에만 전사 프로토타입을 부여했다
hero_1.__proto__ = warrior;

console.log(hero_1.__proto__ === warrior);
console.log(deven_1.__proto__ === warrior);

warrior.name_kor = "전사";

console.log(hero_1.classes_base);
console.log(deven_1.classes_base);

Singleton(싱글톤) Pattern

실행 환경에서 어떤 생성자로부터 생성된 객체가 단 하나만 존재하는 패턴이다.
비유를 하자면 메이플 내 운영자 NPC는 업데이트 주기가 매우 길고, 사료를 주는 기간이 아니면 각각의 클릭 이벤트마다 서버와의 연동은 필요하지 않다.
운영자 인스턴스를 재클릭하면 기존에 존재하는 객체를 참조하여 반환한다.

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
//운영자 NPC 객체
const Master = (function () {
let instance;

function createInstance() {
return {
name: "운영자",
action1: "이벤트 1에 대한 설명",
action1: "이벤트 2에 대한 설명",
};
}

function getInstance() {
if (!instance) {
instance = createInstance();
}

return instance;
}

return getInstance;
})();

const instance1 = Master.getInstance();
const instance2 = Master.getInstance();

console.log(instance1 === instance2);