Manus에서 모든 스킬 실행
원클릭으로
원클릭으로
원클릭으로 Manus에서 모든 스킬 실행
시작하기$pwd:
vercel-best-practice
// Vercel Engineering의 React/Next.js 성능 최적화 가이드라인을 적용하여 렌더링 성능, 번들 크기, API 호출을 최적화합니다.
$ git log --oneline --stat
stars:0
forks:0
updated:2026년 2월 1일 13:25
SKILL.md
// Vercel Engineering의 React/Next.js 성능 최적화 가이드라인을 적용하여 렌더링 성능, 번들 크기, API 호출을 최적화합니다.
| 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프로젝트의 workthrough 문서들을 참조하여 과거 개발 작업 기록과 패턴을 이해하고 적용합니다.
Gemini-Claude Engineering Loop 방법론을 적용하여 계획, 검토, 구현의 3단계 프로세스로 안전하고 품질 높은 코드를 작성합니다.
PostgreSQL 데이터베이스 백업 및 복원. Docker/로컬 환경 지원.
CSV에서 worklog를 DB로 증분 마이그레이션. User/Project/WorkType을 의미론적으로 추론하여 매핑. 날짜 범위 지정 가능.