728x90
반응형
목차
디자인 패턴이란?
- 소프트웨어를 개발하는 과정의 반복되는 일반적인 문제들에 대해 기준이 되는 해결책 제공
- 반복되는 문제 상황들을 최적화된 방법으로 해결하도록 돕는 컨셉
Singleton 패턴
- 앱 전체에서 공유 및 사용되는 단일 인스턴스
- 즉, 싱글톤 패턴은 인스턴스를 1번만 만들 수 있어야 한다.
클래스로 예시를 작성해보면 변수를 생성하여 생성자에서 변수가 생성된 인스턴스를 가리키도록 하면 된다.
let instance
let counter = 0
class Counter {
constructor() {
if (instance) {
throw new Error('You can only create one instance!')
}
instance = this
}
getInstance() {
return this
}
getCount() {
return counter
}
increment() {
return ++counter
}
decrement() {
return --counter
}
}
const singletonCounter = Object.freeze(new Counter())
export default singletonCounter
장점
- 메모리 공간 절약
단점
- JS에서는 객체 리터럴을 사용해 동일한 구현을 할 수 있기에 굳이..?
- 모든 테스트는 이전 테스트에서 만들어진 전역 인스턴스를 수정해야하기 때문에 테스트의 어려움
React의 상태 관리
- Redux, Context를 사용
- 싱글톤은 인스턴스의 값을 직접 수정할 수 있지만, 위의 도구는 읽기 전용 상태를 제거
Svelte의 상태 관리
- 내장된 store 사용
Proxy 패턴
- 대상 객체에 대하여 읽기 및 쓰기 직접 제어
- 대상 객체를 직접 다루지 않고 proxy 객체와 인터렉션
const person = {
name: 'John Doe',
age: 42,
nationality: 'American',
}
const personProxy = new Proxy(person, {
get: (obj, prop) => {
console.log(`The value of ${prop} is ${obj[prop]}`)
},
set: (obj, prop, value) => {
console.log(`Changed ${prop} from ${obj[prop]} to ${value}`)
obj[prop] = value
},
})
장점
- 유효성 검사, 포메팅, 알림, 디버깅 구현 시 유용
- JS에서 제공되는 빌트인 객체 중 하나인 Reflect를 사용하면 대상 객체 쉽게 조작 가능
- 객체의 동작 커스터마이징 가능 (다양한 메서드 존재) MDN 참고
const personProxy = new Proxy(person, {
get: (obj, prop) => {
console.log(Reflect.get(obj, prop))
},
set: (obj, prop, value) => {
console.log(Reflect.set(obj, prop, value)) // boolean
},
})
단점
- 2번째 인자로 전달하는 핸들러 객체를 너무 헤비하게 사용하면 앱 성능에 좋지 않음
Provider 패턴
- 여러 자식 컴포넌트 간에 데이터 공유
- 각 레이어에 직접 데이터를 주지 않고, 컴포넌트가 직접 데이터에 접근하는 방식
장점
- data를 필요로 하지 않는 컴포넌트는 prop을 받지 않음
- props drilling 제거
- 보통 UI 테마를 여러 컴포넌트가 공유하기 위해 사용
- styled-components를 사용하는 경우 제공되는 ThemeProvider 사용하여 해당 Provider 값에 접근 가능
단점
- 컨텍스트를 참조하는 모든 컴포넌트는 컨텍스트 변경시 모두 리렌더링되기 때문에 성능 이슈 발생
React의 상태 관리
- 모든 컴포넌트를 Provider(고차함수(HOC)로 Context 객체 제공)로 감싸고 createContext 메서드를 사용하여 Context 객체 생성
- 각 컴포넌트는 useContext 훅을 사용하여 data에 접근 및 함수 호출 가능
export const ThemeContext = React.createContext()
const themes = {
light: {
background: '#fff',
color: '#000',
},
dark: {
background: '#171717',
color: '#fff',
},
}
export default function App() {
const [theme, setTheme] = useState('dark')
function toggleTheme() {
setTheme(theme === 'light' ? 'dark' : 'light')
}
const providerValue = {
theme: themes[theme],
toggleTheme,
}
return (
<div className={`App theme-${theme}`}>
<ThemeContext.Provider value={providerValue}>
<Toggle />
<List />
</ThemeContext.Provider>
</div>
)
}
// Toggle 컴포넌트
import React, { useContext } from 'react'
import { ThemeContext } from './App'
export default function Toggle() {
const theme = useContext(ThemeContext)
return (
<label className="switch">
<input type="checkbox" onClick={theme.toggleTheme} />
<span className="slider round" />
</label>
)
}
Svelte의 상태 관리
- 부모 컴포넌트에서 setContext를 사용하여 값을 설정하고, 자식 컴포넌트에서 getContext를 사용하여 값을 가져오기
<script>
import { setContext } from 'svelte';
const value = { user: 'John Doe' };
setContext('userContext', value);
</script>
<Child />
<script>
import { getContext } from 'svelte';
const { user } = getContext('userContext');
</script>
<p>User: {user}</p>
- writable 스토어 사용하기
import { writable } from 'svelte/store';
export const userStore = writable('John Doe');
<script>
import { userStore } from './store.js';
$userStore = 'Jane Doe';
</script>
<Child />
<script>
import { userStore } from './store.js';
let user;
$: user = $userStore;
</script>
<p>User: {user}</p>
Prototype 패턴
- 동일 타입의 여러 객체들이 프로퍼티 공유
- JS 객체의 기본 속성인 Prototype 사용 (prototype chain 가능)
모든 프로퍼티는 클래스 자체에 선언되고 prototype 또는 __proto__를 사용하여 prototype 객체 확인이 가능하다.
class Dog {
constructor(name) {
this.name = name
}
bark() {
return `Woof!`
}
}
const dog1 = new Dog('Daisy')
console.log(Dog.prototype) // constructor: ƒ Dog(name, breed) bark: ƒ bark()
console.log(dog1.__proto__) // constructor: ƒ Dog(name, breed) bark: ƒ bark()
Object.create 메서드를 사용해서 프로토타입으로 쓰일 객체를 인자로 받아 새로운 객체를 생성할 수 있다.
const dog = {
bark() {
return `Woof!`
},
}
const pet1 = Object.create(dog)
장점
- 메서드 중복을 줄일 수 있음
- 인스턴스를 만든 뒤에도 prototype에 프로퍼티 추가 가능
Dog.prototype.play
단점
- 프로토타입 체인이 깊어지면 코드 추적, 디버깅 어려움
- 프로토타입을 변경하면 모든 인스턴스에 영향을 줌
Container/Presentational 패턴
- 비즈니스 로직으로부터 뷰를 분리하여 관심사 분리 강제
- Presentational Components: 뷰 로직
- Container Components: 비즈니스 로직
장점
- Presentational 컴포넌트는 데이터 변경 없이 화면에 출력하기 때문에 재사용 가능
- Presentational 컴포넌트는 테스트 용이 (일반적으로 순수함수로 구현되기 때문에 요구하는 데이터만 인자로 넘겨 테스트)
단점
- 너무 작은 규모의 앱에서는 오버엔지니어링
React의 상태 관리
- 비즈니스 로직을 커스텀 훅으로 생성하여 사용
export default function useDogImages() {
const [dogs, setDogs] = useState([])
useEffect(() => {
fetch('https://dog.ceo/api/breed/labrador/images/random/6')
.then(res => res.json())
.then(({ message }) => setDogs(message))
}, [])
return dogs
}
svelte의 상태 관리
- svelte에서는 .svelte 파일 하나에 HTML, CSS, JS가 모두 포함되는 구조이기 때문에 관심사 분리를 React처럼 하는 것은 오히려 비효율적이라는 생각이 든다.
Observer 패턴
- Observer를 활용해 Observer에게 이벤트 발생을 알림
- 이벤트가 발생할 때마다 Observable은 모든 Observer에게 이벤트 전파
- Observer: 구독하는 주체(받은 데이터 처리)
- Observable: 구독 가능한 주체(이벤트 모니터링)
- observable 객체의 주요 특징
- observers: 이벤트가 발생할 때마다 전파할 observer들의 배열
- subscribe(): Observer를 Observer 배열에 추가
- unsubscribe(): Observer 배열에서 Observer 제거
- notify(): 등록된 모든 Observer들에게 이벤트 전파
class Observable {
constructor() {
this.observers = []
}
subscribe(func) {
this.observers.push(func)
}
unsubscribe(func) {
this.observers = this.observers.filter(observer => observer !== func)
}
notify(data) {
this.observers.forEach(observer => observer(data))
}
}
RxJS 오픈소스 라이브러리를 사용하면 Observable과 Observer를 만들 수 있다.
import React from "react";
import ReactDOM from "react-dom";
import { fromEvent, merge } from "rxjs";
import { sample, mapTo } from "rxjs/operators";
import "./styles.css";
merge(
fromEvent(document, "mousedown").pipe(mapTo(false)),
fromEvent(document, "mousemove").pipe(mapTo(true))
)
.pipe(sample(fromEvent(document, "mouseup")))
.subscribe(isDragging => {
console.log("Were you dragging?", isDragging);
});
ReactDOM.render(
<div className="App">Click or drag anywhere and check the console!</div>,
document.getElementById("root")
);
장점
- 비동기 호출, 이벤트 기반 데이터 처리 시 유용
- Observer 객체는 Observable 객체와 언제든지 분리 가능
단점
- Observer가 복잡해지면 모든 Observer들에 알림 전파 시 성능 이슈 발생
Module 패턴
- 코드를 재사용 가능하면서도 작게 나누기
- import, export하여 재사용
- Dynamic import를 사용하여 특정 조건에서 특정 모듈을 로드할 수 있다.
import('module').then(module => {
module.default()
module.namedExport()
})
// Or with async/await
(async () => {
const module = await import('module')
module.default()
module.namedExport()
})()
장점
- 코드의 일부분 캡슐화 가능
- 의도치 않은 전역 변수 할당 예방
Mixin 패턴
- 상속 없이 객체나 클래스에 기능 추가
- 단독으로 사용 불가
class Dog {
constructor(name) {
this.name = name
}
}
const dogFunctionality = {
bark: () => console.log('Woof!'),
wagTail: () => console.log('Wagging my tail!'),
play: () => console.log('Playing!'),
}
Object.assign(Dog.prototype, dogFunctionality)
단점
- 복잡도 증가
- 재사용 어려움
React의 상태 관리
- 훅으로 대체됨
Svelte의 상태 관리
- 믹스인 객체를 가지고 와 컴포넌트의 메소드로 추가하여 필요할 때마다 재사용
// src/dogFunctionality.js
export const dogFunctionality = {
bark() {
console.log('Woof!');
},
wagTail() {
console.log('Wagging my tail!');
},
play() {
console.log('Playing!');
},
};
<!-- src/DogComponent.svelte -->
<script>
import { dogFunctionality } from './dogFunctionality.js';
// 믹스인 기능을 현재 컴포넌트의 메소드로 추가
Object.assign(this, dogFunctionality);
let name = 'Buddy';
onMount(() => {
console.log(`${name} is ready!`);
});
</script>
<h1>{name}</h1>
<button on:click={() => bark()}>Bark</button>
<button on:click={() => wagTail()}>Wag Tail</button>
<button on:click={() => play()}>Play</button>
Command 패턴
- 특정 작업을 실행하는 개체와 메서드를 호출하는 개체 분리
- class 함수 내에서 모든 메서드를 구현하지 않고, execute라는 하나의 메서드만 가지도록 하기
class OrderManager {
constructor() {
this.orders = []
}
execute(command, ...args) {
return command.execute(this.orders, ...args)
}
}
class Command {
constructor(execute) {
this.execute = execute
}
}
function PlaceOrderCommand(order, id) {
return new Command(orders => {
orders.push(id)
return `You have successfully ordered ${order} (${id})`
})
}
function CancelOrderCommand(id) {
return new Command(orders => {
orders = orders.filter(order => order.id !== id)
return `You have canceled your order ${id}`
})
}
function TrackOrderCommand(id) {
return new Command(() => `Your order ${id} will arrive in 20 minutes.`)
}
=> OrderManager가 메서드를 직접 갖지 않고, execute라는 메서드를 통해 분리된 함수를 사용
장점
- 객체와 메서드 분리
- (수명이 지정된 명령, 명령을 큐에 담아 특정한 시간대에 처리 가능)
단점
- 커맨드 패턴을 쓸만한 상황이 많지 않아 불필요한 코드 생성되는 이슈
참고자료
728x90
반응형
댓글