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

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
    반응형

    댓글