본문 바로가기
카테고리 없음

FE 디자인 패턴_(1)

by 1two13 2024. 8. 4.
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
반응형

댓글