|
| 1 | +# 7. 자바스크립트 디자인패턴 (1/3) |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +### 생성패턴 |
| 6 | + |
| 7 | +* 생성자 패턴 |
| 8 | +* 모듈 패턴 |
| 9 | +* 노출 모듈 패턴 |
| 10 | +* 싱글톤 패턴 |
| 11 | +* 프로토타입 패턴 |
| 12 | +* 팩토리 패턴 |
| 13 | + |
| 14 | + |
| 15 | + |
| 16 | +### 7.2 생성자 패턴 |
| 17 | + |
| 18 | + |
| 19 | + |
| 20 | +#### 7.2.1 객체 생성 |
| 21 | + |
| 22 | +```js |
| 23 | +// <= es5 |
| 24 | + |
| 25 | +const newObject = {} |
| 26 | + |
| 27 | +Object.defineProperty(newObject, "name", { |
| 28 | + value: "Foo", |
| 29 | + writable: true, |
| 30 | + enumerable: true, |
| 31 | + configurable: true |
| 32 | +}) |
| 33 | + |
| 34 | +//이렇게도 할당 가능 |
| 35 | +newObject.name = "Bar" |
| 36 | +console.log(newObject) |
| 37 | +``` |
| 38 | + |
| 39 | + |
| 40 | + |
| 41 | +#### 7.2.2 생성자의 기본 특징 |
| 42 | + |
| 43 | +es6에서 도입된 class를 통해 객체를 생성하고 초기화할 수 있다. |
| 44 | + |
| 45 | +```js |
| 46 | +class Person { |
| 47 | + constructor(name) { |
| 48 | + this.name = name |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +const bun = new Person("FooBar") |
| 53 | +console.log(bun) |
| 54 | +``` |
| 55 | + |
| 56 | + |
| 57 | + |
| 58 | +### 7.3 모듈 패턴 |
| 59 | + |
| 60 | +모듈 패턴을 잘 알지 못하고 처음보았지만, es module을 native하게 지원하고 사용할 수 있는 환경에서 유용성이 큰가? 에 대해아직 물음표인 것 같다. |
| 61 | + |
| 62 | +es module은 그 자체로 모듈로서 동작하는데, 왜 모듈 래퍼를 하나 더 씌우는 건지 궁금하다. |
| 63 | + |
| 64 | + |
| 65 | + |
| 66 | +모듈 그 자체로의 장점은 단일 인스턴스를 보장한다는 점인 것 같다. 객체를 만들려면 singleton으로 몸 비틀기를 해야하는 Java에 비해 단일 인스턴스가 필요한 상황이면 모듈을 이용하는 것으로 충분해 보인다. |
| 67 | + |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | + |
| 72 | +### 7.3.4 WeakMap을 사용하는 최신 모듈 패턴 |
| 73 | + |
| 74 | +WeakMap을 통한 접근제한 필요할 때가 있을 것 같지만, class의 instance로 관리하는 것과 유사해보인다. |
| 75 | + |
| 76 | +1. 먼저, 모듈 변수를 이용한 패턴을 살펴보면, 두 가지 문제점을 볼 수 있다. (WeakMap ❌) |
| 77 | + |
| 78 | +* 모듈 내의 변수를 공유하게 된다. 즉, 인스턴스 간의 독립적인 상태를 갖지 못한다. |
| 79 | +* 동일 모듈에서 모듈 변수들에게 자유롭게 접근이 가능하다. |
| 80 | + |
| 81 | +```js |
| 82 | +let counter = 0; |
| 83 | +let action; |
| 84 | + |
| 85 | +class Countdown { |
| 86 | + constructor(initialCounter, initialAction) { |
| 87 | + counter = initialCounter; // 모듈 스코프의 counter 변경 |
| 88 | + action = initialAction; // 모듈 스코프의 action 변경 |
| 89 | + } |
| 90 | + |
| 91 | + dec() { |
| 92 | + if (counter < 1) return; |
| 93 | + |
| 94 | + counter--; // 모듈 스코프의 counter 감소 |
| 95 | + |
| 96 | + if (counter === 0) { |
| 97 | + action(); // 모듈 스코프의 action 호출 |
| 98 | + } |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | + |
| 104 | + |
| 105 | +2. 클래스 내의 멤버 변수로 선언할 경우. |
| 106 | + |
| 107 | +* 자바스크립트는 비공개 필드를 선언할 수 없었다. |
| 108 | + |
| 109 | +```js |
| 110 | +class Countdown { |
| 111 | + counter; |
| 112 | + action; |
| 113 | + |
| 114 | + constructor(counter, action) { |
| 115 | + this.counter = counter; |
| 116 | + this.action = action; |
| 117 | + } |
| 118 | + |
| 119 | + dec() { |
| 120 | + if (this.counter < 1) return; |
| 121 | + |
| 122 | + this.counter--; // 프라이빗 필드 값 감소 |
| 123 | + |
| 124 | + if (this.counter === 0) { |
| 125 | + this.action(); // 프라이빗 필드로 접근 |
| 126 | + } |
| 127 | + } |
| 128 | +} |
| 129 | +const c = new Countdown(2, () => console.log('DONE')); |
| 130 | +console.log(c.counter); //1 |
| 131 | +c.counter = -1; |
| 132 | +console.log(c.counter); // -1 |
| 133 | +``` |
| 134 | + |
| 135 | + |
| 136 | + |
| 137 | +es2019에 추가된 private class member(`#`)가 도입되었다. 사실 private class member는 트랜스파일링 시, weakMap으로 트랜스파일링 된다. |
| 138 | + |
| 139 | +```js |
| 140 | +class A { |
| 141 | + #privateFieldA = 1; |
| 142 | +} |
| 143 | + |
| 144 | +// 트랜스파일링 |
| 145 | +var _privateFieldA = /*#__PURE__*/new WeakMap(); |
| 146 | + |
| 147 | +class A { |
| 148 | + constructor() { |
| 149 | + _privateFieldA.set(this, { |
| 150 | + writable: true, |
| 151 | + value: 1 |
| 152 | + }); |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +``` |
| 157 | + |
| 158 | + |
| 159 | + |
| 160 | + |
| 161 | + |
| 162 | +#### 7.4 노출 모듈 패턴 |
| 163 | + |
| 164 | +export로 비공개/공개 요소를 지정하는 것보다 포인터 객체를 만드는 것이 어떤 이점이 있는지 잘 모르겠다.. |
| 165 | + |
| 166 | + |
| 167 | + |
| 168 | +#### 7.5 싱글톤 패턴 |
| 169 | + |
| 170 | +Java의 싱글톤 패턴을 자바스크립트에서 사용하면 문제가 있는지 의심해봐야한다. |
| 171 | + |
| 172 | + |
| 173 | + |
| 174 | +#### 7.7 팩토리 패턴 |
| 175 | + |
| 176 | +싱글 팩토리 패턴 |
| 177 | + |
| 178 | +```js |
| 179 | +function createVechicle(data) { |
| 180 | + switch(data.type) { |
| 181 | + case "car": |
| 182 | + return new Car({...}); |
| 183 | + case "truck": |
| 184 | + return new Truck({...}); |
| 185 | + default: |
| 186 | + throw new Error("일체하는 type이 없습니다.") |
| 187 | + } |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | + |
| 192 | + |
| 193 | +위의 싱글 팩토리 패턴은 srp, ocp 위반이다. 그래서 나온 것이 추상 팩토리 메서드 패턴이다. |
| 194 | + |
| 195 | +잘 동의가 안 되는게, 추상 팩토리를 만든다고 하더라도 진입점에서는 결국 분기처리 해주어야 하는 것이 아닌가? 결국 if,else 분기문의 위치만 달라진다고 느껴진다. |
| 196 | + |
| 197 | +그러면, 인터페이스 복잡도, 추상도만 올라가는 것 같은데.. 어떤 이점이 있는지 잘 모르겠다. 유용함을 깨달으면 수정해놓겠지. |
| 198 | + |
| 199 | + |
| 200 | + |
| 201 | +### 7.8 구조 패턴 |
| 202 | + |
| 203 | +구조 패턴은 클래스와 객체를 체계적으로 구성하는 방법에 관한 것 |
| 204 | + |
| 205 | + |
| 206 | + |
| 207 | +### 7.10 퍼사드 패턴 |
| 208 | + |
| 209 | +퍼사드란, 내부의 복잡한 로직을 외부에 편리한 높은 수준의 인터페이스를 제공하는 패턴. |
| 210 | + |
| 211 | +이름을 적절히 추상화해서 외부에 노출하는 것이 중요하는 것이 중요하다. |
| 212 | + |
| 213 | + |
| 214 | + |
| 215 | +### 7.10 믹스인 패턴 |
| 216 | + |
| 217 | +믹스인은 서브클래스가 쉽게 상속받아 기능을 재사용할 수 있도록 하는 클래스 |
| 218 | + |
| 219 | + |
| 220 | + |
| 221 | +### 7.11 서브 클래싱 패턴 |
| 222 | + |
| 223 | +부모 클래스 생성자를 호출(super())하는 것 |
| 224 | + |
| 225 | +```js |
| 226 | +class SuperHero extends Person { |
| 227 | + constructor(firstName, lastName, powers) { |
| 228 | + super(firstName, lastName) |
| 229 | + this.powers = powers; |
| 230 | + } |
| 231 | +} |
| 232 | +``` |
| 233 | + |
| 234 | + |
| 235 | + |
| 236 | +### 7.12 믹스인 |
| 237 | + |
| 238 | +자바스크립트에서 다중 상속을 지원하지 않기 때문에, mixin 패턴을 통해 상속을 흉내낼 수 있다. |
| 239 | + |
| 240 | +```js |
| 241 | +function LoggingMixin<T extends { new(...args: any[]): {} }>(Base: T) { |
| 242 | + return class extends Base { |
| 243 | + log(message: string) { |
| 244 | + console.log(`[LOG]: ${message}`); |
| 245 | + } |
| 246 | + }; |
| 247 | +} |
| 248 | +class Product { |
| 249 | + name: string; |
| 250 | + price: number; |
| 251 | + constructor(name: string, price: number) { |
| 252 | + this.name = name; |
| 253 | + this.price = price; |
| 254 | + } |
| 255 | +} |
| 256 | +// Mixins을 통해 기능 추가 |
| 257 | +const LoggingProduct = LoggingMixin(Product) |
| 258 | +const product1 = new LoggingProduct("Laptop", 1500) |
| 259 | +product1.log("Hello") |
| 260 | +``` |
| 261 | + |
| 262 | + |
| 263 | + |
| 264 | +React의 HOC와도 비슷해보인다. |
| 265 | + |
| 266 | +```js |
| 267 | +import React from "react"; |
| 268 | + |
| 269 | +function withLogging(WrappedComponent) { |
| 270 | + return class extends React.Component { |
| 271 | + log(message) { |
| 272 | + console.log(`[LOG]: ${message}`); |
| 273 | + } |
| 274 | + |
| 275 | + render() { |
| 276 | + // HOC가 props를 WrappedComponent에 전달 |
| 277 | + return <WrappedComponent {...this.props} log={this.log} />; |
| 278 | + } |
| 279 | + }; |
| 280 | +} |
| 281 | + |
| 282 | +// 기본 컴포넌트 |
| 283 | +function Product({ name, price, log }) { |
| 284 | + return ( |
| 285 | + <div> |
| 286 | + <h1>{name}</h1> |
| 287 | + <p>Price: {price}</p> |
| 288 | + <button onClick={() => log("Product clicked!")}>Log</button> |
| 289 | + </div> |
| 290 | + ); |
| 291 | +} |
| 292 | + |
| 293 | +// HOC를 통해 기능 추가 |
| 294 | +const LoggingProduct = withLogging(Product); |
| 295 | + |
| 296 | +``` |
| 297 | + |
| 298 | + |
| 299 | + |
| 300 | +HOC는 이러한 간단한 예시에서는 적절한 추상화를 제공하고, 문제를 우아하게 해결하는 것처럼 보인다. 하지만, 경험적으로 HOC가 많아질수록, 복잡성과 유지보수를 어렵게 만든다. |
| 301 | + |
| 302 | + |
| 303 | + |
| 304 | +### 7.13 데코레이터 패턴 |
| 305 | + |
| 306 | +객체에 동적으로 기능을 추가, 확장할 수 있는 디자인 패턴 |
| 307 | + |
| 308 | +상속이 컴파일 타임에 확정되는 데 비해, 데코레이터 패턴은 동적으로 추가할 수 있다. |
| 309 | + |
| 310 | + |
| 311 | + |
| 312 | +```js |
| 313 | +class Coffee { |
| 314 | + getCost() { |
| 315 | + return 5; // 기본 커피 가격 |
| 316 | + } |
| 317 | + |
| 318 | + getDescription() { |
| 319 | + return "Basic Coffee"; |
| 320 | + } |
| 321 | +} |
| 322 | + |
| 323 | + |
| 324 | +class MilkDecorator { |
| 325 | + constructor(coffee) { |
| 326 | + this.coffee = coffee; |
| 327 | + } |
| 328 | + |
| 329 | + getCost() { |
| 330 | + return this.coffee.getCost() + 2; // 우유 추가 가격 |
| 331 | + } |
| 332 | + |
| 333 | + getDescription() { |
| 334 | + return this.coffee.getDescription() + ", Milk"; |
| 335 | + } |
| 336 | +} |
| 337 | + |
| 338 | + |
| 339 | +//개발자의 인자부하를 올리지는 않을까? 어떤 클래스의 데코레이터인지 파악하기 어려울 것 같다는 생각도 든다 |
| 340 | +const coffee = new Coffee(); |
| 341 | +const milkCoffee = new MilkDecorator(coffee); |
| 342 | +``` |
| 343 | + |
| 344 | + |
| 345 | + |
| 346 | + |
| 347 | + |
| 348 | + |
| 349 | + |
| 350 | +### 7.14 의사 클래스 데코레이터 |
| 351 | + |
| 352 | +`Interface.ensureImplements`까지 하는 것은 투머치.. 타입스크립트를 사용하자 |
| 353 | + |
| 354 | + |
| 355 | + |
| 356 | + |
| 357 | + |
| 358 | + |
| 359 | + |
| 360 | + |
| 361 | + |
| 362 | + |
| 363 | + |
| 364 | + |
| 365 | + |
| 366 | + |
| 367 | + |
0 commit comments