2025.03.11 - [Programming Language/React] - [React] Design Pattern in React (3)
[React] Design Pattern in React (3)
2025.03.10 - [Programming Language/React] - [React] Design Pattern in React (2) 이전 글에 이어,이번엔 프로토타입(Prototype) 패턴의 자주 쓰이는 용례들을 적어보고자 한다. 프로토타입 패턴(Prototype)프로토타입
juniortunar.tistory.com
이전 글에 이어,
이번엔 프록시(Proxy) 패턴의 자주 쓰이는 용례들을 적어보고자 한다.
프로토타입 패턴(Prototype)
프록시 패턴은 특정 객체에 대한 접근을 제어하는 대리 객체를 제공하는 디자인 패턴입니다. React에서는 컴포넌트나 데이터에 대한 접근을 중간에서 제어하거나, 추가 기능을 제공하는 방식으로 활용됩니다.
1. 기본 구조
JavaScript의 Proxy 객체를 활용한 기본 구현:
// 기본적인 프록시 패턴 구현
function createStateProxy<T extends Record<string, any>>(initialState: T) {
let listeners: (() => void)[] = [];
let state = { ...initialState };
const notifyListeners = () => {
listeners.forEach(listener => listener());
};
const stateProxy = new Proxy(state, {
get(target, prop) {
if (typeof prop === 'string') {
return target[prop];
}
return undefined;
},
set(target, prop, value) {
if (typeof prop === 'string') {
const oldValue = target[prop];
target[prop] = value;
if (oldValue !== value) {
notifyListeners();
}
}
return true;
}
});
return {
state: stateProxy,
subscribe: (listener: () => void) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
}
};
}
// 사용 예시
const { state, subscribe } = createStateProxy({ count: 0, text: 'Hello' });
// 변경 감지
const unsubscribe = subscribe(() => {
console.log('State changed:', state);
});
// 상태 변경
state.count = 1; // 콘솔에 로그 출력
state.text = 'World'; // 콘솔에 로그 출력
// 구독 해제
unsubscribe();
2. 컴포넌트 프록시
컴포넌트를 감싸서 추가 기능을 제공하는 프록시 패턴:
import { useState, useEffect } from 'react';
// 로딩 상태를 처리하는 프록시 컴포넌트
function withLoading<P extends object>(
Component: React.ComponentType<P>,
loadingCondition: (props: P) => boolean
) {
return function LoadingProxy(props: P) {
const isLoading = loadingCondition(props);
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
}
return <Component {...props} />;
};
}
// 사용 예시
interface DataDisplayProps {
data: any[] | null;
isLoading: boolean;
}
function DataDisplay({ data }: DataDisplayProps) {
return (
<div className="grid grid-cols-3 gap-4">
{data?.map((item, index) => (
<div key={index} className="p-4 border rounded">
{JSON.stringify(item)}
</div>
))}
</div>
);
}
// 프록시 컴포넌트 생성
const DataDisplayWithLoading = withLoading(
DataDisplay,
(props: DataDisplayProps) => props.isLoading || !props.data
);
// 사용
function DataPage() {
const [data, setData] = useState<any[] | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
return <DataDisplayWithLoading data={data} isLoading={isLoading} />;
}
3. API 프록시
API 호출을 중간에서 가로채고 처리하는 프록시:
// API 프록시 클래스
class ApiProxy {
private baseUrl: string;
private authToken: string | null = null;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
setAuthToken(token: string | null) {
this.authToken = token;
}
async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
// 기본 헤더 설정
const headers = new Headers(options.headers);
// 인증 토큰이 있으면 추가
if (this.authToken) {
headers.set('Authorization', `Bearer ${this.authToken}`);
}
// Content-Type이 설정되지 않았고 body가 있으면 JSON으로 설정
if (!headers.has('Content-Type') && options.body &&
typeof options.body !== 'FormData') {
headers.set('Content-Type', 'application/json');
}
// 요청 옵션 병합
const requestOptions: RequestInit = {
...options,
headers
};
// JSON 문자열화
if (requestOptions.body &&
headers.get('Content-Type') === 'application/json' &&
typeof requestOptions.body !== 'string') {
requestOptions.body = JSON.stringify(requestOptions.body);
}
try {
const response = await fetch(url, requestOptions);
// 응답 상태 확인
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `Request failed with status ${response.status}`
);
}
// 응답이 비어있는지 확인
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
return await response.json();
}
return await response.text() as unknown as T;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// 편의 메서드들
async get<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
return this.request<T>(endpoint, { ...options, method: 'GET' });
}
async post<T>(
endpoint: string,
data: any,
options: RequestInit = {}
): Promise<T> {
return this.request<T>(endpoint, {
...options,
method: 'POST',
body: data
});
}
async put<T>(
endpoint: string,
data: any,
options: RequestInit = {}
): Promise<T> {
return this.request<T>(endpoint, {
...options,
method: 'PUT',
body: data
});
}
async delete<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
return this.request<T>(endpoint, { ...options, method: 'DELETE' });
}
}
// API 프록시 인스턴스 생성
export const api = new ApiProxy('/api');
// 사용 예시
interface User {
id: string;
name: string;
email: string;
}
// API 훅
function useApi() {
// 인증 토큰 설정 (실제로는 인증 시스템에서 가져옴)
useEffect(() => {
const token = localStorage.getItem('auth_token');
if (token) {
api.setAuthToken(token);
}
return () => {
// 클린업 시 토큰 제거 (선택적)
// api.setAuthToken(null);
};
}, []);
return {
getUsers: () => api.get<User[]>('/users'),
getUserById: (id: string) => api.get<User>(`/users/${id}`),
createUser: (userData: Omit<User, 'id'>) => api.post<User>('/users', userData),
updateUser: (id: string, userData: Partial<User>) =>
api.put<User>(`/users/${id}`, userData),
deleteUser: (id: string) => api.delete<{ success: boolean }>(`/users/${id}`)
};
}
// 컴포넌트에서 사용
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const apiClient = useApi();
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const data = await apiClient.getUsers();
setUsers(data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch users');
} finally {
setLoading(false);
}
};
fetchUsers();
}, [apiClient]);
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
</div>
);
}
4. 상태 관리 프록시
use 훅과 함께 사용하는 상태 관리 프록시:
import { useState, useEffect, useSyncExternalStore } from 'react';
// 상태 저장소 프록시
class StoreProxy<T extends Record<string, any>> {
private state: T;
private listeners: Set<() => void> = new Set();
constructor(initialState: T) {
this.state = { ...initialState };
}
getState(): T {
return this.state;
}
setState(updater: Partial<T> | ((state: T) => Partial<T>)) {
const newPartialState = typeof updater === 'function'
? updater(this.state)
: updater;
const newState = { ...this.state, ...newPartialState };
// 변경 사항이 있는지 확인
let hasChanged = false;
for (const key in newPartialState) {
if (this.state[key] !== newState[key]) {
hasChanged = true;
break;
}
}
if (hasChanged) {
this.state = newState;
this.notifyListeners();
}
}
subscribe(listener: () => void) {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
private notifyListeners() {
this.listeners.forEach(listener => listener());
}
}
// 사용자 상태 저장소
interface UserState {
currentUser: {
id: string;
name: string;
email: string;
} | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
const initialUserState: UserState = {
currentUser: null,
isAuthenticated: false,
isLoading: false,
error: null
};
// 사용자 저장소 인스턴스 생성
const userStore = new StoreProxy(initialUserState);
// 사용자 액션
const userActions = {
login: async (email: string, password: string) => {
try {
userStore.setState({ isLoading: true, error: null });
// 실제 로그인 API 호출
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Login failed');
}
const userData = await response.json();
// 토큰 저장
localStorage.setItem('auth_token', userData.token);
userStore.setState({
currentUser: userData.user,
isAuthenticated: true,
isLoading: false
});
return true;
} catch (error) {
userStore.setState({
isLoading: false,
error: error instanceof Error ? error.message : 'Login failed'
});
return false;
}
},
logout: () => {
localStorage.removeItem('auth_token');
userStore.setState({
currentUser: null,
isAuthenticated: false
});
},
checkAuth: async () => {
const token = localStorage.getItem('auth_token');
if (!token) {
userStore.setState({
currentUser: null,
isAuthenticated: false
});
return;
}
try {
userStore.setState({ isLoading: true });
const response = await fetch('/api/me', {
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) {
throw new Error('Authentication failed');
}
const userData = await response.json();
userStore.setState({
currentUser: userData,
isAuthenticated: true,
isLoading: false
});
} catch (error) {
localStorage.removeItem('auth_token');
userStore.setState({
currentUser: null,
isAuthenticated: false,
isLoading: false,
error: error instanceof Error ? error.message : 'Authentication failed'
});
}
}
};
// 사용자 훅
function useUser() {
// React 18+ useSyncExternalStore 훅 사용
const state = useSyncExternalStore(
userStore.subscribe.bind(userStore),
userStore.getState.bind(userStore)
);
// 컴포넌트 마운트 시 인증 상태 확인
useEffect(() => {
userActions.checkAuth();
}, []);
return {
...state,
...userActions
};
}
// 로그인 컴포넌트
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login, isLoading, error } = useUser();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login(email, password);
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-1 block w-full rounded-md border p-2"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full rounded-md border p-2"
/>
</div>
{error && <p className="text-red-500">{error}</p>}
<button
type="submit"
disabled={isLoading}
className="w-full py-2 px-4 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? 'Logging in...' : 'Log in'}
</button>
</form>
);
}
// 사용자 프로필 컴포넌트
function UserProfile() {
const { currentUser, isAuthenticated, logout } = useUser();
if (!isAuthenticated || !currentUser) {
return <div>Please log in to view your profile.</div>;
}
return (
<div className="p-4 border rounded-md">
<h2 className="text-xl font-bold">{currentUser.name}</h2>
<p className="text-gray-600">{currentUser.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>
);
}
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>
);
}
Next.js에서 고려해야할 사항
1. 서버 컴포넌트와 클라이언트 컴포넌트 구분
프록시 패턴을 서버 컴포넌트와 클라이언트 컴포넌트에 적용:
서버 컴포넌트
// app/dashboard/page.tsx (서버 컴포넌트)
import { Suspense } from 'react';
import DashboardClient from './DashboardClient';
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';
// 서버 사이드 인증 확인
async function getAuthSession() {
return await getServerSession();
}
export default async function DashboardPage() {
const session = await getAuthSession();
// 서버 사이드 인증 프록시
if (!session) {
redirect('/login');
}
// 서버에서 데이터 가져오기
const dashboardData = await fetch('https://api.example.com/dashboard', {
headers: {
Authorization: `Bearer ${session.accessToken}`
},
next: { revalidate: 60 } // 60초마다 재검증
}).then(res => res.json());
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
<Suspense fallback={<div>Loading dashboard...</div>}>
<DashboardClient initialData={dashboardData} session={session} />
</Suspense>
</div>
);
}
클라이언트 컴포넌트
// app/dashboard/DashboardClient.tsx (클라이언트 컴포넌트)
'use client';
import { useState } from 'react';
import { Session } from 'next-auth';
interface DashboardClientProps {
initialData: any;
session: Session;
}
export default function DashboardClient({
initialData,
session
}: DashboardClientProps) {
const [data, setData] = useState(initialData);
// 클라이언트 사이드 데이터 새로고침
const refreshData = async () => {
try {
const response = await fetch('/api/dashboard', {
headers: {
Authorization: `Bearer ${session.accessToken}`
}
});
if (!response.ok) {
throw new Error('Failed to refresh data');
}
const newData = await response.json();
setData(newData);
} catch (error) {
console.error('Error refreshing data:', error);
}
};
return (
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Dashboard Data</h2>
<button
onClick={refreshData}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Refresh
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.items.map((item: any) => (
<div key={item.id} className="p-4 border rounded-lg shadow-sm">
<h3 className="font-medium">{item.title}</h3>
<p>{item.description}</p>
</div>
))}
</div>
</div>
);
}
2. 서버 액션과 프로토 타입 패턴 통합
서버 액션을 프록시 패턴과 통합하는 예시:
// app/actions/user.ts (서버 액션)
'use server';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
import { getServerSession } from 'next-auth';
// 사용자 스키마
const userSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
role: z.enum(['user', 'admin'], {
errorMap: () => ({ message: 'Invalid role' })
})
});
// 서버 액션 프록시 팩토리
function createServerActionProxy<T, R>(
schema: z.ZodType<T>,
handler: (data: T, session: any) => Promise<R>
) {
return async (formData: FormData): Promise<{
success: boolean;
data?: R;
error?: string;
validationErrors?: Record<string, string[]>;
}> => {
try {
// 인증 확인
const session = await getServerSession();
if (!session) {
return {
success: false,
error: 'Authentication required'
};
}
// FormData를 객체로 변환
const rawData = Object.fromEntries(formData.entries());
// 유효성 검증
const result = schema.safeParse(rawData);
if (!result.success) {
return {
success: false,
validationErrors: result.error.flatten().fieldErrors
};
}
// 핸들러 실행
const data = await handler(result.data, session);
return { success: true, data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'An error occurred'
};
}
};
}
// 사용자 생성 액션
export const createUser = createServerActionProxy(
userSchema,
async (data, session) => {
// 권한 확인
if (session.user.role !== 'admin') {
throw new Error('Only admins can create users');
}
// 데이터베이스에 사용자 생성 로직
console.log('Creating user:', data);
// 예시 응답
const newUser = {
id: `user-${Date.now()}`,
...data,
createdAt: new Date().toISOString()
};
// 경로 재검증
revalidatePath('/users');
return newUser;
}
);
// 사용자 업데이트 액션
export const updateUser = createServerActionProxy(
userSchema.partial().extend({ id: z.string() }),
async (data, session) => {
const { id, ...updateData } = data;
// 권한 확인 (자신의 데이터 또는 관리자만 업데이트 가능)
if (session.user.role !== 'admin' && session.user.id !== id) {
throw new Error('Permission denied');
}
// 데이터베이스에서 사용자 업데이트 로직
console.log('Updating user:', id, updateData);
// 예시 응답
const updatedUser = {
id,
...updateData,
updatedAt: new Date().toISOString()
};
// 경로 재검증
revalidatePath(`/users/${id}`);
revalidatePath('/users');
return updatedUser;
}
);
서버 컴포넌트
// app/users/create/page.tsx (서버 컴포넌트)
import CreateUserForm from './CreateUserForm';
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';
export default async function CreateUserPage() {
const session = await getServerSession();
// 서버 사이드 권한 확인
if (!session || session.user.role !== 'admin') {
redirect('/unauthorized');
}
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">Create New User</h1>
<CreateUserForm />
</div>
);
}
클라이언트 컴포넌트
// app/users/create/CreateUserForm.tsx (클라이언트 컴포넌트)
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createUser } from '@/app/actions/user';
// 제출 버튼 컴포넌트
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="w-full py-2 px-4 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50"
>
{pending ? 'Creating...' : 'Create User'}
</button>
);
}
export default function CreateUserForm() {
const [state, formAction] = useFormState(createUser, {
success: false,
error: undefined,
validationErrors: undefined
});
return (
<form action={formAction} className="max-w-md space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium">
Name
</label>
<input
id="name"
name="name"
type="text"
className="mt-1 block w-full rounded-md border p-2"
/>
{state.validationErrors?.name && (
<p className="mt-1 text-sm text-red-600">{state.validationErrors.name[0]}</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
id="email"
name="email"
type="email"
className="mt-1 block w-full rounded-md border p-2"
/>
{state.validationErrors?.email && (
<p className="mt-1 text-sm text-red-600">{state.validationErrors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="role" className="block text-sm font-medium">
Role
</label>
<select
id="role"
name="role"
className="mt-1 block w-full rounded-md border p-2"
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
{state.validationErrors?.role && (
<p className="mt-1 text-sm text-red-600">{state.validationErrors.role[0]}</p>
)}
</div>
<SubmitButton />
{state.error && (
<p className="text-sm text-red-600">{state.error}</p>
)}
{state.success && (
<p className="text-sm text-green-600">User created successfully!</p>
)}
</form>
);
}
장점
- 관심사 분리: 프록시 패턴은 핵심 로직과 부가 기능(로깅, 캐싱, 인증 등)을 분리하여 코드의 가독성과 유지보수성을 향상시킴
- 코드 재사용: 공통 기능을 프록시에 구현하여 여러 컴포넌트나 서비스에서 재사용 가능.
- 투명한 확장: 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있어 개방-폐쇄 원칙(OCP)을 준수.
- 보안 강화: 인증, 권한 부여 등의 보안 로직을 중앙화하여 일관되게 적용할 수 있음.
- 성능 최적화: 캐싱, 지연 로딩 등의 최적화 전략을 투명하게 적용할 수 있음.
- 에러 처리 일관성: API 호출이나 데이터 접근에 대한 에러 처리를 일관되게 구현할 수 있음.
단점
- 복잡성 증가: 추가적인 계층이 도입되어 코드베이스의 복잡성이 증가할 수 있음
- 디버깅 어려움: 프록시 계층을 통과하는 로직은 디버깅이 더 어려울 수 있음
- 성능 오버헤드: 프록시 계층이 추가되면 약간의 성능 오버헤드가 발생할 수 있음
- 과도한 추상화: 너무 많은 프록시 계층은 코드를 이해하기 어렵게 만들 수 있음
- 테스트 복잡성: 프록시와 실제 구현체를 모두 테스트해야 하므로 테스트가 더 복잡해질 수 있음
어느 때에 쓰는게 적합할까
- API 통신 추상화: 백엔드 API와의 통신을 추상화하고 캐싱, 에러 처리, 인증 토큰 관리 등의 공통 로직을 적용할 때
- 권한 관리: 컴포넌트나 기능에 대한 접근 권한을 제어해야 할 때
- 상태 관리: 전역 상태에 대한 접근을 제어하고 변경 사항을 추적해야 할 때
- 성능 최적화: 비용이 많이 드는 연산이나 네트워크 요청에 캐싱을 적용해야 할 때
- 크로스 커팅 관심사: 로깅, 에러 처리, 분석 등 여러 컴포넌트에 걸쳐 적용해야 하는 기능을 구현할 때
- 레거시 코드 통합: 기존 코드를 변경하지 않고 새로운 기능이나 인터페이스를 제공해야 할 때
- 테스트 용이성: 실제 구현체를 모킹하여 테스트를 쉽게 작성해야 할 때
- 점진적 마이그레이션: 시스템의 일부를 점진적으로 변경하면서 기존 인터페이스를 유지해야 할 때
React를 사용하면 기본적으로 다루게 되는 흔한 패턴이지만,
용례를 적으면서 어느 때에 써야할지 명확해지는 것 같다.