React 치트시트

React 18+ 핵심 패턴과 훅, Next.js App Router까지 한눈에 정리한 빠른 참조 가이드

🧩

컴포넌트 기본

7 items

Function Component

기본 함수형 컴포넌트 선언

function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}!</h1>
}

Props with Interface

인터페이스를 사용한 props 타입 정의

interface CardProps {
  title: string
  subtitle?: string
  onClick: () => void
}

function Card({ title, subtitle, onClick }: CardProps) {
  return (
    <div onClick={onClick}>
      <h2>{title}</h2>
      {subtitle && <p>{subtitle}</p>}
    </div>
  )
}
Tip: 선택적 props에는 ?를 사용하고, 구조 분해 할당으로 기본값을 설정하세요.

Children Prop

children을 사용한 레이아웃 래퍼

function Container({ children }: { children: React.ReactNode }) {
  return <div className="max-w-4xl mx-auto p-4">{children}</div>
}

// Usage
<Container>
  <h1>Title</h1>
  <p>Content goes here</p>
</Container>

Conditional Rendering

조건부 렌더링 패턴

// && short-circuit
{isLoggedIn && <Dashboard />}

// Ternary
{isAdmin ? <AdminPanel /> : <UserPanel />}

// Early return
function Profile({ user }: { user: User | null }) {
  if (!user) return <p>Please log in</p>
  return <h1>{user.name}</h1>
}

List Rendering (map + key)

배열을 컴포넌트 리스트로 렌더링

const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]

function UserList() {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}
Tip: key에 배열 인덱스 대신 고유 ID를 사용하세요. 인덱스는 순서 변경 시 버그를 유발합니다.

Fragments

불필요한 DOM 노드 없이 여러 요소 반환

// Short syntax
function Item() {
  return (
    <>
      <dt>Name</dt>
      <dd>React</dd>
    </>
  )
}

// With key (e.g., in a map)
items.map(item => (
  <Fragment key={item.id}>
    <dt>{item.label}</dt>
    <dd>{item.value}</dd>
  </Fragment>
))

Default Props

props 기본값 설정 방법

interface ButtonProps {
  variant?: 'primary' | 'secondary'
  size?: 'sm' | 'md' | 'lg'
  children: React.ReactNode
}

function Button({ variant = 'primary', size = 'md', children }: ButtonProps) {
  return <button className={`btn-${variant} btn-${size}`}>{children}</button>
}
📌

Hooks - 상태 관리

6 items

useState

컴포넌트 로컬 상태 관리

const [count, setCount] = useState(0)
const [user, setUser] = useState<User | null>(null)
const [items, setItems] = useState<string[]>([])

// Update
setCount(prev => prev + 1)
setUser({ name: 'Alice', age: 30 })
setItems(prev => [...prev, 'new item'])

useReducer

복잡한 상태 로직을 리듀서로 관리

type State = { count: number; step: number }
type Action = { type: 'increment' } | { type: 'setStep'; step: number }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + state.step }
    case 'setStep': return { ...state, step: action.step }
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 })
dispatch({ type: 'increment' })
Tip: 상태 업데이트가 이전 상태에 의존하거나 여러 하위 값이 있을 때 useReducer를 사용하세요.

Lazy Initialization

비용이 큰 초기값 계산을 지연

// Bad: runs every render
const [data, setData] = useState(parseLocalStorage())

// Good: runs only once
const [data, setData] = useState(() => parseLocalStorage())

// With useReducer
const [state, dispatch] = useReducer(reducer, undefined, () => {
  return JSON.parse(localStorage.getItem('state') ?? '{}')
})

setState with Updater Function

이전 상태를 기반으로 안전하게 업데이트

// Wrong: may use stale state
const handleClick = () => {
  setCount(count + 1)
  setCount(count + 1) // still +1, not +2
}

// Correct: always uses latest state
const handleClick = () => {
  setCount(prev => prev + 1)
  setCount(prev => prev + 1) // +2 as expected
}
Tip: 이벤트 핸들러에서 같은 상태를 여러 번 업데이트할 때는 반드시 updater 함수를 사용하세요.

Object & Array State Updates

불변성을 유지하며 객체/배열 상태 업데이트

// Object: spread and override
setUser(prev => ({ ...prev, name: 'Bob' }))

// Array: add
setItems(prev => [...prev, newItem])

// Array: remove
setItems(prev => prev.filter(item => item.id !== targetId))

// Array: update one item
setItems(prev => prev.map(item =>
  item.id === targetId ? { ...item, done: true } : item
))

Lifting State Up

부모 컴포넌트에서 공유 상태 관리

function Parent() {
  const [selected, setSelected] = useState<string | null>(null)

  return (
    <>
      <Sidebar selected={selected} onSelect={setSelected} />
      <Content selectedId={selected} />
    </>
  )
}
🔄

Hooks - 이펙트 & Ref

6 items

useEffect — Basic

컴포넌트 마운트, 업데이트, 언마운트 시 사이드 이펙트 실행

// Run on every render
useEffect(() => { document.title = `Count: ${count}` })

// Run once on mount
useEffect(() => { fetchData() }, [])

// Run when deps change
useEffect(() => { fetchUser(userId) }, [userId])

useEffect — Cleanup

이벤트 리스너, 타이머, 구독 등의 정리

useEffect(() => {
  const controller = new AbortController()
  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(setData)

  return () => controller.abort()
}, [])

useEffect(() => {
  const id = setInterval(() => setTime(Date.now()), 1000)
  return () => clearInterval(id)
}, [])
Tip: cleanup 함수를 반환하여 메모리 누수를 방지하세요. 특히 WebSocket, EventSource, setInterval에서 중요합니다.

useLayoutEffect

DOM 측정이 필요할 때 브라우저 페인트 전에 실행

import { useLayoutEffect, useRef, useState } from 'react'

function Tooltip({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null)
  const [height, setHeight] = useState(0)

  useLayoutEffect(() => {
    if (ref.current) setHeight(ref.current.getBoundingClientRect().height)
  }, [children])

  return <div ref={ref} style={{ marginTop: -height }}>{children}</div>
}

useRef — DOM Access

DOM 요소에 직접 접근

function SearchInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  const focus = () => inputRef.current?.focus()

  return (
    <>
      <input ref={inputRef} placeholder="Search..." />
      <button onClick={focus}>Focus</button>
    </>
  )
}

useRef — Mutable Value

렌더링을 트리거하지 않는 변경 가능한 값 저장

function Timer() {
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)

  const start = () => {
    intervalRef.current = setInterval(() => console.log('tick'), 1000)
  }

  const stop = () => {
    if (intervalRef.current) clearInterval(intervalRef.current)
  }

  return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>
}
Tip: useRef는 리렌더링 없이 값을 유지합니다. 이전 값 추적, 타이머 ID 저장 등에 활용하세요.

forwardRef + useImperativeHandle

부모 컴포넌트에 자식의 메서드를 노출

import { forwardRef, useImperativeHandle, useRef } from 'react'

interface ModalHandle { open: () => void; close: () => void }

const Modal = forwardRef<ModalHandle, { children: React.ReactNode }>(
  ({ children }, ref) => {
    const [visible, setVisible] = useState(false)

    useImperativeHandle(ref, () => ({
      open: () => setVisible(true),
      close: () => setVisible(false),
    }))

    if (!visible) return null
    return <div className="modal">{children}</div>
  }
)

// Parent
const modalRef = useRef<ModalHandle>(null)
<button onClick={() => modalRef.current?.open()}>Open</button>
<Modal ref={modalRef}><p>Content</p></Modal>

Hooks - 성능 최적화

6 items

useMemo

비용이 큰 계산 결과를 메모이제이션

const sortedItems = useMemo(() => {
  return items
    .filter(item => item.status === 'active')
    .sort((a, b) => a.name.localeCompare(b.name))
}, [items])

// Also useful for referential equality
const config = useMemo(() => ({ theme, locale }), [theme, locale])
Tip: useMemo는 최적화 도구이지, 보장이 아닙니다. 실제 성능 문제가 있을 때만 사용하세요.

useCallback

함수 참조를 메모이제이션하여 불필요한 리렌더링 방지

const handleSearch = useCallback((query: string) => {
  const results = data.filter(item =>
    item.name.toLowerCase().includes(query.toLowerCase())
  )
  setResults(results)
}, [data])

// Common with React.memo children
<SearchInput onSearch={handleSearch} />

React.memo

props가 변경되지 않으면 리렌더링 건너뛰기

const ExpensiveList = React.memo(function ExpensiveList({
  items,
  onSelect,
}: {
  items: Item[]
  onSelect: (id: string) => void
}) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onSelect(item.id)}>{item.name}</li>
      ))}
    </ul>
  )
})

// Custom comparison
const Chart = React.memo(ChartComponent, (prev, next) => {
  return prev.data.length === next.data.length
})

useTransition

긴급하지 않은 상태 업데이트를 지연시켜 UI 반응성 유지

const [isPending, startTransition] = useTransition()
const [query, setQuery] = useState('')
const [results, setResults] = useState<Item[]>([])

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setQuery(e.target.value) // urgent: update input immediately
  startTransition(() => {
    setResults(filterHugeList(e.target.value)) // deferred: can be slow
  })
}

return (
  <>
    <input value={query} onChange={handleChange} />
    {isPending ? <Spinner /> : <ResultList items={results} />}
  </>
)
Tip: useTransition은 입력 필드 + 대규모 필터링 등 즉각 응답이 필요한 UI에서 유용합니다.

useDeferredValue

값의 업데이트를 지연시켜 성능 향상

const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)

// The list re-renders with a deferred query
return (
  <>
    <input value={query} onChange={e => setQuery(e.target.value)} />
    <HeavyList filter={deferredQuery} />
  </>
)

lazy + Suspense

컴포넌트 지연 로딩으로 번들 크기 줄이기

import { lazy, Suspense } from 'react'

const HeavyChart = lazy(() => import('./HeavyChart'))
const MarkdownEditor = lazy(() => import('./MarkdownEditor'))

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyChart data={chartData} />
    </Suspense>
  )
}
🏪

Context & 상태 관리

5 items

createContext + useContext

전역 상태를 prop drilling 없이 공유

interface ThemeCtx { theme: 'light' | 'dark'; toggle: () => void }

const ThemeContext = createContext<ThemeCtx | null>(null)

function useTheme() {
  const ctx = useContext(ThemeContext)
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
  return ctx
}

function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')
  const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light')

  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  )
}
Tip: useContext를 커스텀 훅으로 감싸서 null 체크를 한 곳에서 처리하세요.

Context + Reducer Pattern

Context와 useReducer를 결합한 상태 관리

type Action = { type: 'add'; item: Item } | { type: 'remove'; id: string }
type State = { items: Item[] }

const StoreContext = createContext<{
  state: State
  dispatch: React.Dispatch<Action>
} | null>(null)

function StoreProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, { items: [] })
  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  )
}

Provider Composition

여러 프로바이더를 깔끔하게 합성

function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        <StoreProvider>
          {children}
        </StoreProvider>
      </AuthProvider>
    </ThemeProvider>
  )
}

// In layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

Splitting State and Dispatch Context

상태와 dispatch를 분리하여 불필요한 리렌더링 방지

const StateCtx = createContext<State | null>(null)
const DispatchCtx = createContext<React.Dispatch<Action> | null>(null)

function Provider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <DispatchCtx.Provider value={dispatch}>
      <StateCtx.Provider value={state}>
        {children}
      </StateCtx.Provider>
    </DispatchCtx.Provider>
  )
}

// Components that only dispatch won't re-render on state changes
function AddButton() {
  const dispatch = useContext(DispatchCtx)!
  return <button onClick={() => dispatch({ type: 'add' })}>Add</button>
}
Tip: 상태를 읽는 컴포넌트와 변경만 하는 컴포넌트를 분리하면 렌더링 성능이 향상됩니다.

Context with Selector (Zustand)

Zustand를 사용한 선택적 구독

import { create } from 'zustand'

interface AppStore {
  count: number
  user: User | null
  increment: () => void
  setUser: (user: User) => void
}

const useAppStore = create<AppStore>((set) => ({
  count: 0,
  user: null,
  increment: () => set(s => ({ count: s.count + 1 })),
  setUser: (user) => set({ user }),
}))

// Only re-renders when count changes
const count = useAppStore(s => s.count)
const increment = useAppStore(s => s.increment)
🎯

이벤트 처리

6 items

onClick & onChange

기본 이벤트 핸들러 패턴

// Click with event type
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log('clicked at', e.clientX, e.clientY)
}

// Input change
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setQuery(e.target.value)
}

// Select change
const handleSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
  setCategory(e.target.value)
}

Form onSubmit

폼 제출 처리 및 기본 동작 방지

function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    await login(email, password)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password}
        onChange={e => setPassword(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  )
}
Tip: form의 onSubmit과 preventDefault()를 사용하면 Enter 키 제출도 자동으로 처리됩니다.

Keyboard Events

키보드 이벤트 처리

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault()
    handleSubmit()
  }
  if (e.key === 'Escape') {
    setOpen(false)
  }
}

// Ctrl/Cmd + K shortcut
useEffect(() => {
  const handler = (e: KeyboardEvent) => {
    if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
      e.preventDefault()
      setSearchOpen(true)
    }
  }
  window.addEventListener('keydown', handler)
  return () => window.removeEventListener('keydown', handler)
}, [])

Event Delegation with Data Attributes

data 속성을 사용한 이벤트 위임

function ItemList({ items }: { items: Item[] }) {
  const handleClick = (e: React.MouseEvent<HTMLUListElement>) => {
    const target = e.target as HTMLElement
    const id = target.closest('[data-id]')?.getAttribute('data-id')
    if (id) onSelect(id)
  }

  return (
    <ul onClick={handleClick}>
      {items.map(item => (
        <li key={item.id} data-id={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}
Tip: 이벤트 위임은 수백 개의 리스트 아이템에 개별 핸들러를 붙이는 것보다 효율적입니다.

Debounced Input

입력 이벤트를 디바운스하여 API 호출 최적화

function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value)
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay)
    return () => clearTimeout(timer)
  }, [value, delay])
  return debounced
}

// Usage
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)

useEffect(() => {
  if (debouncedQuery) search(debouncedQuery)
}, [debouncedQuery])

Passing Arguments to Handlers

이벤트 핸들러에 추가 인수 전달

// Inline arrow function (simple, fine for small lists)
{items.map(item => (
  <button onClick={() => handleDelete(item.id)}>Delete</button>
))}

// Curried handler (reusable)
const handleAction = (action: string) => (e: React.MouseEvent) => {
  e.stopPropagation()
  performAction(action)
}

<button onClick={handleAction('delete')}>Delete</button>
<button onClick={handleAction('archive')}>Archive</button>
💡

패턴

6 items

Custom Hook

재사용 가능한 로직을 커스텀 훅으로 추출

function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const saved = localStorage.getItem(key)
      return saved ? JSON.parse(saved) : initial
    } catch { return initial }
  })

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value))
  }, [key, value])

  return [value, setValue] as const
}

// Usage
const [theme, setTheme] = useLocalStorage('theme', 'dark')
Tip: 커스텀 훅 이름은 반드시 use로 시작해야 합니다. 이것이 React의 훅 규칙입니다.

Compound Components

유연한 API를 가진 컴포넌트 그룹

function Tabs({ children }: { children: React.ReactNode }) {
  const [active, setActive] = useState(0)
  return (
    <TabsContext.Provider value={{ active, setActive }}>
      {children}
    </TabsContext.Provider>
  )
}

Tabs.List = function TabList({ children }: { children: React.ReactNode }) {
  return <div role="tablist">{children}</div>
}

Tabs.Tab = function Tab({ index, children }: { index: number; children: React.ReactNode }) {
  const { active, setActive } = useContext(TabsContext)!
  return <button role="tab" aria-selected={active === index}
    onClick={() => setActive(index)}>{children}</button>
}

Tabs.Panel = function TabPanel({ index, children }: { index: number; children: React.ReactNode }) {
  const { active } = useContext(TabsContext)!
  return active === index ? <div role="tabpanel">{children}</div> : null
}

Render Props

함수를 통해 렌더링 로직을 주입

interface MouseTrackerProps {
  render: (pos: { x: number; y: number }) => React.ReactNode
}

function MouseTracker({ render }: MouseTrackerProps) {
  const [pos, setPos] = useState({ x: 0, y: 0 })

  useEffect(() => {
    const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY })
    window.addEventListener('mousemove', handler)
    return () => window.removeEventListener('mousemove', handler)
  }, [])

  return <>{render(pos)}</>
}

// Usage
<MouseTracker render={({ x, y }) => <p>Mouse: {x}, {y}</p>} />

Controlled vs Uncontrolled

제어 컴포넌트와 비제어 컴포넌트 비교

// Controlled: React state drives the value
function Controlled() {
  const [value, setValue] = useState('')
  return <input value={value} onChange={e => setValue(e.target.value)} />
}

// Uncontrolled: DOM holds the value
function Uncontrolled() {
  const ref = useRef<HTMLInputElement>(null)
  const handleSubmit = () => console.log(ref.current?.value)
  return <input ref={ref} defaultValue="initial" />
}

Error Boundary

컴포넌트 에러를 포착하여 폴백 UI 표시

class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback: React.ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false }

  static getDerivedStateFromError() { return { hasError: true } }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('ErrorBoundary caught:', error, info)
  }

  render() {
    if (this.state.hasError) return this.props.fallback
    return this.props.children
  }
}

// Usage
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
  <RiskyComponent />
</ErrorBoundary>
Tip: Error Boundary는 클래스 컴포넌트로만 구현 가능합니다. react-error-boundary 라이브러리를 사용하면 편리합니다.

Portal

DOM 트리 외부에 렌더링 (모달, 툴팁)

import { createPortal } from 'react-dom'

function Modal({ open, onClose, children }: {
  open: boolean; onClose: () => void; children: React.ReactNode
}) {
  if (!open) return null

  return createPortal(
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center"
      onClick={onClose}>
      <div className="bg-white rounded-xl p-6" onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body
  )
}
📁

Next.js App Router

7 items

page.tsx & layout.tsx

라우트 페이지와 공유 레이아웃

// app/dashboard/page.tsx — Server Component by default
export default async function DashboardPage() {
  const data = await fetchDashboard() // direct server-side fetch
  return <Dashboard data={data} />
}

// app/dashboard/layout.tsx — wraps all pages in /dashboard/*
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1">{children}</main>
    </div>
  )
}

loading.tsx & error.tsx

로딩 상태와 에러 핸들링 자동 처리

// app/dashboard/loading.tsx
export default function Loading() {
  return <div className="animate-pulse">Loading dashboard...</div>
}

// app/dashboard/error.tsx
'use client' // error.tsx must be a Client Component

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  )
}
Tip: error.tsx는 반드시 "use client"를 선언해야 합니다. loading.tsx는 React Suspense 경계를 자동 생성합니다.

'use client' Directive

Client Component 선언 (hooks, 이벤트 핸들러, 브라우저 API)

'use client'

import { useState } from 'react'

// This component runs in the browser
export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  )
}

// Import in a Server Component page:
// import Counter from './Counter'  // boundary is automatic

Server vs Client Components

Server Component와 Client Component 사용 구분

// Server Component (default) — use for:
// - Data fetching, DB access
// - Accessing backend resources
// - Keeping secrets server-side
// - Reducing client JS bundle
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({ where: { id: params.id } })
  return (
    <div>
      <h1>{product.name}</h1>
      <AddToCartButton id={product.id} /> {/* Client Component */}
    </div>
  )
}

// Client Component — use for:
// - useState, useEffect, useContext
// - Event listeners (onClick, onChange)
// - Browser APIs (localStorage, IntersectionObserver)
// - Third-party client libraries
Tip: 기본적으로 Server Component를 사용하고, 상호작용이 필요한 부분만 Client Component로 분리하세요.

generateMetadata

동적 메타데이터 생성 (SEO)

import type { Metadata } from 'next'

type Props = { params: { slug: string } }

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.coverImage, width: 1200, height: 630 }],
    },
  }
}

export default async function PostPage({ params }: Props) {
  const post = await getPost(params.slug)
  return <Article post={post} />
}

Route Handlers (API Routes)

App Router API 라우트 정의

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const page = searchParams.get('page') ?? '1'
  const users = await db.user.findMany({ skip: (+page - 1) * 20, take: 20 })
  return NextResponse.json(users)
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  const user = await db.user.create({ data: body })
  return NextResponse.json(user, { status: 201 })
}

Dynamic Routes & generateStaticParams

동적 경로와 정적 생성 파라미터

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}

export default async function BlogPost({
  params,
}: {
  params: { slug: string }
}) {
  const post = await getPost(params.slug)
  if (!post) notFound()

  return <Article post={post} />
}

React 치트시트 사용 가이드

React는 Facebook이 만든 UI 라이브러리로, 컴포넌트 기반 선언적 프로그래밍을 통해 인터랙티브한 웹 애플리케이션을 구축합니다. 이 치트시트는 React 18+의 핵심 패턴, 훅, 그리고 Next.js App Router까지 한눈에 정리한 빠른 참조 가이드입니다.

이 치트시트의 활용법

각 섹션은 독립적으로 참조할 수 있습니다. 상단 검색창으로 필요한 패턴을 빠르게 찾고, 코드 블록의 복사 버튼으로 바로 프로젝트에 적용하세요. 모든 예제는 TypeScript와 함수형 컴포넌트 기반입니다.

React 18+의 핵심 변화

React 18은 자동 배칭, Concurrent 렌더링, useTransition, useDeferredValue 등을 도입했습니다. Server Components와 Suspense를 활용한 스트리밍 SSR도 지원됩니다. Next.js App Router는 이러한 기능을 프레임워크 수준에서 통합합니다.

모범 사례

Server Component를 기본으로 사용하고 인터랙션이 필요한 부분만 Client Component로 분리하세요. 상태는 가능한 한 낮은 수준에 배치하고, 커스텀 훅으로 로직을 재사용하세요. useMemo/useCallback은 성능 문제가 측정된 후에 추가하세요.

FAQ

Related Tools

Also Used Together