Skip to main content Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/affaan-m/ECC --skill frontend-patternsThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... More from this repository
GitHub repository operations, automation, and management. Issue triage, PR management, CI/CD operations, release management, and security monitoring using the gh CLI. Use when the user wants to manage GitHub issues, PRs, CI status, releases, contributors, stale items, or any GitHub operational task beyond simple git commands.
Related occupations SOC
Based on SOC occupation classification
name frontend-patterns description React, Next.js, 상태 관리, 성능 최적화 및 UI 모범 사례를 위한 프론트엔드 개발 패턴. origin ECC
프론트엔드 개발 패턴
React, Next.js 및 고성능 사용자 인터페이스를 위한 모던 프론트엔드 패턴.
활성화 시점
React 컴포넌트를 구축할 때 (합성, props, 렌더링)
상태를 관리할 때 (useState, useReducer, Zustand, Context)
데이터 페칭을 구현할 때 (SWR, React Query, server components)
성능을 최적화할 때 (메모이제이션, 가상화, 코드 분할)
폼을 다룰 때 (유효성 검사, 제어 입력, Zod 스키마)
클라이언트 사이드 라우팅과 네비게이션을 처리할 때
접근성 있고 반응형인 UI 패턴을 구축할 때
컴포넌트 패턴
상속보다 합성
interface CardProps {
children : React .ReactNode
variant ?: 'default' | 'outlined'
}
export function Card ({ children, variant = 'default' }: CardProps ) {
return <div className ={ `card card- ${ }`}> {children}
}
( ) {
}
( ) {
}
< >
</ >
variant
</div >
export
function
CardHeader
{ children }: { children: React.ReactNode }
return
<div className ="card-header" > {children}</div >
export
function
CardBody
{ children }: { children: React.ReactNode }
return
<div className ="card-body" > {children}</div >
Card
<CardHeader > Title</CardHeader >
<CardBody > Content</CardBody >
Card
Compound Components interface TabsContextValue {
activeTab : string
setActiveTab : (tab : string ) => void
}
const TabsContext = createContext<TabsContextValue | undefined >(undefined )
export function Tabs ({ children, defaultTab }: {
children: React.ReactNode
defaultTab: string
} ) {
const [activeTab, setActiveTab] = useState (defaultTab)
return (
<TabsContext.Provider value ={{ activeTab , setActiveTab }}>
{children}
</TabsContext.Provider >
)
}
export function TabList ({ children }: { children: React.ReactNode } ) {
return <div className ="tab-list" > {children}</div >
}
export function Tab ({ id, children }: { id: string , children: React.ReactNode } ) {
const context = useContext (TabsContext )
if (!context) throw new Error ('Tab must be used within Tabs' )
return (
<button
className ={context.activeTab === id ? 'active ' : ''}
onClick ={() => context.setActiveTab(id)}
>
{children}
</button >
)
}
<Tabs defaultTab="overview" >
<TabList >
<Tab id ="overview" > Overview</Tab >
<Tab id ="details" > Details</Tab >
</TabList >
</Tabs >
Render Props 패턴 interface DataLoaderProps <T> {
url : string
children : (data : T | null , loading : boolean , error : Error | null ) => React .ReactNode
}
export function DataLoader <T>({ url, children }: DataLoaderProps <T>) {
const [data, setData] = useState<T | null >(null )
const [loading, setLoading] = useState (true )
const [error, setError] = useState<Error | null >(null )
useEffect (() => {
fetch (url)
.then (res => res.json ())
.then (setData)
.catch (setError)
.finally (() => setLoading (false ))
}, [url])
return <> {children(data, loading, error)}</>
}
<DataLoader <Market []> url="/api/markets" >
{(markets, loading, error ) => {
if (loading) return <Spinner />
if (error) return <Error error ={error} />
return <MarketList markets ={markets!} />
}}
</DataLoader >
커스텀 Hook 패턴
상태 관리 Hook export function useToggle (initialValue = false ): [boolean , () => void ] {
const [value, setValue] = useState (initialValue)
const toggle = useCallback (() => {
setValue (v => !v)
}, [])
return [value, toggle]
}
const [isOpen, toggleOpen] = useToggle ()
비동기 데이터 페칭 Hook import { useCallback, useEffect, useRef, useState } from 'react'
interface UseQueryOptions <T> {
onSuccess ?: (data : T ) => void
onError ?: (error : Error ) => void
enabled ?: boolean
}
export function useQuery<T>(
key : string ,
fetcher : () => Promise <T>,
options ?: UseQueryOptions <T>
) {
const [data, setData] = useState<T | null >(null )
const [error, setError] = useState<Error | null >(null )
const [loading, setLoading] = useState (false )
const successRef = useRef (options?.onSuccess )
const errorRef = useRef (options?.onError )
const enabled = options?.enabled !== false
useEffect (() => {
successRef.current = options?.onSuccess
errorRef.current = options?.onError
}, [options?.onSuccess , options?.onError ])
const refetch = useCallback (async () => {
setLoading (true )
setError (null )
try {
const result = await fetcher ()
setData (result)
successRef.current ?.(result)
} catch (err) {
const error = err as Error
setError (error)
errorRef.current ?.(error)
} finally {
setLoading (false )
}
}, [fetcher])
useEffect (() => {
if (enabled) {
refetch ()
}
}, [key, enabled, refetch])
return { data, error, loading, refetch }
}
const { data : markets, loading, error, refetch } = useQuery (
'markets' ,
() => fetch ('/api/markets' ).then (r => r.json ()),
{
onSuccess : data => console .log ('Fetched' , data.length , 'markets' ),
onError : err => console .error ('Failed:' , err)
}
)
Debounce Hook export function useDebounce<T>(value : T, delay : number ): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect (() => {
const handler = setTimeout (() => {
setDebouncedValue (value)
}, delay)
return () => clearTimeout (handler)
}, [value, delay])
return debouncedValue
}
const [searchQuery, setSearchQuery] = useState ('' )
const debouncedQuery = useDebounce (searchQuery, 500 )
useEffect (() => {
if (debouncedQuery) {
performSearch (debouncedQuery)
}
}, [debouncedQuery])
상태 관리 패턴
Context + Reducer 패턴 interface State {
markets : Market []
selectedMarket : Market | null
loading : boolean
}
type Action =
| { type : 'SET_MARKETS' ; payload : Market [] }
| { type : 'SELECT_MARKET' ; payload : Market }
| { type : 'SET_LOADING' ; payload : boolean }
function reducer (state : State , action : Action ): State {
switch (action.type ) {
case 'SET_MARKETS' :
return { ...state, markets : action.payload }
case 'SELECT_MARKET' :
return { ...state, selectedMarket : action.payload }
case 'SET_LOADING' :
return { ...state, loading : action.payload }
default :
return state
}
}
const MarketContext = createContext<{
state : State
dispatch : Dispatch <Action >
} | undefined >(undefined )
export function MarketProvider ({ children }: { children: React.ReactNode } ) {
const [state, dispatch] = useReducer (reducer, {
markets : [],
selectedMarket : null ,
loading : false
})
return (
<MarketContext.Provider value ={{ state , dispatch }}>
{children}
</MarketContext.Provider >
)
}
export function useMarkets ( ) {
const context = useContext (MarketContext )
if (!context) throw new Error ('useMarkets must be used within MarketProvider' )
return context
}
성능 최적화
메모이제이션
const sortedMarkets = useMemo (() => {
return [...markets].sort ((a, b ) => b.volume - a.volume )
}, [markets])
const handleSearch = useCallback ((query : string ) => {
setSearchQuery (query)
}, [])
export const MarketCard = React .memo <MarketCardProps >(({ market } ) => {
return (
<div className ="market-card" >
<h3 > {market.name}</h3 >
<p > {market.description}</p >
</div >
)
})
코드 분할 및 지연 로딩 import { lazy, Suspense } from 'react'
const HeavyChart = lazy (() => import ('./HeavyChart' ))
const ThreeJsBackground = lazy (() => import ('./ThreeJsBackground' ))
export function Dashboard ( ) {
return (
<div >
<Suspense fallback ={ <ChartSkeleton /> }>
<HeavyChart data ={data} />
</Suspense >
<Suspense fallback ={null} >
<ThreeJsBackground />
</Suspense >
</div >
)
}
긴 리스트를 위한 가상화 import { useVirtualizer } from '@tanstack/react-virtual'
export function VirtualMarketList ({ markets }: { markets: Market[] } ) {
const parentRef = useRef<HTMLDivElement >(null )
const virtualizer = useVirtualizer ({
count : markets.length ,
getScrollElement : () => parentRef.current ,
estimateSize : () => 100 ,
overscan : 5
})
return (
<div ref ={parentRef} style ={{ height: '600px ', overflow: 'auto ' }}>
<div
style ={{
height: `${virtualizer.getTotalSize ()}px `,
position: 'relative '
}}
>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key ={virtualRow.index}
style ={{
position: 'absolute ',
top: 0 ,
left: 0 ,
width: '100 %',
height: `${virtualRow.size }px `,
transform: `translateY (${virtualRow.start }px )`
}}
>
<MarketCard market ={markets[virtualRow.index]} />
</div >
))}
</div >
</div >
)
}
폼 처리 패턴
유효성 검사가 포함된 제어 폼 interface FormData {
name : string
description : string
endDate : string
}
interface FormErrors {
name ?: string
description ?: string
endDate ?: string
}
export function CreateMarketForm ( ) {
const [formData, setFormData] = useState<FormData >({
name : '' ,
description : '' ,
endDate : ''
})
const [errors, setErrors] = useState<FormErrors >({})
const validate = (): boolean => {
const newErrors : FormErrors = {}
if (!formData.name .trim ()) {
newErrors.name = 'Name is required'
} else if (formData.name .length > 200 ) {
newErrors.name = 'Name must be under 200 characters'
}
if (!formData.description .trim ()) {
newErrors.description = 'Description is required'
}
if (!formData.endDate ) {
newErrors.endDate = 'End date is required'
}
setErrors (newErrors)
return Object .keys (newErrors).length === 0
}
const handleSubmit = async (e : React .FormEvent ) => {
e.preventDefault ()
if (!validate ()) return
try {
await createMarket (formData)
} catch (error) {
}
}
return (
<form onSubmit ={handleSubmit} >
<input
value ={formData.name}
onChange ={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Market name"
/>
{errors.name && <span className ="error" > {errors.name}</span > }
{/* Other fields */}
<button type ="submit" > Create Market</button >
</form >
)
}
Error Boundary 패턴 interface ErrorBoundaryState {
hasError : boolean
error : Error | null
}
export class ErrorBoundary extends React.Component <
{ children : React .ReactNode },
ErrorBoundaryState
> {
state : ErrorBoundaryState = {
hasError : false ,
error : null
}
static getDerivedStateFromError (error : Error ): ErrorBoundaryState {
return { hasError : true , error }
}
componentDidCatch (error : Error , errorInfo : React .ErrorInfo ) {
console .error ('Error boundary caught:' , error, errorInfo)
}
render ( ) {
if (this .state .hasError ) {
return (
<div className ="error-fallback" >
<h2 > Something went wrong</h2 >
<p > {this.state.error?.message}</p >
<button onClick ={() => this.setState({ hasError: false })}>
Try again
</button >
</div >
)
}
return this .props .children
}
}
<ErrorBoundary >
<App />
</ErrorBoundary >
애니메이션 패턴
Framer Motion 애니메이션 import { motion, AnimatePresence } from 'framer-motion'
export function AnimatedMarketList ({ markets }: { markets: Market[] } ) {
return (
<AnimatePresence >
{markets.map(market => (
<motion.div
key ={market.id}
initial ={{ opacity: 0 , y: 20 }}
animate ={{ opacity: 1 , y: 0 }}
exit ={{ opacity: 0 , y: -20 }}
transition ={{ duration: 0.3 }}
>
<MarketCard market ={market} />
</motion.div >
))}
</AnimatePresence >
)
}
export function Modal ({ isOpen, onClose, children }: ModalProps ) {
return (
<AnimatePresence >
{isOpen && (
<>
<motion.div
className ="modal-overlay"
initial ={{ opacity: 0 }}
animate ={{ opacity: 1 }}
exit ={{ opacity: 0 }}
onClick ={onClose}
/>
<motion.div
className ="modal-content"
initial ={{ opacity: 0 , scale: 0.9 , y: 20 }}
animate ={{ opacity: 1 , scale: 1 , y: 0 }}
exit ={{ opacity: 0 , scale: 0.9 , y: 20 }}
>
{children}
</motion.div >
</>
)}
</AnimatePresence >
)
}
접근성 패턴
키보드 네비게이션 export function Dropdown ({ options, onSelect }: DropdownProps ) {
const [isOpen, setIsOpen] = useState (false )
const [activeIndex, setActiveIndex] = useState (0 )
const handleKeyDown = (e : React .KeyboardEvent ) => {
switch (e.key ) {
case 'ArrowDown' :
e.preventDefault ()
setActiveIndex (i => Math .min (i + 1 , options.length - 1 ))
break
case 'ArrowUp' :
e.preventDefault ()
setActiveIndex (i => Math .max (i - 1 , 0 ))
break
case 'Enter' :
e.preventDefault ()
onSelect (options[activeIndex])
setIsOpen (false )
break
case 'Escape' :
setIsOpen (false )
break
}
}
return (
<div
role ="combobox"
aria-expanded ={isOpen}
aria-haspopup ="listbox"
onKeyDown ={handleKeyDown}
>
{/* Dropdown implementation */}
</div >
)
}
포커스 관리 export function Modal ({ isOpen, onClose, children }: ModalProps ) {
const modalRef = useRef<HTMLDivElement >(null )
const previousFocusRef = useRef<HTMLElement | null >(null )
useEffect (() => {
if (isOpen) {
previousFocusRef.current = document .activeElement as HTMLElement
modalRef.current ?.focus ()
} else {
previousFocusRef.current ?.focus ()
}
}, [isOpen])
return isOpen ? (
<div
ref ={modalRef}
role ="dialog"
aria-modal ="true"
tabIndex ={-1}
onKeyDown ={e => e.key === 'Escape' && onClose()}
>
{children}
</div >
) : null
}
기억하세요 : 모던 프론트엔드 패턴은 유지보수 가능하고 고성능인 사용자 인터페이스를 가능하게 합니다. 프로젝트 복잡도에 맞는 패턴을 선택하세요.