프레임워크별 MDC 베스트 프랙티스: 주요 기술 스택별 최적화 전략




각 프레임워크마다 고유한 특성과 패턴이 있듯이, MDC 규칙도 해당 기술 스택에 맞게 최적화되어야 합니다. 이 가이드에서는 React/Next.js, Vue/Nuxt.js, Node.js/Express, Python/Django 등 주요 프레임워크별로 특화된 MDC 베스트 프랙티스를 상세히 다룹니다.

1. React/Next.js MDC 베스트 프랙티스

React 컴포넌트 패턴 규칙

---
description: React component patterns and best practices
version: "3.1.0"
globs: ["src/components/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"]
alwaysApply: false
framework: "react"
tags: ["react", "components", "patterns", "typescript"]
---

# React 컴포넌트 개발 표준

## ⚛️ 컴포넌트 구조 패턴

### 1. 함수형 컴포넌트 기본 구조
권장 패턴: 명확한 타입 정의와 구조화된 컴포넌트

    interface ButtonProps {
      variant: 'primary' | 'secondary' | 'danger';
      size?: 'sm' | 'md' | 'lg';
      disabled?: boolean;
      loading?: boolean;
      onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
      children: React.ReactNode;
      className?: string;
      'data-testid'?: string;
    }

    export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
      ({ 
        variant, 
        size = 'md', 
        disabled = false, 
        loading = false,
        onClick, 
        children, 
        className,
        'data-testid': testId,
        ...restProps 
      }, ref) => {
        const handleClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
          if (disabled || loading) return;
          onClick(event);
        }, [disabled, loading, onClick]);

        const buttonClasses = useMemo(() => {
          return cn(
            'inline-flex items-center justify-center rounded-md transition-colors',
            {
              'px-3 py-1.5 text-sm': size === 'sm',
              'px-4 py-2 text-base': size === 'md',
              'px-6 py-3 text-lg': size === 'lg',
            },
            {
              'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
              'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
              'bg-red-600 text-white hover:bg-red-700': variant === 'danger',
            },
            {
              'opacity-50 cursor-not-allowed': disabled || loading,
            },
            className
          );
        }, [size, variant, disabled, loading, className]);

        return (
          <button
            ref={ref}
            type="button"
            className={buttonClasses}
            onClick={handleClick}
            disabled={disabled || loading}
            data-testid={testId}
            {...restProps}
          >
            {loading && <Spinner className="mr-2 h-4 w-4" />}
            {children}
          </button>
        );
      }
    );

    Button.displayName = 'Button';

### 2. 컴포넌트 파일 구조
    components/
    ├── Button/
    │   ├── index.ts              # 배럴 익스포트
    │   ├── Button.tsx           # 메인 컴포넌트
    │   ├── Button.test.tsx      # 단위 테스트
    │   ├── Button.stories.tsx   # 스토리북 스토리
    │   └── Button.module.css    # 스타일 (필요시)

## 🎣 React Hooks 패턴

### Custom Hook 작성 규칙
권장 패턴: 타입 안전하고 재사용 가능한 커스텀 훅

    interface UseApiOptions<T> {
      initialData?: T;
      enabled?: boolean;
      refetchInterval?: number;
      onSuccess?: (data: T) => void;
      onError?: (error: Error) => void;
    }

    interface UseApiReturn<T> {
      data: T | null;
      loading: boolean;
      error: Error | null;
      refetch: () => Promise<void>;
    }

    export function useApi<T>(
      url: string, 
      options: UseApiOptions<T> = {}
    ): UseApiReturn<T> {
      const {
        initialData = null,
        enabled = true,
        refetchInterval,
        onSuccess,
        onError
      } = options;

      const [data, setData] = useState<T | null>(initialData);
      const [loading, setLoading] = useState(false);
      const [error, setError] = useState<Error | null>(null);

      const fetchData = useCallback(async () => {
        if (!enabled) return;
        
        try {
          setLoading(true);
          setError(null);
          
          const response = await fetch(url);
          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          }
          
          const result = await response.json();
          setData(result);
          onSuccess?.(result);
        } catch (err) {
          const error = err instanceof Error ? err : new Error('Unknown error');
          setError(error);
          onError?.(error);
        } finally {
          setLoading(false);
        }
      }, [url, enabled, onSuccess, onError]);

      useEffect(() => {
        fetchData();
      }, [fetchData]);

      useEffect(() => {
        if (refetchInterval && enabled) {
          const interval = setInterval(fetchData, refetchInterval);
          return () => clearInterval(interval);
        }
      }, [fetchData, refetchInterval, enabled]);

      return { data, loading, error, refetch: fetchData };
    }

## ⚡ 성능 최적화 패턴

### 메모이제이션 전략
적절한 메모이제이션 사용 예시:

    const ExpensiveComponent = React.memo<Props>(({ data, onAction }) => {
      // 복잡한 계산은 useMemo로 캐싱
      const processedData = useMemo(() => {
        return data.map(item => ({
          ...item,
          calculated: expensiveCalculation(item)
        }));
      }, [data]);

      // 콜백 함수는 useCallback으로 안정화
      const handleItemClick = useCallback((id: string) => {
        onAction({ type: 'ITEM_CLICKED', payload: id });
      }, [onAction]);

      return (
        <div>
          {processedData.map(item => (
            <Item 
              key={item.id} 
              item={item} 
              onClick={handleItemClick}
            />
          ))}
        </div>
      );
    });

@component-template.tsx
@hooks-template.ts
@performance-patterns.tsx

Next.js 특화 규칙

---
description: Next.js specific patterns and optimizations
version: "2.0.0"
globs: ["pages/**/*.{ts,tsx}", "app/**/*.{ts,tsx}", "src/app/**/*.{ts,tsx}"]
alwaysApply: false
framework: "nextjs"
extends: ["react-patterns.mdc"]
tags: ["nextjs", "ssr", "routing", "optimization"]
---

# Next.js 최적화 패턴

## 🚀 페이지 및 라우팅 패턴

### 1. App Router 패턴 (Next.js 13+)
app/products/[id]/page.tsx 예시:

    interface ProductPageProps {
      params: { id: string };
      searchParams: { [key: string]: string | string[] | undefined };
    }

    export default async function ProductPage({ 
      params, 
      searchParams 
    }: ProductPageProps) {
      // 서버 컴포넌트에서 데이터 페칭
      const product = await getProduct(params.id);
      
      if (!product) {
        notFound(); // Next.js 404 처리
      }

      return (
        <div>
          <ProductHeader product={product} />
          <ProductDetails product={product} />
          <ProductReviews productId={params.id} />
        </div>
      );
    }

    // 메타데이터 생성
    export async function generateMetadata({ 
      params 
    }: ProductPageProps): Promise<Metadata> {
      const product = await getProduct(params.id);
      
      return {
        title: product?.name ?? 'Product Not Found',
        description: product?.description,
        openGraph: {
          title: product?.name,
          description: product?.description,
          images: product?.images?.map(img => ({ url: img.url })),
        },
      };
    }

    // 정적 파라미터 생성
    export async function generateStaticParams() {
      const products = await getProducts();
      
      return products.map((product) => ({
        id: product.id,
      }));
    }

### 2. 데이터 페칭 패턴
서버 컴포넌트 데이터 페칭:

    async function ServerComponent() {
      // 병렬 데이터 페칭
      const [user, posts, comments] = await Promise.all([
        getUser(),
        getPosts(),
        getComments()
      ]);

      return (
        <div>
          <UserProfile user={user} />
          <PostsList posts={posts} />
          <CommentsList comments={comments} />
        </div>
      );
    }

클라이언트 컴포넌트 데이터 페칭:

    'use client';

    function ClientComponent() {
      const { data, error, isLoading } = useSWR('/api/data', fetcher, {
        revalidateOnFocus: false,
        revalidateOnReconnect: true,
        refreshInterval: 30000,
      });

      if (error) return <ErrorBoundary error={error} />;
      if (isLoading) return <LoadingSkeleton />;

      return <DataDisplay data={data} />;
    }

## 🏎️ 성능 최적화

### 이미지 최적화
Next.js Image 컴포넌트 최적 사용:

    import Image from 'next/image';

    function OptimizedImage({ src, alt }: { src: string; alt: string }) {
      return (
        <Image
          src={src}
          alt={alt}
          width={800}
          height={600}
          sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
          priority={false} // Above-the-fold 이미지만 true
          placeholder="blur"
          blurDataURL="data:image/jpeg;base64,..."
          style={{
            width: '100%',
            height: 'auto',
          }}
        />
      );
    }

### 동적 임포트 패턴
컴포넌트 지연 로딩:

    const DynamicComponent = dynamic(() => import('./HeavyComponent'), {
      loading: () => <ComponentSkeleton />,
      ssr: false, // 클라이언트에서만 렌더링
    });

라이브러리 지연 로딩:

    const DynamicChart = dynamic(() => import('react-chartjs-2'), {
      loading: () => <ChartSkeleton />,
    });

@nextjs-page-template.tsx
@api-route-template.ts
@middleware-template.ts

2. Vue/Nuxt.js MDC 베스트 프랙티스

Vue 3 Composition API 패턴

---
description: Vue 3 Composition API patterns and best practices
version: "2.5.0"
globs: ["src/components/**/*.vue", "components/**/*.vue", "composables/**/*.{ts,js}"]
alwaysApply: false
framework: "vue"
tags: ["vue3", "composition-api", "typescript", "reactivity"]
---

# Vue 3 Composition API 개발 표준

## 🎨 컴포넌트 구조 패턴

### 1. SFC (Single File Component) 구조
UserProfile.vue 예시:

    <template>
      <div class="user-profile" :class="{ 'loading': isLoading }">
        <div v-if="error" class="error-message">
          {{ error.message }}
        </div>
        
        <div v-else-if="user" class="user-content">
          <img 
            :src="user.avatar" 
            :alt="`${user.name} avatar`"
            class="user-avatar"
            @error="handleImageError"
          />
          <div class="user-info">
            <h2>{{ user.name }}</h2>
            <p>{{ user.email }}</p>
            <button 
              @click="handleFollow" 
              :disabled="isFollowing"
              class="follow-btn"
            >
              {{ isFollowing ? 'Following...' : 'Follow' }}
            </button>
          </div>
        </div>
        
        <div v-else class="loading-skeleton">
          <UserSkeleton />
        </div>
      </div>
    </template>

    <script setup lang="ts">
    interface User {
      id: string;
      name: string;
      email: string;
      avatar: string;
    }

    interface Props {
      userId: string;
      showFollowButton?: boolean;
    }

    interface Emits {
      follow: [userId: string];
      error: [error: Error];
    }

    // Props 및 Emits 정의
    const props = withDefaults(defineProps<Props>(), {
      showFollowButton: true
    });

    const emit = defineEmits<Emits>();

    // Composables 사용
    const { user, isLoading, error, fetchUser } = useUser(props.userId);
    const { isFollowing, followUser } = useFollow();

    // 반응형 데이터
    const imageError = ref(false);

    // 계산된 속성
    const displayName = computed(() => {
      return user.value?.name || 'Unknown User';
    });

    // 메서드
    const handleFollow = async () => {
      try {
        await followUser(props.userId);
        emit('follow', props.userId);
      } catch (err) {
        emit('error', err as Error);
      }
    };

    const handleImageError = () => {
      imageError.value = true;
    };

    // 라이프사이클
    onMounted(() => {
      fetchUser();
    });

    // 워처
    watch(() => props.userId, (newUserId) => {
      if (newUserId) {
        fetchUser();
      }
    }, { immediate: true });
    </script>

    <style scoped>
    .user-profile {
      @apply p-4 border rounded-lg;
    }

    .user-content {
      @apply flex items-center space-x-4;
    }

    .user-avatar {
      @apply w-16 h-16 rounded-full object-cover;
    }

    .follow-btn {
      @apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50;
    }

    .loading {
      @apply opacity-50 pointer-events-none;
    }
    </style>

### 2. Composables 패턴
composables/useUser.ts 예시:

    export function useUser(userId: string) {
      const user = ref<User | null>(null);
      const isLoading = ref(false);
      const error = ref<Error | null>(null);

      const fetchUser = async () => {
        if (!userId) return;
        
        try {
          isLoading.value = true;
          error.value = null;
          
          const response = await $fetch<User>(`/api/users/${userId}`);
          user.value = response;
        } catch (err) {
          error.value = err as Error;
        } finally {
          isLoading.value = false;
        }
      };

      const updateUser = async (updates: Partial<User>) => {
        if (!user.value) return;
        
        try {
          const updated = await $fetch<User>(`/api/users/${userId}`, {
            method: 'PATCH',
            body: updates,
          });
          user.value = { ...user.value, ...updated };
        } catch (err) {
          error.value = err as Error;
          throw err;
        }
      };

      return {
        user: readonly(user),
        isLoading: readonly(isLoading),
        error: readonly(error),
        fetchUser,
        updateUser,
      };
    }

## 🗃️ 상태 관리 패턴

### Pinia Store 패턴
stores/user.ts 예시:

    import { defineStore } from 'pinia';

    interface UserState {
      currentUser: User | null;
      users: Record<string, User>;
      loading: boolean;
      error: string | null;
    }

    export const useUserStore = defineStore('user', () => {
      // State
      const state = reactive<UserState>({
        currentUser: null,
        users: {},
        loading: false,
        error: null,
      });

      // Getters
      const isAuthenticated = computed(() => !!state.currentUser);
      const getUserById = computed(() => {
        return (id: string) => state.users[id];
      });

      // Actions
      const login = async (credentials: LoginCredentials) => {
        try {
          state.loading = true;
          state.error = null;
          
          const user = await authApi.login(credentials);
          state.currentUser = user;
          
          // 로컬 스토리지에 토큰 저장
          const token = await authApi.getToken();
          localStorage.setItem('auth-token', token);
          
          return user;
        } catch (error) {
          state.error = error.message;
          throw error;
        } finally {
          state.loading = false;
        }
      };

      const logout = async () => {
        try {
          await authApi.logout();
        } finally {
          state.currentUser = null;
          localStorage.removeItem('auth-token');
        }
      };

      const fetchUser = async (id: string) => {
        if (state.users[id]) {
          return state.users[id];
        }

        try {
          const user = await userApi.getUser(id);
          state.users[id] = user;
          return user;
        } catch (error) {
          state.error = error.message;
          throw error;
        }
      };

      return {
        // State
        ...toRefs(state),
        
        // Getters
        isAuthenticated,
        getUserById,
        
        // Actions
        login,
        logout,
        fetchUser,
      };
    });

@vue-component-template.vue
@composable-template.ts
@pinia-store-template.ts

Nuxt.js 특화 패턴

---
description: Nuxt.js specific patterns and optimizations
version: "1.8.0"
globs: ["pages/**/*.vue", "layouts/**/*.vue", "middleware/**/*.{ts,js}", "plugins/**/*.{ts,js}"]
alwaysApply: false
framework: "nuxt"
extends: ["vue-patterns.mdc"]
tags: ["nuxt3", "ssr", "routing", "modules"]
---

# Nuxt.js 3 최적화 패턴

## 📄 페이지 및 레이아웃 패턴

### 1. 페이지 컴포넌트 구조
pages/products/[id].vue 예시:

    <template>
      <div>
        <Head>
          <Title>{{ product?.name || 'Loading...' }}</Title>
          <Meta name="description" :content="product?.description" />
        </Head>
        
        <div v-if="pending" class="loading">
          <ProductSkeleton />
        </div>
        
        <div v-else-if="error" class="error">
          <ErrorMessage :error="error" />
        </div>
        
        <div v-else-if="product">
          <ProductHeader :product="product" />
          <ProductDetails :product="product" />
          <LazyProductReviews :product-id="product.id" />
        </div>
      </div>
    </template>

    <script setup lang="ts">
    // 페이지 메타데이터
    definePageMeta({
      layout: 'product',
      middleware: ['auth'],
      keepalive: true,
    });

    // 서버 사이드 데이터 페칭
    const route = useRoute();
    const { data: product, pending, error } = await useFetch(`/api/products/${route.params.id}`, {
      key: `product-${route.params.id}`,
      server: true,
      default: () => null,
      transform: (data: any) => ({
        ...data,
        price: Number(data.price),
        createdAt: new Date(data.createdAt),
      }),
    });

    // SEO 최적화
    useSeoMeta({
      title: () => product.value?.name,
      ogTitle: () => product.value?.name,
      description: () => product.value?.description,
      ogDescription: () => product.value?.description,
      ogImage: () => product.value?.images?.[0],
    });
    </script>

### 2. 미들웨어 패턴
middleware/auth.ts:

    export default defineNuxtRouteMiddleware((to, from) => {
      const { $auth } = useNuxtApp();
      
      if (!$auth.isAuthenticated) {
        return navigateTo('/login', {
          redirectTo: to.fullPath,
        });
      }
    });

middleware/role.ts:

    export default defineNuxtRouteMiddleware((to) => {
      const { $auth } = useNuxtApp();
      const requiredRole = to.meta.role;
      
      if (requiredRole && !$auth.hasRole(requiredRole)) {
        throw createError({
          statusCode: 403,
          statusMessage: 'Access Denied',
        });
      }
    });

### 3. 플러그인 패턴
plugins/api.client.ts:

    export default defineNuxtPlugin(() => {
      const config = useRuntimeConfig();
      
      const api = $fetch.create({
        baseURL: config.public.apiBase,
        
        onRequest({ request, options }) {
          // 인증 토큰 추가
          const token = useCookie('auth-token');
          if (token.value) {
            options.headers = {
              ...options.headers,
              Authorization: `Bearer ${token.value}`,
            };
          }
        },
        
        onResponseError({ response }) {
          // 에러 처리
          if (response.status === 401) {
            return navigateTo('/login');
          }
        },
      });

      return {
        provide: {
          api,
        },
      };
    });

@nuxt-page-template.vue
@nuxt-middleware-template.ts
@nuxt-plugin-template.ts

3. Node.js/Express MDC 베스트 프랙티스

Express API 설계 패턴

---
description: Node.js Express API design patterns and best practices
version: "2.8.0"
globs: ["src/routes/**/*.{ts,js}", "src/controllers/**/*.{ts,js}", "src/middleware/**/*.{ts,js}"]
alwaysApply: false
framework: "express"
tags: ["nodejs", "express", "api", "middleware", "typescript"]
---

# Express API 개발 표준

## 🛠️ API 구조 패턴

### 1. 컨트롤러 패턴
controllers/userController.ts 예시:

    import { Request, Response, NextFunction } from 'express';
    import { z } from 'zod';
    import { UserService } from '../services/UserService';
    import { validateRequest } from '../middleware/validation';
    import { ApiResponse } from '../types/api';

    // 요청 스키마 정의
    const CreateUserSchema = z.object({
      body: z.object({
        name: z.string().min(2).max(50),
        email: z.string().email(),
        password: z.string().min(8).regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
        role: z.enum(['user', 'admin']).default('user'),
      }),
    });

    const UpdateUserSchema = z.object({
      params: z.object({
        id: z.string().uuid(),
      }),
      body: z.object({
        name: z.string().min(2).max(50).optional(),
        email: z.string().email().optional(),
        status: z.enum(['active', 'inactive']).optional(),
      }).refine(data => Object.keys(data).length > 0, {
        message: "At least one field must be provided for update"
      }),
    });

    export class UserController {
      constructor(private userService: UserService) {}

      // 사용자 생성
      createUser = async (
        req: Request, 
        res: Response<ApiResponse<User>>, 
        next: NextFunction
      ) => {
        try {
          const { body } = req as z.infer<typeof CreateUserSchema>;
          
          const user = await this.userService.createUser(body);
          
          res.status(201).json({
            success: true,
            data: user,
            message: 'User created successfully',
          });
        } catch (error) {
          next(error);
        }
      };

      // 사용자 목록 조회
      getUsers = async (
        req: Request, 
        res: Response<ApiResponse<User[]>>, 
        next: NextFunction
      ) => {
        try {
          const { page = 1, limit = 10, search } = req.query;
          
          const result = await this.userService.getUsers({
            page: Number(page),
            limit: Number(limit),
            search: search as string,
          });
          
          res.json({
            success: true,
            data: result.users,
            meta: {
              page: result.page,
              limit: result.limit,
              total: result.total,
              totalPages: result.totalPages,
            },
          });
        } catch (error) {
          next(error);
        }
      };

      // 사용자 수정
      updateUser = async (
        req: Request, 
        res: Response<ApiResponse<User>>, 
        next: NextFunction
      ) => {
        try {
          const { params, body } = req as z.infer<typeof UpdateUserSchema>;
          
          const user = await this.userService.updateUser(params.id, body);
          
          res.json({
            success: true,
            data: user,
            message: 'User updated successfully',
          });
        } catch (error) {
          next(error);
        }
      };
    }

    // 라우터 설정
    export const createUserRoutes = (userService: UserService) => {
      const controller = new UserController(userService);
      const router = express.Router();

      router.post('/', 
        validateRequest(CreateUserSchema), 
        controller.createUser
      );
      
      router.get('/', 
        authenticateToken, 
        authorizeRoles(['admin', 'manager']),
        controller.getUsers
      );
      
      router.put('/:id', 
        authenticateToken,
        validateRequest(UpdateUserSchema),
        controller.updateUser
      );

      return router;
    };

### 2. 미들웨어 패턴
middleware/auth.ts 예시:

    import jwt from 'jsonwebtoken';
    import { Request, Response, NextFunction } from 'express';
    import { AppError } from '../utils/AppError';

    interface AuthenticatedRequest extends Request {
      user?: {
        id: string;
        email: string;
        role: string;
      };
    }

    export const authenticateToken = async (
      req: AuthenticatedRequest,
      res: Response,
      next: NextFunction
    ) => {
      try {
        const authHeader = req.headers.authorization;
        const token = authHeader?.startsWith('Bearer ') 
          ? authHeader.substring(7) 
          : null;

        if (!token) {
          throw new AppError('Access token is required', 401);
        }

        const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
        
        // 토큰 만료 검증
        if (decoded.exp < Date.now() / 1000) {
          throw new AppError('Token has expired', 401);
        }

        // 사용자 정보 조회 및 검증
        const user = await UserService.findById(decoded.userId);
        if (!user || user.status !== 'active') {
          throw new AppError('Invalid token', 401);
        }

        req.user = {
          id: user.id,
          email: user.email,
          role: user.role,
        };

        next();
      } catch (error) {
        if (error instanceof jwt.JsonWebTokenError) {
          next(new AppError('Invalid token', 401));
        } else {
          next(error);
        }
      }
    };

    export const authorizeRoles = (roles: string[]) => {
      return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
        if (!req.user) {
          return next(new AppError('Authentication required', 401));
        }

        if (!roles.includes(req.user.role)) {
          return next(new AppError('Insufficient permissions', 403));
        }

        next();
      };
    };

    // 요청 제한 미들웨어
    export const rateLimiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15분
      max: 100, // 최대 100 요청
      message: {
        success: false,
        error: 'Too many requests, please try again later',
      },
      standardHeaders: true,
      legacyHeaders: false,
    });

### 3. 에러 처리 패턴
utils/AppError.ts:

    export class AppError extends Error {
      public statusCode: number;
      public isOperational: boolean;

      constructor(message: string, statusCode: number) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = true;

        Error.captureStackTrace(this, this.constructor);
      }
    }

middleware/errorHandler.ts:

    export const errorHandler = (
      error: Error,
      req: Request,
      res: Response,
      next: NextFunction
    ) => {
      let statusCode = 500;
      let message = 'Internal Server Error';

      // 운영 환경에서는 스택 트레이스 숨김
      const stack = process.env.NODE_ENV === 'production' ? undefined : error.stack;

      if (error instanceof AppError) {
        statusCode = error.statusCode;
        message = error.message;
      } else if (error instanceof z.ZodError) {
        statusCode = 400;
        message = 'Validation failed';
      } else if (error.name === 'ValidationError') {
        statusCode = 400;
        message = error.message;
      }

      // 로깅
      console.error({
        error: {
          message: error.message,
          stack: error.stack,
          statusCode,
        },
        request: {
          method: req.method,
          url: req.url,
          headers: req.headers,
          body: req.body,
        },
      });

      res.status(statusCode).json({
        success: false,
        error: message,
        ...(process.env.NODE_ENV === 'development' && { stack }),
      });
    };

@express-controller-template.ts
@middleware-template.ts
@error-handler-template.ts

데이터베이스 통합 패턴

---
description: Database integration patterns for Node.js applications
version: "1.6.0"
globs: ["src/models/**/*.{ts,js}", "src/repositories/**/*.{ts,js}", "src/services/**/*.{ts,js}"]
alwaysApply: false
framework: "nodejs"
tags: ["database", "orm", "repository", "service"]
---

# 데이터베이스 통합 패턴

## 🗄️ Repository 패턴

### 1. Generic Repository
repositories/BaseRepository.ts:

    export abstract class BaseRepository<T, CreateDTO, UpdateDTO> {
      constructor(protected model: Model<T>) {}

      async findById(id: string): Promise<T | null> {
        return await this.model.findByPk(id);
      }

      async findAll(options: FindOptions = {}): Promise<T[]> {
        return await this.model.findAll(options);
      }

      async create(data: CreateDTO): Promise<T> {
        return await this.model.create(data as any);
      }

      async update(id: string, data: UpdateDTO): Promise<T | null> {
        const [affectedCount] = await this.model.update(data as any, {
          where: { id },
          returning: true,
        });

        if (affectedCount === 0) {
          return null;
        }

        return await this.findById(id);
      }

      async delete(id: string): Promise<boolean> {
        const deletedCount = await this.model.destroy({
          where: { id },
        });

        return deletedCount > 0;
      }

      async findWithPagination({
        page = 1,
        limit = 10,
        where = {},
        include = [],
        order = [['createdAt', 'DESC']],
      }: PaginationOptions): Promise<PaginatedResult<T>> {
        const offset = (page - 1) * limit;

        const { count, rows } = await this.model.findAndCountAll({
          where,
          include,
          order,
          limit,
          offset,
        });

        return {
          data: rows,
          page,
          limit,
          total: count,
          totalPages: Math.ceil(count / limit),
        };
      }
    }

repositories/UserRepository.ts:

    export class UserRepository extends BaseRepository<User, CreateUserDTO, UpdateUserDTO> {
      constructor() {
        super(UserModel);
      }

      async findByEmail(email: string): Promise<User | null> {
        return await this.model.findOne({
          where: { email },
        });
      }

      async findActiveUsers(): Promise<User[]> {
        return await this.model.findAll({
          where: { status: 'active' },
          include: [
            {
              model: ProfileModel,
              as: 'profile',
            },
          ],
        });
      }

      async searchUsers(query: string): Promise<User[]> {
        return await this.model.findAll({
          where: {
            [Op.or]: [
              { name: { [Op.iLike]: `%${query}%` } },
              { email: { [Op.iLike]: `%${query}%` } },
            ],
          },
        });
      }
    }

### 2. Service Layer 패턴
services/UserService.ts:

    export class UserService {
      constructor(
        private userRepository: UserRepository,
        private emailService: EmailService,
        private cacheService: CacheService
      ) {}

      async createUser(userData: CreateUserDTO): Promise<User> {
        // 비즈니스 로직 검증
        const existingUser = await this.userRepository.findByEmail(userData.email);
        if (existingUser) {
          throw new AppError('Email already exists', 409);
        }

        // 비밀번호 해싱
        const hashedPassword = await bcrypt.hash(userData.password, 12);

        // 트랜잭션 처리
        const transaction = await sequelize.transaction();
        
        try {
          const user = await this.userRepository.create({
            ...userData,
            password: hashedPassword,
          });

          // 프로필 생성
          await ProfileModel.create({
            userId: user.id,
            displayName: user.name,
          }, { transaction });

          // 환영 이메일 발송
          await this.emailService.sendWelcomeEmail(user.email, user.name);

          // 캐시 무효화
          await this.cacheService.deletePattern(`users:*`);

          await transaction.commit();
          return user;
        } catch (error) {
          await transaction.rollback();
          throw error;
        }
      }

      async getUsersWithCache(options: PaginationOptions): Promise<PaginatedResult<User>> {
        const cacheKey = `users:${JSON.stringify(options)}`;
        
        // 캐시 확인
        const cached = await this.cacheService.get(cacheKey);
        if (cached) {
          return JSON.parse(cached);
        }

        // 데이터베이스 조회
        const result = await this.userRepository.findWithPagination(options);

        // 캐시 저장 (5분)
        await this.cacheService.set(cacheKey, JSON.stringify(result), 300);

        return result;
      }

      async updateUserStatus(userId: string, status: UserStatus): Promise<User> {
        const user = await this.userRepository.findById(userId);
        if (!user) {
          throw new AppError('User not found', 404);
        }

        // 상태 변경 로직
        if (status === 'inactive' && user.role === 'admin') {
          throw new AppError('Cannot deactivate admin user', 400);
        }

        const updatedUser = await this.userRepository.update(userId, { status });
        
        // 이벤트 발행
        await this.eventEmitter.emit('user.status.changed', {
          userId,
          oldStatus: user.status,
          newStatus: status,
        });

        return updatedUser!;
      }
    }

@repository-template.ts
@service-template.ts
@database-config.ts

4. Python/Django MDC 베스트 프랙티스

Django 모델 및 뷰 패턴

---
description: Django development patterns and best practices
version: "3.2.0"
globs: ["**/models.py", "**/views.py", "**/serializers.py", "**/urls.py"]
alwaysApply: false
framework: "django"
tags: ["python", "django", "rest", "orm", "api"]
---

# Django 개발 표준

## 🏗️ 모델 설계 패턴

### 1. 모델 클래스 구조
models.py 예시:

    from django.db import models
    from django.contrib.auth.models import AbstractUser
    from django.core.validators import MinLengthValidator, RegexValidator
    from django.utils import timezone
    from uuid import uuid4

    class TimeStampedModel(models.Model):
        """타임스탬프 기본 모델"""
        id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)

        class Meta:
            abstract = True

    class User(AbstractUser):
        """사용자 모델"""
        email = models.EmailField(unique=True)
        phone_number = models.CharField(
            max_length=20,
            validators=[RegexValidator(
                regex=r'^\+?1?\d{9,15}$',
                message="Phone number must be entered in the format: '+999999999'"
            )],
            blank=True
        )
        is_verified = models.BooleanField(default=False)
        last_login_ip = models.GenericIPAddressField(null=True, blank=True)
        
        USERNAME_FIELD = 'email'
        REQUIRED_FIELDS = ['username', 'first_name', 'last_name']

        class Meta:
            db_table = 'users'
            indexes = [
                models.Index(fields=['email']),
                models.Index(fields=['is_active', 'is_verified']),
            ]

        def __str__(self):
            return f"{self.get_full_name()} ({self.email})"

        @property
        def full_name(self):
            return f"{self.first_name} {self.last_name}".strip()

    class Product(TimeStampedModel):
        """상품 모델"""
        name = models.CharField(max_length=200, validators=[MinLengthValidator(3)])
        slug = models.SlugField(unique=True)
        description = models.TextField()
        price = models.DecimalField(max_digits=10, decimal_places=2)
        stock_quantity = models.PositiveIntegerField(default=0)
        is_active = models.BooleanField(default=True)
        category = models.ForeignKey(
            'Category',
            on_delete=models.PROTECT,
            related_name='products'
        )
        tags = models.ManyToManyField('Tag', blank=True)

        class Meta:
            db_table = 'products'
            ordering = ['-created_at']
            indexes = [
                models.Index(fields=['slug']),
                models.Index(fields=['is_active', 'category']),
                models.Index(fields=['price']),
            ]

        def __str__(self):
            return self.name

        def save(self, *args, **kwargs):
            # 슬러그 자동 생성
            if not self.slug:
                from django.utils.text import slugify
                self.slug = slugify(self.name)
            super().save(*args, **kwargs)

        @property
        def is_in_stock(self):
            return self.stock_quantity > 0

        def reduce_stock(self, quantity):
            """재고 감소 메서드"""
            if self.stock_quantity >= quantity:
                self.stock_quantity -= quantity
                self.save(update_fields=['stock_quantity'])
            else:
                raise ValueError("Insufficient stock")

### 2. Django REST Framework 패턴
serializers.py:

    from rest_framework import serializers
    from rest_framework.validators import UniqueValidator
    from django.contrib.auth.password_validation import validate_password
    from .models import User, Product, Category

    class UserRegistrationSerializer(serializers.ModelSerializer):
        """사용자 등록 시리얼라이저"""
        email = serializers.EmailField(
            validators=[UniqueValidator(queryset=User.objects.all())]
        )
        password = serializers.CharField(
            write_only=True,
            validators=[validate_password]
        )
        password_confirm = serializers.CharField(write_only=True)

        class Meta:
            model = User
            fields = ('email', 'username', 'first_name', 'last_name', 
                     'password', 'password_confirm', 'phone_number')

        def validate(self, attrs):
            if attrs['password'] != attrs['password_confirm']:
                raise serializers.ValidationError("Passwords don't match")
            return attrs

        def create(self, validated_data):
            validated_data.pop('password_confirm')
            password = validated_data.pop('password')
            user = User.objects.create_user(**validated_data)
            user.set_password(password)
            user.save()
            return user

    class ProductSerializer(serializers.ModelSerializer):
        """상품 시리얼라이저"""
        category_name = serializers.CharField(source='category.name', read_only=True)
        tags = serializers.StringRelatedField(many=True, read_only=True)
        is_in_stock = serializers.ReadOnlyField()

        class Meta:
            model = Product
            fields = '__all__'
            read_only_fields = ('id', 'created_at', 'updated_at', 'slug')

        def validate_price(self, value):
            if value <= 0:
                raise serializers.ValidationError("Price must be greater than 0")
            return value

        def to_representation(self, instance):
            data = super().to_representation(instance)
            # 가격을 문자열로 포맷팅
            data['formatted_price'] = f"${data['price']}"
            return data

views.py:

    from rest_framework import generics, status, permissions
    from rest_framework.decorators import api_view, permission_classes
    from rest_framework.response import Response
    from rest_framework.pagination import PageNumberPagination
    from django_filters.rest_framework import DjangoFilterBackend
    from django.db.models import Q
    from .models import Product, User
    from .serializers import ProductSerializer, UserRegistrationSerializer

    class StandardResultsSetPagination(PageNumberPagination):
        page_size = 20
        page_size_query_param = 'page_size'
        max_page_size = 100

    class ProductListCreateView(generics.ListCreateAPIView):
        """상품 목록 조회 및 생성"""
        queryset = Product.objects.select_related('category').prefetch_related('tags')
        serializer_class = ProductSerializer
        permission_classes = [permissions.IsAuthenticatedOrReadOnly]
        pagination_class = StandardResultsSetPagination
        filter_backends = [DjangoFilterBackend]
        filterset_fields = ['category', 'is_active']
        search_fields = ['name', 'description']
        ordering_fields = ['name', 'price', 'created_at']
        ordering = ['-created_at']

        def get_queryset(self):
            queryset = super().get_queryset()
            
            # 검색 기능
            search = self.request.query_params.get('search')
            if search:
                queryset = queryset.filter(
                    Q(name__icontains=search) | 
                    Q(description__icontains=search)
                )
            
            # 가격 범위 필터
            min_price = self.request.query_params.get('min_price')
            max_price = self.request.query_params.get('max_price')
            
            if min_price:
                queryset = queryset.filter(price__gte=min_price)
            if max_price:
                queryset = queryset.filter(price__lte=max_price)
                
            return queryset

        def perform_create(self, serializer):
            # 생성 시 추가 로직
            serializer.save()
            
            # 로그 기록
            logger.info(f"Product created: {serializer.instance.name}")

    class ProductDetailView(generics.RetrieveUpdateDestroyAPIView):
        """상품 상세 조회, 수정, 삭제"""
        queryset = Product.objects.select_related('category').prefetch_related('tags')
        serializer_class = ProductSerializer
        lookup_field = 'slug'
        
        def get_permissions(self):
            if self.request.method == 'GET':
                permission_classes = [permissions.AllowAny]
            else:
                permission_classes = [permissions.IsAuthenticated]
            return [permission() for permission in permission_classes]

    @api_view(['POST'])
    @permission_classes([permissions.AllowAny])
    def register_user(request):
        """사용자 등록"""
        serializer = UserRegistrationSerializer(data=request.data)
        
        if serializer.is_valid():
            user = serializer.save()
            
            # 환영 이메일 발송 (Celery 태스크)
            from .tasks import send_welcome_email
            send_welcome_email.delay(user.id)
            
            return Response({
                'message': 'User registered successfully',
                'user_id': user.id
            }, status=status.HTTP_201_CREATED)
        
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

### 3. Django 고급 패턴
managers.py:

    from django.db import models
    from django.utils import timezone

    class ActiveProductManager(models.Manager):
        """활성화된 상품만 조회하는 매니저"""
        def get_queryset(self):
            return super().get_queryset().filter(is_active=True)

    class ProductQuerySet(models.QuerySet):
        """상품 QuerySet"""
        def active(self):
            return self.filter(is_active=True)
        
        def in_stock(self):
            return self.filter(stock_quantity__gt=0)
        
        def by_category(self, category):
            return self.filter(category=category)
        
        def price_range(self, min_price=None, max_price=None):
            qs = self
            if min_price:
                qs = qs.filter(price__gte=min_price)
            if max_price:
                qs = qs.filter(price__lte=max_price)
            return qs

models.py (업데이트):

    class Product(TimeStampedModel):
        # ... 기존 필드들 ...
        
        objects = models.Manager()
        active_objects = ActiveProductManager()
        
        def get_queryset(self):
            return ProductQuerySet(self.model, using=self._db)

signals.py:

    from django.db.models.signals import post_save, pre_delete
    from django.dispatch import receiver
    from django.core.cache import cache
    from .models import Product

    @receiver(post_save, sender=Product)
    def invalidate_product_cache(sender, instance, **kwargs):
        """상품 저장 시 캐시 무효화"""
        cache.delete_many([
            f'product_{instance.slug}',
            'product_list',
            f'category_{instance.category.id}_products'
        ])

    @receiver(pre_delete, sender=Product)
    def log_product_deletion(sender, instance, **kwargs):
        """상품 삭제 로그"""
        logger.warning(f"Product deleted: {instance.name} (ID: {instance.id})")

tasks.py (Celery):

    from celery import shared_task
    from django.core.mail import send_mail
    from django.template.loader import render_to_string
    from .models import User

    @shared_task
    def send_welcome_email(user_id):
        """환영 이메일 발송 태스크"""
        try:
            user = User.objects.get(id=user_id)
            
            subject = 'Welcome to Our Platform!'
            html_message = render_to_string('emails/welcome.html', {
                'user': user,
            })
            
            send_mail(
                subject,
                '',
                'noreply@example.com',
                [user.email],
                html_message=html_message,
            )
            
            logger.info(f"Welcome email sent to {user.email}")
        except User.DoesNotExist:
            logger.error(f"User with ID {user_id} not found")
        except Exception as e:
            logger.error(f"Failed to send welcome email: {str(e)}")

    @shared_task
    def cleanup_inactive_users():
        """비활성 사용자 정리 태스크"""
        from datetime import timedelta
        from django.utils import timezone
        
        cutoff_date = timezone.now() - timedelta(days=365)
        inactive_users = User.objects.filter(
            last_login__lt=cutoff_date,
            is_active=False
        )
        
        count = inactive_users.count()
        inactive_users.delete()
        
        logger.info(f"Deleted {count} inactive users")

@django-model-template.py
@django-serializer-template.py
@django-view-template.py
@celery-task-template.py

5. 크로스 프레임워크 공통 패턴

환경 설정 관리

---
description: Environment configuration management across frameworks
version: "1.4.0"
globs: ["**/.env*", "**/config/**/*.{ts,js,py}", "**/settings/**/*.{ts,js,py}"]
alwaysApply: true
category: "configuration"
tags: ["environment", "config", "security", "deployment"]
---

# 환경 설정 관리 표준

## ⚙️ 설정 파일 구조

### 1. 환경별 설정 분리
config/index.ts:

    interface Config {
      app: {
        name: string;
        version: string;
        port: number;
        env: 'development' | 'staging' | 'production';
      };
      database: {
        url: string;
        maxConnections: number;
        ssl: boolean;
      };
      redis: {
        url: string;
        ttl: number;
      };
      auth: {
        jwtSecret: string;
        jwtExpiry: string;
        bcryptRounds: number;
      };
      external: {
        apiKey: string;
        webhookSecret: string;
      };
    }

    const config: Config = {
      app: {
        name: process.env.APP_NAME || 'MyApp',
        version: process.env.APP_VERSION || '1.0.0',
        port: parseInt(process.env.PORT || '3000'),
        env: (process.env.NODE_ENV as Config['app']['env']) || 'development',
      },
      database: {
        url: process.env.DATABASE_URL || 'postgresql://localhost:5432/myapp',
        maxConnections: parseInt(process.env.DB_MAX_CONNECTIONS || '20'),
        ssl: process.env.DB_SSL === 'true',
      },
      redis: {
        url: process.env.REDIS_URL || 'redis://localhost:6379',
        ttl: parseInt(process.env.REDIS_TTL || '3600'),
      },
      auth: {
        jwtSecret: process.env.JWT_SECRET || (() => {
          if (process.env.NODE_ENV === 'production') {
            throw new Error('JWT_SECRET is required in production');
          }
          return 'dev-secret-key';
        })(),
        jwtExpiry: process.env.JWT_EXPIRY || '24h',
        bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS || '12'),
      },
      external: {
        apiKey: process.env.EXTERNAL_API_KEY || '',
        webhookSecret: process.env.WEBHOOK_SECRET || '',
      },
    };

    // 설정 검증
    function validateConfig(config: Config): void {
      const requiredForProduction = [
        'JWT_SECRET',
        'DATABASE_URL',
        'EXTERNAL_API_KEY',
      ];

      if (config.app.env === 'production') {
        for (const key of requiredForProduction) {
          if (!process.env[key]) {
            throw new Error(`${key} is required in production environment`);
          }
        }
      }
    }

    validateConfig(config);
    export default config;

### 2. 보안 설정 패턴
.env.example:

    # 애플리케이션 설정
    APP_NAME=MyApp
    APP_VERSION=1.0.0
    PORT=3000
    NODE_ENV=development

    # 데이터베이스 설정
    DATABASE_URL=postgresql://username:password@localhost:5432/database
    DB_MAX_CONNECTIONS=20
    DB_SSL=false

    # Redis 설정
    REDIS_URL=redis://localhost:6379
    REDIS_TTL=3600

    # 인증 설정
    JWT_SECRET=your-super-secret-jwt-key-here
    JWT_EXPIRY=24h
    BCRYPT_ROUNDS=12

    # 외부 서비스
    EXTERNAL_API_KEY=your-api-key
    WEBHOOK_SECRET=your-webhook-secret

    # 로깅
    LOG_LEVEL=info
    LOG_FORMAT=json

    # 모니터링
    SENTRY_DSN=https://your-sentry-dsn
    NEW_RELIC_LICENSE_KEY=your-newrelic-key

## 보안 체크리스트
- [ ] 프로덕션 환경에서 DEBUG 모드 비활성화
- [ ] 강력한 시크릿 키 사용 (최소 32자)
- [ ] 민감한 정보를 환경 변수로 관리
- [ ] .env 파일을 .gitignore에 추가
- [ ] HTTPS 강제 사용 (프로덕션)
- [ ] CORS 설정 적절히 구성
- [ ] Rate limiting 적용

@environment-config-template.ts
@docker-compose.yml
@deployment-checklist.md

이 가이드를 통해 각 프레임워크의 특성에 맞는 MDC 규칙을 구성하여 개발 생산성과 코드 품질을 동시에 향상시킬 수 있습니다. 프로젝트에서 사용하는 기술 스택에 맞는 패턴을 선택하여 활용해보시기 바랍니다.




Leave a Comment