각 프레임워크마다 고유한 특성과 패턴이 있듯이, 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 규칙을 구성하여 개발 생산성과 코드 품질을 동시에 향상시킬 수 있습니다. 프로젝트에서 사용하는 기술 스택에 맞는 패턴을 선택하여 활용해보시기 바랍니다.