用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/cyanluna-git/edwards.reousrce.management --skill vercel-best-practice命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| 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