بنقرة واحدة
vercel-best-practice
Vercel Engineering의 React/Next.js 성능 최적화 가이드라인을 적용하여 렌더링 성능, 번들 크기, API 호출을 최적화합니다.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Vercel Engineering의 React/Next.js 성능 최적화 가이드라인을 적용하여 렌더링 성능, 번들 크기, API 호출을 최적화합니다.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
프로젝트의 workthrough 문서들을 참조하여 과거 개발 작업 기록과 패턴을 이해하고 적용합니다.
Gemini-Claude Engineering Loop 방법론을 적용하여 계획, 검토, 구현의 3단계 프로세스로 안전하고 품질 높은 코드를 작성합니다.
PostgreSQL 데이터베이스 백업 및 복원. Docker/로컬 환경 지원.
CSV에서 worklog를 DB로 증분 마이그레이션. User/Project/WorkType을 의미론적으로 추론하여 매핑. 날짜 범위 지정 가능.
| name | vercel-best-practice |
| description | Vercel Engineering의 React/Next.js 성능 최적화 가이드라인을 적용하여 렌더링 성능, 번들 크기, API 호출을 최적화합니다. |
| disable-model-invocation | false |
Vercel Engineering의 React/Next.js 성능 최적화 가이드라인을 Edwards 프로젝트에 적용합니다.
rerender-memoReact.memo를 사용하여 불필요한 리렌더링 방지
// ✅ Good: Memoized component
const InlineEditableRow = memo<Props>(({ project, onUpdate }) => {
return <tr>...</tr>;
});
// ❌ Bad: 매번 리렌더
export function InlineEditableRow({ project, onUpdate }: Props) {
return <tr>...</tr>;
}
적용 대상:
rerender-functional-setstatesetState에서 변경 없으면 스킵
// ✅ Good: Skip if no change
setColumnWidths(prev => {
if (prev[column] === newWidth) return prev;
return { ...prev, [column]: newWidth };
});
// ❌ Bad: 항상 새 객체 생성
setColumnWidths({ ...columnWidths, [column]: newWidth });
rerender-dependenciesuseEffect 의존성을 primitive 값으로 최적화
// ✅ Good: Memoized filtered list
const filteredProductLines = useMemo(() => {
return selectedBusinessUnitId
? productLines.filter(pl => pl.business_unit_id === selectedBusinessUnitId)
: productLines;
}, [productLines, selectedBusinessUnitId]);
// ✅ Good: Primitive dependency
const valueExistsInFiltered = useMemo(() => {
return value ? filteredProductLines.some(pl => pl.id === value) : true;
}, [value, filteredProductLines]);
useEffect(() => {
if (value && !valueExistsInFiltered) {
onChange('');
}
}, [value, valueExistsInFiltered, onChange]);
rendering-hoist-jsx정적 JSX를 컴포넌트 외부로 호이스팅
// ✅ Good: Static JSX hoisted
const SortIconDefault = <ArrowUpDown className="h-3 w-3 ml-1 text-gray-400" />;
const SortIconAsc = <ArrowUp className="h-3 w-3 ml-1 text-gray-700" />;
const SortIconDesc = <ArrowDown className="h-3 w-3 ml-1 text-gray-700" />;
export function ProjectInlineTable() {
return (
<th>
{sortField === 'name' && sortDirection === 'asc' ? SortIconAsc : SortIconDefault}
</th>
);
}
// ❌ Bad: 매 렌더마다 재생성
export function ProjectInlineTable() {
return (
<th>
{sortField === 'name' && sortDirection === 'asc'
? <ArrowUp className="h-3 w-3 ml-1 text-gray-700" />
: <ArrowUpDown className="h-3 w-3 ml-1 text-gray-400" />
}
</th>
);
}
적용 대상:
js-index-maps배열.find() 대신 Map을 사용하여 O(1) 조회
// ✅ Good: Map for O(1) lookup
const FUNDING_ENTITY_MAP = new Map(
FUNDING_ENTITY_OPTIONS.map(o => [o.value, o.label])
);
// 사용
{FUNDING_ENTITY_MAP.get(project.funding_entity_id ?? '') || EmptyPlaceholder}
// ❌ Bad: O(n) find each render
const option = FUNDING_ENTITY_OPTIONS.find(o => o.value === project.funding_entity_id);
{option?.label || '-'}
js-combine-iterations여러 순회를 하나로 통합
// ✅ Good: Single-pass filter + sort
const sortedProjects = useMemo(() => {
let result: Project[] = [];
for (let i = 0; i < projects.length; i++) {
const p = projects[i];
if (hasCategories && (!p.category || !selectedCategories.includes(p.category))) continue;
if (hasStatuses && !selectedStatuses.includes(p.status)) continue;
result.push(p);
}
if (sortField && sortDirection) {
result.sort((a, b) => { ... });
}
return result;
}, [projects, selectedCategories, selectedStatuses, sortField, sortDirection]);
// ❌ Bad: Multiple iterations
const filtered = projects.filter(p => selectedCategories.includes(p.category));
const sorted = filtered.sort((a, b) => { ... });
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 데이터를 5분간 fresh 상태로 유지
staleTime: 5 * 60 * 1000,
// 미사용 데이터를 30분간 캐시에 유지
gcTime: 30 * 60 * 1000,
// 윈도우 포커스 시 자동 refetch 비활성화
refetchOnWindowFocus: false,
// 실패 시 1회만 재시도
retry: 1,
retryDelay: 1000,
},
},
});
// Job Position 같은 참조 데이터는 30분 staleTime
const { data: jobPositions } = useQuery({
queryKey: ['jobPositions'],
queryFn: () => apiClient.get('/job-positions').then(r => r.data),
staleTime: 30 * 60 * 1000, // 30분
});
/* 화면 밖 콘텐츠 렌더링 지연 */
tbody.virtualized {
content-visibility: auto;
contain-intrinsic-size: auto 300px;
}
적용 대상: 대량 데이터 테이블 (500+ 행)
// ✅ Good: Lazy loading
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const ReportsPage = lazy(() => import('./pages/ReportsPage'));
// Suspense로 감싸서 로딩 상태 처리
<Route path="/" element={
<Suspense fallback={<PageLoader />}>
<DashboardPage />
</Suspense>
} />
효과: 초기 번들 크기 61% 감소 (1,127KB → 437KB)
✓ built in 4.10s
ProjectsPage: 41.37 kB → 30.94 kB (-25% 감소)
workthrough/2026-01-28_14_30_vercel-react-best-practices.md - 초기 적용workthrough/2026-01-31_19_30_vercel-best-practices-optimization.md - 인라인 테이블 최적화workthrough/2026-01-28_11_00_route-level-code-splitting.md - Code splitting