2025.03.12 - [Programming Language/React] - [React] Design Pattern in React (4) - Proxy
이전 글에 이어,
이번엔 싱글톤(Singleton) 패턴의 자주 쓰이는 용례들을 적어보고자 한다.
싱글톤 패턴(Singleton)
싱글톤 패턴은 클래스의 인스턴스가 애플리케이션 전체에서 단 하나만 존재하도록 보장하는 디자인 패턴입니다. React와 Next.js에서는 전역 상태 관리, 서비스 인스턴스, 유틸리티 등을 구현할 때 유용하게 활용됩니다.
1. 기본 구조
JavaScript/TypeScript에서의 싱글톤 패턴기본 구현:
// 기본적인 싱글톤 패턴 구현
class Logger {
private static instance: Logger;
private logs: string[] = [];
private constructor() {
// private 생성자로 외부에서 new 키워드로 인스턴스 생성 방지
}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message: string): void {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ${message}`);
console.log(`[${timestamp}] ${message}`);
}
getLogs(): string[] {
return [...this.logs];
}
clearLogs(): void {
this.logs = [];
}
}
// 사용 예시
const logger = Logger.getInstance();
logger.log('Application started');
// 다른 모듈에서도 동일한 인스턴스 사용
const sameLogger = Logger.getInstance();
sameLogger.log('User logged in');
// 두 변수는 동일한 인스턴스를 참조
console.log(logger === sameLogger); // true
console.log(logger.getLogs()); // 두 로그 메시지 모두 포함
2. 전역 상태 관리 싱글톤
전역 상태 관리하는 싱글톤 패턴:
// app/lib/store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// 상태 타입 정의
interface AppState {
theme: 'light' | 'dark';
language: string;
user: {
id: string;
name: string;
email: string;
} | null;
// 액션
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (language: string) => void;
setUser: (user: AppState['user']) => void;
logout: () => void;
}
// 싱글톤 상태 저장소 생성
export const useAppStore = create<AppState>()(
persist(
(set) => ({
theme: 'light',
language: 'en',
user: null,
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
setUser: (user) => set({ user }),
logout: () => set({ user: null })
}),
{
name: 'app-storage',
partialize: (state) => ({
theme: state.theme,
language: state.language,
user: state.user
})
}
)
);
// 테마 훅 (선택적)
export function useTheme() {
return useAppStore((state) => ({
theme: state.theme,
setTheme: state.setTheme
}));
}
// 사용자 훅 (선택적)
export function useUser() {
return useAppStore((state) => ({
user: state.user,
setUser: state.setUser,
logout: state.logout
}));
}
// app/components/ThemeToggle.tsx
'use client';
import { useTheme } from '@/app/lib/store';
export default function ThemeToggle() {
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<button
onClick={toggleTheme}
className="p-2 rounded-full bg-gray-200 dark:bg-gray-800"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
// app/components/UserProfile.tsx
'use client';
import { useUser } from '@/app/lib/store';
export default function UserProfile() {
const { user, logout } = useUser();
if (!user) {
return <div>Please log in to view your profile.</div>;
}
return (
<div className="p-4 border rounded-md">
<h2 className="text-xl font-bold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
<button
onClick={logout}
className="mt-4 py-2 px-4 bg-red-500 text-white rounded-md hover:bg-red-600"
>
Log out
</button>
</div>
);
}
3. API 클라이언트 싱글톤
API 통신을 위한 싱글톤 클라이언트:
// app/lib/apiClient.ts
import { getSession } from 'next-auth/react';
// API 클라이언트 싱글톤
class ApiClient {
private static instance: ApiClient;
private baseUrl: string;
private constructor() {
this.baseUrl = process.env.NEXT_PUBLIC_API_URL || '/api';
}
public static getInstance(): ApiClient {
if (!ApiClient.instance) {
ApiClient.instance = new ApiClient();
}
return ApiClient.instance;
}
// 인증 헤더 가져오기
private async getAuthHeaders(): Promise<HeadersInit> {
const session = await getSession();
const headers: HeadersInit = {
'Content-Type': 'application/json'
};
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
return headers;
}
// GET 요청
async get<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const headers = await this.getAuthHeaders();
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'GET',
headers: {
...headers,
...options.headers
},
...options
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
// POST 요청
async post<T>(endpoint: string, data: any, options: RequestInit = {}): Promise<T> {
const headers = await this.getAuthHeaders();
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
...headers,
...options.headers
},
body: JSON.stringify(data),
...options
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
// PUT 요청
async put<T>(endpoint: string, data: any, options: RequestInit = {}): Promise<T> {
const headers = await this.getAuthHeaders();
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'PUT',
headers: {
...headers,
...options.headers
},
body: JSON.stringify(data),
...options
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
// DELETE 요청
async delete<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const headers = await this.getAuthHeaders();
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'DELETE',
headers: {
...headers,
...options.headers
},
...options
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
}
// 싱글톤 인스턴스 내보내기
export const apiClient = ApiClient.getInstance();
// 사용 예시
export async function fetchUsers() {
return apiClient.get<User[]>('/users');
}
export async function createUser(userData: Omit<User, 'id'>) {
return apiClient.post<User>('/users', userData);
}
// app/users/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { apiClient } from '@/app/lib/apiClient';
interface User {
id: string;
name: string;
email: string;
}
export default function UsersPage() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadUsers() {
try {
setLoading(true);
const data = await apiClient.get<User[]>('/users');
setUsers(data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load users');
} finally {
setLoading(false);
}
}
loadUsers();
}, []);
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">Users</h1>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{users.map(user => (
<div key={user.id} className="p-4 border rounded-lg shadow-sm">
<h2 className="text-xl font-semibold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
</div>
))}
</div>
</div>
);
}
4. 설정 관리 프록시
애플리케이션 설정을 관리하는 싱글톤:
// app/lib/config.ts
// 설정 타입 정의
interface AppConfig {
apiUrl: string;
maxUploadSize: number;
supportedLocales: string[];
features: {
darkMode: boolean;
analytics: boolean;
comments: boolean;
};
}
// 환경별 설정
const developmentConfig: AppConfig = {
apiUrl: 'http://localhost:3000/api',
maxUploadSize: 10 * 1024 * 1024, // 10MB
supportedLocales: ['en', 'fr', 'es', 'de'],
features: {
darkMode: true,
analytics: false,
comments: true
}
};
const productionConfig: AppConfig = {
apiUrl: 'https://api.example.com',
maxUploadSize: 5 * 1024 * 1024, // 5MB
supportedLocales: ['en', 'fr', 'es', 'de', 'ja', 'zh'],
features: {
darkMode: true,
analytics: true,
comments: true
}
};
const testConfig: AppConfig = {
apiUrl: 'http://localhost:3000/api',
maxUploadSize: 10 * 1024 * 1024, // 10MB
supportedLocales: ['en'],
features: {
darkMode: true,
analytics: false,
comments: false
}
};
// 설정 싱글톤 클래스
class ConfigManager {
private static instance: ConfigManager;
private config: AppConfig;
private constructor() {
// 환경에 따른 설정 선택
const env = process.env.NODE_ENV || 'development';
switch (env) {
case 'production':
this.config = productionConfig;
break;
case 'test':
this.config = testConfig;
break;
default:
this.config = developmentConfig;
}
// 환경 변수로 설정 덮어쓰기 (선택적)
if (process.env.NEXT_PUBLIC_API_URL) {
this.config.apiUrl = process.env.NEXT_PUBLIC_API_URL;
}
// 설정 불변성 보장
Object.freeze(this.config);
}
public static getInstance(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
// 전체 설정 가져오기
getConfig(): Readonly<AppConfig> {
return this.config;
}
// 특정 설정 가져오기
get<K extends keyof AppConfig>(key: K): AppConfig[K] {
return this.config[key];
}
// 기능 활성화 여부 확인
isFeatureEnabled(featureName: keyof AppConfig['features']): boolean {
return this.config.features[featureName];
}
}
// 싱글톤 인스턴스 내보내기
export const configManager = ConfigManager.getInstance();
// 편의 함수
export function getConfig() {
return configManager.getConfig();
}
export function isFeatureEnabled(featureName: keyof AppConfig['features']) {
return configManager.isFeatureEnabled(featureName);
}
// app/components/FeatureFlag.tsx
'use client';
import { ReactNode } from 'react';
import { isFeatureEnabled } from '@/app/lib/config';
interface FeatureFlagProps {
feature: 'darkMode' | 'analytics' | 'comments';
children: ReactNode;
fallback?: ReactNode;
}
export default function FeatureFlag({
feature,
children,
fallback = null
}: FeatureFlagProps) {
const enabled = isFeatureEnabled(feature);
return enabled ? <>{children}</> : <>{fallback}</>;
}
// app/components/Comments.tsx
import FeatureFlag from './FeatureFlag';
export default function Comments() {
return (
<FeatureFlag
feature="comments"
fallback={<div>Comments are currently disabled.</div>}
>
<div className="mt-8 space-y-4">
<h2 className="text-2xl font-bold">Comments</h2>
{/* 댓글 컴포넌트 내용 */}
<div className="p-4 border rounded">
<div className="flex items-center space-x-2">
<div className="w-10 h-10 bg-gray-200 rounded-full"></div>
<div>
<p className="font-semibold">User Name</p>
<p className="text-sm text-gray-500">2 hours ago</p>
</div>
</div>
<p className="mt-2">This is a comment!</p>
</div>
<form className="mt-4">
<textarea
className="w-full p-2 border rounded"
placeholder="Write a comment..."
rows={3}
></textarea>
<button
type="submit"
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Post Comment
</button>
</form>
</div>
</FeatureFlag>
);
}
5. 이벤트 버스 싱글톤
컴포넌트 간 통신을 위한 이벤트 버스 싱글톤:
'use client';
import { useRouter, usePathname } from 'next/navigation';
import { useEffect } from 'react';
// 권한 타입 정의
type Permission = 'read' | 'write' | 'admin';
// 권한 프록시 컴포넌트
interface AuthProxyProps {
requiredPermissions: Permission[];
children: React.ReactNode;
fallback?: React.ReactNode;
}
function AuthProxy({
requiredPermissions,
children,
fallback = <div>You don't have permission to access this page.</div>
}: AuthProxyProps) {
// 실제 애플리케이션에서는 상태 관리 시스템이나 컨텍스트에서 가져옴
const userPermissions: Permission[] = ['read']; // 예시
const isAuthenticated = true; // 예시
const router = useRouter();
const pathname = usePathname();
// 권한 확인
const hasPermission = requiredPermissions.every(
permission => userPermissions.includes(permission)
);
useEffect(() => {
// 인증되지 않은 경우 로그인 페이지로 리디렉션
if (!isAuthenticated) {
router.push(`/login?returnUrl=${encodeURIComponent(pathname)}`);
}
}, [isAuthenticated, router, pathname]);
if (!isAuthenticated) {
return <div>Checking authentication...</div>;
}
return hasPermission ? <>{children}</> : <>{fallback}</>;
}
// 사용 예시
function AdminPanel() {
return (
<AuthProxy requiredPermissions={['admin']}>
<div className="p-6 bg-white rounded-lg shadow-md">
<h1 className="text-2xl font-bold mb-4">Admin Panel</h1>
<p>Welcome to the admin panel. You can manage users and settings here.</p>
{/* 관리자 기능 */}
</div>
</AuthProxy>
);
}
function UserSettings() {
return (
<AuthProxy requiredPermissions={['write']}>
<div className="p-6 bg-white rounded-lg shadow-md">
<h1 className="text-2xl font-bold mb-4">User Settings</h1>
<p>You can update your profile and preferences here.</p>
{/* 사용자 설정 */}
</div>
</AuthProxy>
);
}
function Dashboard() {
return (
<AuthProxy requiredPermissions={['read']}>
<div className="p-6 bg-white rounded-lg shadow-md">
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
<p>Welcome to your dashboard. Here's an overview of your activity.</p>
{/* 대시보드 내용 */}
</div>
</AuthProxy>
);
}
// app/lib/eventBus.ts
type EventCallback = (...args: any[]) => void;
// 이벤트 버스 싱글톤
class EventBus {
private static instance: EventBus;
private events: Map<string, EventCallback[]>;
private constructor() {
this.events = new Map();
}
public static getInstance(): EventBus {
if (!EventBus.instance) {
EventBus.instance = new EventBus();
}
return EventBus.instance;
}
// 이벤트 구독
on(event: string, callback: EventCallback): () => void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
const callbacks = this.events.get(event)!;
callbacks.push(callback);
// 구독 취소 함수 반환
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
};
}
// 이벤트 발행
emit(event: string, ...args: any[]): void {
const callbacks = this.events.get(event) || [];
callbacks.forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
}
// 모든 이벤트 구독 취소
off(event: string): void {
this.events.delete(event);
}
// 이벤트 한 번만 구독
once(event: string, callback: EventCallback): () => void {
const unsubscribe = this.on(event, (...args) => {
unsubscribe();
callback(...args);
});
return unsubscribe;
}
}
// 싱글톤 인스턴스 내보내기
export const eventBus = EventBus.getInstance();
// React 훅
import { useEffect } from 'react';
export function useEventListener(event: string, callback: EventCallback) {
useEffect(() => {
const unsubscribe = eventBus.on(event, callback);
return unsubscribe;
}, [event, callback]);
}
export function useEventEmitter() {
return {
emit: eventBus.emit.bind(eventBus)
};
}
// app/components/NotificationTrigger.tsx
'use client';
import { eventBus } from '@/app/lib/eventBus';
export default function NotificationTrigger() {
const showSuccessNotification = () => {
eventBus.emit('notification', {
message: 'Operation completed successfully!',
type: 'success'
});
};
const showErrorNotification = () => {
eventBus.emit('notification', {
message: 'An error occurred. Please try again.',
type: 'error'
});
};
const showInfoNotification = () => {
eventBus.emit('notification', {
message: 'This is an informational message.',
type: 'info'
});
};
return (
<div className="space-x-2">
<button
onClick={showSuccessNotification}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
>
Success
</button>
<button
onClick={showErrorNotification}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Error
</button>
<button
onClick={showInfoNotification}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Info
</button>
</div>
);
}
Next.js에서의 활용
1. 서버 컴포넌트에서의 싱글톤
서버 컴포넌트에서 싱글톤 패턴 활용:
// app/lib/database.ts
import { PrismaClient } from '@prisma/client';
// 전역 변수로 PrismaClient 인스턴스 선언
let prisma: PrismaClient;
// 싱글톤 패턴으로 PrismaClient 인스턴스 관리
if (process.env.NODE_ENV === 'production') {
// 프로덕션 환경에서는 새 인스턴스 생성
prisma = new PrismaClient();
} else {
// 개발 환경에서는 전역 객체에 캐싱하여 핫 리로딩 시 연결 재사용
if (!(global as any).prisma) {
(global as any).prisma = new PrismaClient();
}
prisma = (global as any).prisma;
}
export default prisma;
// app/users/[id]/page.tsx
import prisma from '@/app/lib/database';
import { notFound } from 'next/navigation';
interface UserPageProps {
params: {
id: string;
};
}
export default async function UserPage({ params }: UserPageProps) {
// 데이터베이스에서 사용자 조회
const user = await prisma.user.findUnique({
where: { id: params.id },
include: {
posts: {
orderBy: { createdAt: 'desc' },
take: 5
}
}
});
if (!user) {
notFound();
}
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">{user.name}</h1>
<p className="text-gray-600 mb-4">{user.email}</p>
<h2 className="text-2xl font-semibold mt-8 mb-4">Recent Posts</h2>
{user.posts.length > 0 ? (
<div className="space-y-4">
{user.posts.map(post => (
<div key={post.id} className="p-4 border rounded-lg">
<h3 className="text-xl font-medium">{post.title}</h3>
<p className="text-gray-700 mt-2">{post.content}</p>
<p className="text-sm text-gray-500 mt-2">
{new Date(post.createdAt).toLocaleDateString()}
</p>
</div>
))}
</div>
) : (
<p>This user hasn't created any posts yet.</p>
)}
</div>
);
}
2. 서버 액션과 싱글톤 패턴
서버 액션에서 싱글톤 패턴 활용:
// app/lib/cache.ts
// 캐시 싱글톤
class CacheManager {
private static instance: CacheManager;
private cache: Map<string, { data: any; timestamp: number }>;
private defaultTTL: number;
private constructor() {
this.cache = new Map();
this.defaultTTL = 60 * 1000; // 기본 1분 캐시
}
public static getInstance(): CacheManager {
if (!CacheManager.instance) {
CacheManager.instance = new CacheManager();
}
return CacheManager.instance;
}
// 캐시에서 데이터 가져오기
get<T>(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
const now = Date.now();
if (now - entry.timestamp > this.defaultTTL) {
// 캐시 만료
this.cache.delete(key);
return null;
}
return entry.data as T;
}
// 캐시에 데이터 저장
set<T>(key: string, data: T, ttl: number = this.defaultTTL): void {
this.cache.set(key, {
data,
timestamp: Date.now() + ttl
});
}
// 캐시 무효화
invalidate(key: string): void {
this.cache.delete(key);
}
// 모든 캐시 무효화
invalidateAll(): void {
this.cache.clear();
}
}
// 싱글톤 인스턴스 내보내기
export const cacheManager = CacheManager.getInstance();
// app/actions/posts.ts
'use server';
import prisma from '@/app/lib/database';
import { cacheManager } from '@/app/lib/cache';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
// 게시물 스키마
const postSchema = z.object({
title: z.string().min(3, 'Title must be at least 3 characters'),
content: z.string().min(10, 'Content must be at least 10 characters')
});
// 캐시된 게시물 목록 가져오기
export async function getPosts(page = 1, limit = 10) {
const cacheKey = `posts-${page}-${limit}`;
// 캐시 확인
const cachedData = cacheManager.get(cacheKey);
if (cachedData) {
return cachedData;
}
// 캐시 없으면 데이터베이스에서 가져오기
const skip = (page - 1) * limit;
const posts = await prisma.post.findMany({
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
author: {
select: {
id: true,
name: true
}
}
}
});
const total = await prisma.post.count();
const result = {
posts,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
};
// 결과 캐싱 (5분)
cacheManager.set(cacheKey, result, 5 * 60 * 1000);
return result;
}
// 게시물 생성
export async function createPost(formData: FormData) {
try {
// 폼 데이터 파싱
const rawData = {
title: formData.get('title'),
content: formData.get('content')
};
// 유효성 검증
const validatedData = postSchema.parse(rawData);
// 현재 사용자 세션 가져오기 (실제 구현에 맞게 조정 필요)
const session = await getServerSession();
if (!session?.user?.id) {
throw new Error('Authentication required');
}
// 데이터베이스에 게시물 생성
const post = await prisma.post.create({
data: {
...validatedData,
authorId: session.user.id
}
});
// 캐시 무효화
cacheManager.invalidateAll();
// 경로 재검증
revalidatePath('/posts');
revalidatePath(`/posts/${post.id}`);
return { success: true, post };
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
errors: error.errors.map(e => ({
path: e.path.join('.'),
message: e.message
}))
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'An error occurred'
};
}
}
// 게시물 삭제
export async function deletePost(postId: string) {
try {
// 현재 사용자 세션 가져오기
const session = await getServerSession();
if (!session?.user?.id) {
throw new Error('Authentication required');
}
// 게시물 조회
const post = await prisma.post.findUnique({
where: { id: postId },
select: { authorId: true }
});
if (!post) {
throw new Error('Post not found');
}
// 권한 확인 (작성자 또는 관리자만 삭제 가능)
if (post.authorId !== session.user.id && session.user.role !== 'admin') {
throw new Error('Permission denied');
}
// 게시물 삭제
await prisma.post.delete({
where: { id: postId }
});
// 캐시 무효화
cacheManager.invalidateAll();
// 경로 재검증
revalidatePath('/posts');
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'An error occurred'
};
}
}
import { Suspense } from 'react';
import { getPosts } from '@/app/actions/posts';
import PostList from './PostList';
import CreatePostForm from './CreatePostForm';
interface PostsPageProps {
searchParams: {
page?: string;
limit?: string;
};
}
export default async function PostsPage({ searchParams }: PostsPageProps) {
const page = parseInt(searchParams.page || '1', 10);
const limit = parseInt(searchParams.limit || '10', 10);
// 서버에서 게시물 데이터 가져오기
const { posts, pagination } = await getPosts(page, limit);
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">Posts</h1>
<CreatePostForm />
<Suspense fallback={<div>Loading posts...</div>}>
<PostList
initialPosts={posts}
pagination={pagination}
/>
</Suspense>
</div>
);
}
장점
- 단일 인스턴스 보장: 애플리케이션 전체에서 단 하나의 인스턴스만 존재하므로 상태 일관성을 유지할 수 있음
- 전역 접근성: 애플리케이션의 어디서든 동일한 인스턴스에 접근할 수 있어 데이터 공유가 용이
- 리소스 효율성: 데이터베이스 연결, 캐시, 로거 등의 리소스를 효율적으로 관리할 수 있음
- 지연 초기화: 필요한 시점에 인스턴스를 생성하여 메모리를 효율적으로 사용할 수 있 음
- 상태 일관성: 여러 컴포넌트나 모듈에서 동일한 상태를 공유할 수 있어 일관된 데이터 관리 가능
- 설정 중앙화: 애플리케이션 설정을 중앙에서 관리하여 일관성을 유지할 수 있음
- 코드 간결성: 의존성 주입 없이도 필요한 서비스에 접근할 수 있어 코드가 간결해짐
단점
- 전역 상태: 전역 상태는 디버깅과 테스트를 어렵게 만들 수 있음
- 결합도 증가: 코드 여러 부분이 싱글톤에 의존하게 되어 결합도가 높아질 수 있음
- 테스트 어려움: 싱글톤은 단위 테스트를 어렵게 만들 수 있으며, 테스트 간 상태 격리가 어려울 수 있음
- 동시성 문제: 멀티스레드 환경에서 싱글톤 접근 시 동시성 문제가 발생할 수 있음
- 숨겨진 의존성: 의존성이 명시적으로 드러나지 않아 코드 이해가 어려울 수 있음
- 확장성 제한: 싱글톤은 상속이나 확장이 어려울 수 있으며, 인터페이스 변경이 어려울 수 있음
- 메모리 누수: 싱글톤이 참조하는 객체들이 제대로 해제되지 않으면 메모리 누수가 발생할 수 있음
어느 때에 쓰는게 적합할까
- 데이터베이스 연결 관리: 데이터베이스 연결은 비용이 많이 들기 때문에 싱글톤으로 관리하면 효율적
- 설정 관리: 애플리케이션 설정을 중앙에서 관리하고 전역적으로 접근해야 할 때 적합
- 캐싱 시스템: 메모리 캐시를 애플리케이션 전체에서 공유해야 할 때 싱글톤이 유용
- 로깅 시스템: 로그를 일관되게 관리하고 모든 모듈에서 접근해야 할 때 적합
- 전역 상태 관리: React 애플리케이션에서 전역 상태를 관리할 때 싱글톤 패턴을 활용할 수 있 음
- API 클라이언트: 서버와의 통신을 담당하는 API 클라이언트를 싱글톤으로 구현하면 인증 토큰 등을 효율적으로 관리할 수 있음
- 이벤트 버스: 컴포넌트 간 통신을 위한 이벤트 버스를 구현할 때 싱글톤이 유용
- 리소스 공유: 파일 시스템, 네트워크 소켓 등 공유 리소스에 대한 접근을 관리할 때 적합
- 서비스 레지스트리: 다양한 서비스를 등록하고 관리하는 레지스트리를 구현할 때 싱글톤이 유용
- 환경 정보 관리: 애플리케이션의 실행 환경 정보를 관리하고 제공할 때 적합
React를 사용하면 기본적으로 다루게 되는 흔한 패턴이지만,
용례를 적으면서 어느 때에 써야할지 명확해지는 것 같다.