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);
});
반응형