React 치트시트
React 18+ 핵심 패턴과 훅, Next.js App Router까지 한눈에 정리한 빠른 참조 가이드
컴포넌트 기본
7 itemsFunction 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>
)
}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>
)
}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 itemsuseState
컴포넌트 로컬 상태 관리
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' })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
}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 itemsuseEffect — 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)
}, [])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></>
}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 itemsuseMemo
비용이 큰 계산 결과를 메모이제이션
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])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} />}
</>
)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 itemscreateContext + 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>
)
}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>
}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 itemsonClick & 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>
)
}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>
)
}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 itemsCustom 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')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>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 itemspage.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>
)
}'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 automaticServer 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 librariesgenerateMetadata
동적 메타데이터 생성 (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은 성능 문제가 측정된 후에 추가하세요.