Personal-Study/Design Patterns

[TS Design Patterns] 생성 패턴 - 싱글톤

Aaron-Kim 2023. 10. 21. 21:27

Singleton

- 클래스에 인스턴스가 하나만 있도록 하면서 이 인스턴스에 대한 전역 접근 지점을 제공하는 생성 패턴

- 싱글톤 인스턴스를 가져오기 위한 공개된 정적 생성 메서드 선언

- 같은 종류의 객체가 하나만 존재하도록 하고 다른 코드의 해당 객체에 대한 단일 접근 지점 제공

 

- 앱 전체에서 공유 및 사용되는 단일 인스턴스

- 앱의 전역 상태를 관리하기 적합

- 인스턴스를 하나만 만들도록 강제하면 꽤 많은 메모리 공간 절약 가능

- 하지만 JS에서 안티패턴으로 간주됨

  - Java, C++와 다르게 JS에서는 클래스를 작성하지 않더라도 쉽게 객체 만들 수 있음

- 테스트 코드 작성할 때도 까다로움

- React에서 전역 상태 관리를 위해 Singleton 객체를 만드는 것 대신 Redux나 Context API 자주 사용

 

- 예시

/**
 * The Singleton class defines the `getInstance` method that lets clients access
 * the unique singleton instance.
 */
class Singleton {
    private static instance: Singleton;

    /**
     * The Singleton's constructor should always be private to prevent direct
     * construction calls with the `new` operator.
     */
    private constructor() { }

    /**
     * The static method that controls the access to the singleton instance.
     *
     * This implementation let you subclass the Singleton class while keeping
     * just one instance of each subclass around.
     */
    public static getInstance(): Singleton {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }

        return Singleton.instance;
    }

    /**
     * Finally, any singleton should define some business logic, which can be
     * executed on its instance.
     */
    public someBusinessLogic() {
        // ...
    }
}

/**
 * The client code.
 */
function clientCode() {
    const s1 = Singleton.getInstance();
    const s2 = Singleton.getInstance();

    if (s1 === s2) {
        console.log('Singleton works, both variables contain the same instance.');
    } else {
        console.log('Singleton failed, variables contain different instances.');
    }
}

clientCode();

 

// UI Component Design Patterns - Singleton

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

 

// pagres-org/react-design-pattern
// singleton-sample.tsx

import { useState } from 'react';
import singletonHOC from './singleton-hoc';
import Counter from './counter';

const SingletonCounter = singletonHOC(Counter);

export const SingletonSample = () => {
  const [mounted1, setMounted1] = useState(false);
  const [mounted2, setMounted2] = useState(false);
  const [mounted3, setMounted3] = useState(false);

  return (
    <div>
      <button onClick={() => setMounted1((mounted) => !mounted)}>
        {mounted1 ? 'Unmount' : 'Mount'}
      </button>
      <button onClick={() => setMounted2((mounted) => !mounted)}>
        {mounted2 ? 'Unmount' : 'Mount'}
      </button>
      <button onClick={() => setMounted3((mounted) => !mounted)}>
        {mounted3 ? 'Unmount' : 'Mount'}
      </button>

      {mounted1 && <SingletonCounter />}
      {mounted2 && <SingletonCounter />}
      {mounted3 && <SingletonCounter />}
    </div>
  );
};

 

// pagres-org/react-design-pattern
// counter.tsx

import { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  return (
    <div
      onClick={() => setCount((prevCount) => prevCount + 1)}
      style={{ fontSize: '32px', userSelect: 'none' }}
    >
      {count}
    </div>
  );
};

export default Counter;

 

// pagres-org/react-design-pattern
// singleton-hoc.tsx

import { useEffect, createElement } from 'react';
import ReactDOM, { Root } from 'react-dom/client';

type WrapperFunc = {
  (props: Record<PropertyKey, unknown>): null;
  refCount: number;
  container?: HTMLDivElement;
};

const singletonHOC = (component: Parameters<typeof createElement>[0]) => {
  const Wrapper: WrapperFunc = (props: Record<PropertyKey, unknown>) => {
    let $parentDOM: Root | null = null;

    useEffect(() => {
      if (Wrapper.refCount === 0) {
        Wrapper.container = document.createElement('div');
        document.body.appendChild(Wrapper.container);

        const reactElement = createElement(component, props);

        // eslint-disable-next-line react-hooks/exhaustive-deps
        $parentDOM = ReactDOM.createRoot(Wrapper.container);
        $parentDOM.render(reactElement);
      }

      Wrapper.refCount++;

      console.log(`Mounted singleton instance, ref count is ${Wrapper.refCount}.`);

      return () => {
        if (Wrapper.refCount === 1 && Wrapper.container) {
          $parentDOM?.unmount();
          document.body.removeChild(Wrapper.container);
        }

        Wrapper.refCount--;

        console.log(`Unmounted singleton instance, ref count is ${Wrapper.refCount}.`);
      };
    }, []);

    return null;
  };

  Wrapper.refCount = 0;

  return Wrapper;
};

export default singletonHOC;

 

- 활용

  - Nest.js entry point

// main.ts

async function bootstrap() {
  const PORT = process.env.PORT;
  const logger = new Logger('Application');
  const IS_DEV_MODE = process.env.NODE_ENV === 'development';

  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  if (!process.env.JWT_SECRET_KEY) {
    logger.error('Set "SECRET" env');
  }

  app.enableCors({
    origin: true,
    credentials: true,
  });
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
    }),
  );

  app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
  app.useGlobalFilters(new HttpApiExceptionFilter());

  await app.listen(PORT);

  if (IS_DEV_MODE) {
    logger.log(`✅ Server on http://localhost:${PORT}`);
  } else {
    logger.log(`✅ Server on port ${PORT}...`);
  }
}

bootstrap().catch((error) => {
  new Logger('init').error(error);
});

[아티클]

[TS 사용 예시]

[Pagers-org/react-design-pattern]

[UI Component] Design Patterns - Singleton

반응형