Frontend Patterns

affaan-m/ECC/docs/ko-KR/skills/frontend-patterns

作者 affaan-mef648e01899ba3e8dc6371642deaaf64b4477775无许可证275K 个星标收录于 2026年10月9日更新于 2026年10月9日仓库4天前更新

React, Next.js, 상태 관리, 성능 최적화 및 UI 모범 사례를 위한 프론트엔드 개발 패턴.

AI 生成的概览

用于构建 React 和 Next.js 前端的参考模式:组件、Hook、状态、性能、表单与无障碍。

功能
提供现代前端开发模式的目录,并配有 React 和 Next.js 的 TypeScript 代码示例。内容涵盖组件组合、复合组件、render props、自定义 Hook、基于 Context 与 reducer 的状态管理、记忆化、代码分割、长列表虚拟化、表单校验、错误边界、动画以及无障碍。它属于教学性参考资料,而不是生成文件的工具。
适用场景
在实现 React 组件、管理客户端状态、添加数据获取、优化渲染性能、处理表单,或构建无障碍且响应式的界面时使用。适合希望在前端编码时参照具体模式示例的开发者。
运行要求
不包含脚本或工具,仅为说明与代码示例。示例涉及 React、Next.js、TypeScript,以及 Zustand、SWR、React Query、Zod、@tanstack/react-virtual、framer-motion 等可选库。

프론트엔드 개발 패턴

React, Next.js 및 고성능 사용자 인터페이스를 위한 모던 프론트엔드 패턴.

활성화 시점

  • React 컴포넌트를 구축할 때 (합성, props, 렌더링)
  • 상태를 관리할 때 (useState, useReducer, Zustand, Context)
  • 데이터 페칭을 구현할 때 (SWR, React Query, server components)
  • 성능을 최적화할 때 (메모이제이션, 가상화, 코드 분할)
  • 폼을 다룰 때 (유효성 검사, 제어 입력, Zod 스키마)
  • 클라이언트 사이드 라우팅과 네비게이션을 처리할 때
  • 접근성 있고 반응형인 UI 패턴을 구축할 때

컴포넌트 패턴

상속보다 합성

typescript
// PASS: GOOD: Component compositioninterface CardProps {  children: React.ReactNode  variant?: 'default' | 'outlined'}
export function Card({ children, variant = 'default' }: CardProps) {  return <div className={`card card-${variant}`}>{children}</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>}
// Usage<Card>  <CardHeader>Title</CardHeader>  <CardBody>Content</CardBody></Card>

Compound Components

typescript
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>  )}
// Usage<Tabs defaultTab="overview">  <TabList>    <Tab id="overview">Overview</Tab>    <Tab id="details">Details</Tab>  </TabList></Tabs>

Render Props 패턴

typescript
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)}</>}
// Usage<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

typescript
export function useToggle(initialValue = false): [boolean, () => void] {  const [value, setValue] = useState(initialValue)
  const toggle = useCallback(() => {    setValue(v => !v)  }, [])
  return [value, toggle]}
// Usageconst [isOpen, toggleOpen] = useToggle()

비동기 데이터 페칭 Hook

typescript
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 }}
// Usageconst { 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

typescript
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}
// Usageconst [searchQuery, setSearchQuery] = useState('')const debouncedQuery = useDebounce(searchQuery, 500)
useEffect(() => {  if (debouncedQuery) {    performSearch(debouncedQuery)  }}, [debouncedQuery])

상태 관리 패턴

Context + Reducer 패턴

typescript
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}

성능 최적화

메모이제이션

typescript
// PASS: useMemo for expensive computationsconst sortedMarkets = useMemo(() => {  return [...markets].sort((a, b) => b.volume - a.volume)}, [markets])
// PASS: useCallback for functions passed to childrenconst handleSearch = useCallback((query: string) => {  setSearchQuery(query)}, [])
// PASS: React.memo for pure componentsexport const MarketCard = React.memo<MarketCardProps>(({ market }) => {  return (    <div className="market-card">      <h3>{market.name}</h3>      <p>{market.description}</p>    </div>  )})

코드 분할 및 지연 로딩

typescript
import { lazy, Suspense } from 'react'
// PASS: Lazy load heavy componentsconst 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>  )}

긴 리스트를 위한 가상화

typescript
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,  // Estimated row height    overscan: 5  // Extra items to render  })
  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>  )}

폼 처리 패턴

유효성 검사가 포함된 제어 폼

typescript
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)      // Success handling    } catch (error) {      // Error handling    }  }
  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 패턴

typescript
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  }}
// Usage<ErrorBoundary>  <App /></ErrorBoundary>

애니메이션 패턴

Framer Motion 애니메이션

typescript
import { motion, AnimatePresence } from 'framer-motion'
// PASS: List animationsexport 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>  )}
// PASS: Modal animationsexport 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>  )}

접근성 패턴

키보드 네비게이션

typescript
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>  )}

포커스 관리

typescript
export function Modal({ isOpen, onClose, children }: ModalProps) {  const modalRef = useRef<HTMLDivElement>(null)  const previousFocusRef = useRef<HTMLElement | null>(null)
  useEffect(() => {    if (isOpen) {      // Save currently focused element      previousFocusRef.current = document.activeElement as HTMLElement
      // Focus modal      modalRef.current?.focus()    } else {      // Restore focus when closing      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}

기억하세요: 모던 프론트엔드 패턴은 유지보수 가능하고 고성능인 사용자 인터페이스를 가능하게 합니다. 프로젝트 복잡도에 맞는 패턴을 선택하세요.

来源与署名

来源:affaan-m/ECC位于docs/ko-KR/skills/frontend-patterns提交ef648e0

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架