Communitygithub.com

marnixGeerkens/rocketlauncher-claude-code

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

rocketlauncher-claude-code란 무엇인가요?

이 스킬은 React 및 Next.js 기반 프론트엔드 개발에 필요한 핵심 패턴과 모범 사례를 종합적으로 제공합니다. 컴포넌트 합성, 복합 컴포넌트, Render Props, 커스텀 훅(상태 관리, 비동기 데이터 페칭, 디바운스) 등을 포함한 고급 컴포넌트 패턴을 다룹니다. 또한 Context + Reducer 패턴을 사용한 상태 관리, 메모이제이션(useMemo, useCallback, React.memo), 코드 스플리팅(React.lazy, Suspense), 가상화(@tanstack/react-virtual) 등의 성능 최적화 기법을 소개합니다. 폼 핸들링, 오류 경계(Error Boundary), Framer Motion 애니메이션, 키보드 네비게이션 및 포커스 관리를 포함한 접근성 패턴까지 포괄하여 생산적이고 유지보수 가능한 프론트엔드 구축을 지원합니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/marnixGeerkens/rocketlauncher-claude-code/tree/HEAD/components/skills/development/cc-skill-frontend-patterns

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

프론트엔드 개발 패턴

React, Next.js 및 성능 좋은 사용자 인터페이스를 위한 현대적인 프론트엔드 패턴입니다.

컴포넌트 패턴

합성(Composition)을 통한 상속(Inheritance) 대체

// ✅ 좋은 예: 컴포넌트 합성
interface 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>
}

// 사용 예
<Card>
  <CardHeader>제목</CardHeader>
  <CardBody>내용</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은 Tabs 내부에서 사용되어야 합니다.')

  return (
    <button
      className={context.activeTab === id ? 'active' : ''}
      onClick={() => context.setActiveTab(id)}
    >
      {children}
    </button>
  )
}

// 사용 예
<Tabs defaultTab="overview">
  <TabList>
    <Tab id="overview">개요</Tab>
    <Tab id="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>

커스텀 훅 패턴

상태 관리 훅

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()

비동기 데이터 페칭 훅

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 refetch = useCallback(async () => {
    setLoading(true)
    setError(null)

    try {
      const result = await fetcher()
      setData(result)
      options?.onSuccess?.(result)
    } catch (err) {
      const error = err as Error
      setError(error)
      options?.onError?.(error)
    } finally {
      setLoading(false)
    }
  }, [fetcher, options])

  useEffect(() => {
    if (options?.enabled !== false) {
      refetch()
    }
  }, [key, refetch, options?.enabled])

  return { data, error, loading, refetch }
}

// 사용 예
const { data: markets, loading, error, refetch } = useQuery(
  'markets',
  () => fetch('/api/markets').then(r => r.json()),
  {
    onSuccess: data => console.log('가져온 마켓 수:', data.length),
    onError: err => console.error('실패:', err)
  }
)

디바운스 훅

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는 MarketProvider 내부에서 사용되어야 합니다.')
  return context
}

성능 최적화

메모이제이션

// ✅ 값비싼 연산을 위한 useMemo
const sortedMarkets = useMemo(() => {
  return markets.sort((a, b) => b.volume - a.volume)
}, [markets])

// ✅ 자식에게 전달되는 함수를 위한 useCallback
const handleSearch = useCallback((query: string) => {
  setSearchQuery(query)
}, [])

// ✅ 순수 컴포넌트를 위한 React.memo
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 = '이름은 필수입니다.'
    } else if (formData.name.length > 200) {
      newErrors.name = '이름은 200자 미만이어야 합니다.'
    }

    if (!formData.description.trim()) {
      newErrors.description = '설명은 필수입니다.'
    }

    if (!formData.endDate) {
      newErrors.endDate = '종료일은 필수입니다.'
    }

    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="마켓 이름"
      />
      {errors.name && <span className="error">{errors.name}</span>}

      {/* 다른 필드 */}

      <button type="submit">마켓 생성</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, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>문제가 발생했습니다</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false })}>
            다시 시도
          </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}
    >
      {/* 드롭다운 구현 */}
    </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
}

상태 끌어올리기 & 내리기 패턴

상태 끌어올리기

// 부모 컴포넌트가 여러 자식 간에 공유 상태를 관리
interface MarketSearchProps {
  filters: FilterState
  onFiltersChange: (filters: FilterState) => void
}

function MarketSearch({ filters, onFiltersChange }: MarketSearchProps) {
  return (
    <div>
      <SearchInput
        value={filters.query}
        onChange={query => onFiltersChange({ ...filters, query })}
      />
      <CategoryFilter
        selected={filters.category}
        onChange={category => onFiltersChange({ ...filters, category })}
      />
    </div>
  )
}

// 사용 예
function MarketsPage() {
  const [filters, setFilters] = useState<FilterState>({
    query: '',
    category: 'all'
  })

  return (
    <div>
      <MarketSearch filters={filters} onFiltersChange={setFilters} />
      <MarketList filters={filters} />
    </div>
  )
}

상태 내리기

function MarketsPage() {
  const { state, dispatch } = useMarkets()

  return (
    <div>
      {/* 자식에게 필요한 상태와 액션만 전달 */}
      <MarketList
        markets={state.markets}
        loading={state.loading}
        onSelect={market => dispatch({ type: 'SELECT_MARKET', payload: market })}
      />
      <MarketDetail
        market={state.selectedMarket}
        onClose={() => dispatch({ type: 'SELECT_MARKET', payload: null })}
      />
    </div>
  )
}

Individual skills in this repo

This repo contains 13 individual skills — each has its own dedicated page.

marnixGeerkens/rocketlauncher-claude-code

When the user wants to optimize, improve, or increase conversions on any marketing page — including homepage, landing pages, pricing pages, feature pages, or blog posts. Also use when the user says "CRO," "conversion rate optimization," "this page isn't converting," "improve conversions," or "why isn't this page working." For signup/registration flows, see signup-flow-cro. For post-signup activation, see onboarding-cro. For forms outside of signup, see form-cro. For popups/modals, see popup-cro.

marnixGeerkens/rocketlauncher-claude-code

Balance visual design with ATS compatibility for creative roles. Use when the user is in design, art, writing, marketing creative, or any visual/creative field and needs a resume that looks distinctive but still parses.

marnixGeerkens/rocketlauncher-claude-code

Transform resume bullets into detailed portfolio case studies with context, action, and outcome. Use when the user mentions portfolio, case study, project write-up, or expanding a resume bullet into a longer narrative.

marnixGeerkens/rocketlauncher-claude-code

Expert in building portfolios that actually land jobs and clients - not just showing work, but creating memorable experiences. Covers developer portfolios, designer portfolios, creative portfolios, and portfolios that convert visitors into opportunities. Use when: portfolio, personal website, showcase work, developer portfolio, designer portfolio.

marnixGeerkens/rocketlauncher-claude-code

Tailwind CSS v4 principles. CSS-first configuration, container queries, modern patterns, design token architecture.

marnixGeerkens/rocketlauncher-claude-code

Frontend development guidelines for React/TypeScript applications. Modern patterns including Suspense, lazy loading, useSuspenseQuery, file organization with features directory, MUI v7 styling, TanStack Router, performance optimization, and TypeScript best practices. Use when creating components, pages, features, fetching data, styling, routing, or working with frontend code.

marnixGeerkens/rocketlauncher-claude-code

Next.js App Router principles. Server Components, data fetching, routing patterns.

marnixGeerkens/rocketlauncher-claude-code

Expert integration of Supabase Auth with Next.js App Router Use when: supabase auth next, authentication next.js, login supabase, auth middleware, protected route.

marnixGeerkens/rocketlauncher-claude-code

Comprehensive frontend development skill for building modern, performant web applications using ReactJS, NextJS, TypeScript, Tailwind CSS. Includes component scaffolding, performance optimization, bundle analysis, and UI best practices. Use when developing frontend features, optimizing performance, implementing UI/UX designs, managing state, or reviewing frontend code.

marnixGeerkens/rocketlauncher-claude-code

Create API handoff documentation for frontend developers. Use when backend work is complete and needs to be documented for frontend integration, or user says 'create handoff', 'document API', 'frontend handoff', or 'API documentation'.

marnixGeerkens/rocketlauncher-claude-code

Document frontend data needs for backend developers. Use when frontend needs to communicate API requirements to backend, or user says 'backend requirements', 'what data do I need', 'API requirements', or is describing data needs for a UI.

marnixGeerkens/rocketlauncher-claude-code

Comprehensive Python library for astronomy and astrophysics. This skill should be used when working with astronomical data including celestial coordinates, physical units, FITS files, cosmological calculations, time systems, tables, world coordinate systems (WCS), and astronomical data analysis. Use when tasks involve coordinate transformations, unit conversions, FITS file manipulation, cosmological distance calculations, time scale conversions, or astronomical data processing.

marnixGeerkens/rocketlauncher-claude-code

Comprehensive patterns for Next.js 14+ App Router architecture, Server Components, and modern full-stack React development.

관련 스킬