컴포넌트, 상태, Hook 개념
React 앱은 컴포넌트(Component)들의 트리로 이루어진다. 각 컴포넌트는 props를 입력으로 받고 JSX를 반환하는 함수다. Hook은 함수형 컴포넌트에서 상태와 사이드 이펙트를 다루는 메커니즘이다.
함수형 컴포넌트
섹션 제목: “함수형 컴포넌트”컴포넌트는 props를 받아 JSX를 반환하는 순수 함수다.
// props 타입을 명시하면 IDE 자동완성이 동작한다interface GreetingProps { name: string role?: string // 선택 prop}
function Greeting({ name, role = '방문자' }: GreetingProps) { return ( <div> <h2>안녕하세요, {name}님!</h2> <p>역할: {role}</p> </div> )}
// 부모 컴포넌트에서 사용function App() { return ( <> <Greeting name="Alice" role="AI 엔지니어" /> <Greeting name="Bob" /> {/* role은 기본값 '방문자' */} </> )}Props vs State
섹션 제목: “Props vs State”| 구분 | Props | State |
|---|---|---|
| 소유자 | 부모 컴포넌트 | 컴포넌트 자신 |
| 변경 가능 여부 | 읽기 전용 | setter로 변경 |
| 목적 | 데이터 전달 | 시간에 따라 변하는 값 |
| 예시 | 사용자 이름, 색상 | 입력값, 로딩 여부, 카운트 |
props는 외부에서 주어지는 “설정값”이고, state는 컴포넌트 내부의 “메모리”다.
useState
섹션 제목: “useState”const [value, setValue] = useState(초기값)useState는 현재 값과 setter 함수를 쌍으로 반환한다. setter를 호출하면 React가 컴포넌트를 다시 렌더링한다.
import { useState } from 'react'
function TextInput() { const [text, setText] = useState('') const [submitted, setSubmitted] = useState(false)
const handleSubmit = () => { if (text.trim()) { setSubmitted(true) } }
if (submitted) { return <p>제출됨: {text}</p> }
return ( <div> <input value={text} onChange={(e) => setText(e.target.value)} placeholder="텍스트 입력" /> <button onClick={handleSubmit}>제출</button> </div> )}상태 업데이트는 비동기다. setter 호출 직후 같은 렌더링 사이클에서 값을 읽으면 이전 값이 나온다. 이전 상태를 기반으로 업데이트할 때는 함수형 업데이트를 쓴다.
// 이전 상태 기반 업데이트 — 함수형 업데이트 사용setCount(prev => prev + 1)
// 객체 상태 — 불변성 유지setUser(prev => ({ ...prev, name: '새 이름' }))useEffect
섹션 제목: “useEffect”컴포넌트 렌더링 이후 “외부 세계”와 동기화할 때 사용한다. API 호출, 이벤트 리스너, 타이머 등이 해당한다.
useEffect(() => { // 실행할 코드
return () => { // 클린업 (컴포넌트 언마운트 또는 의존성 변경 시) }}, [의존성 배열])의존성 배열에 따른 동작 차이:
| 의존성 배열 | 실행 시점 |
|---|---|
| 없음 | 매 렌더링마다 |
[] (빈 배열) | 마운트 시 한 번 |
[value] | 마운트 + value 변경 시 |
import { useState, useEffect } from 'react'
function UserProfile({ userId }: { userId: number }) { const [user, setUser] = useState<{ name: string } | null>(null) const [loading, setLoading] = useState(true)
useEffect(() => { setLoading(true)
fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => { setUser(data) setLoading(false) }) .catch(err => { console.error('유저 로딩 실패:', err) setLoading(false) }) }, [userId]) // userId가 바뀔 때마다 재실행
if (loading) return <p>로딩 중...</p> if (!user) return <p>유저를 찾을 수 없습니다</p>
return <p>{user.name}</p>}TodoList 컴포넌트 예제
섹션 제목: “TodoList 컴포넌트 예제”props, state, useState, useEffect를 모두 사용하는 완성된 예제다.
import { useState, useEffect } from 'react'
interface Todo { id: number text: string done: boolean}
// 단일 Todo 아이템 컴포넌트 (props만 사용)interface TodoItemProps { todo: Todo onToggle: (id: number) => void onDelete: (id: number) => void}
function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) { return ( <li style={{ textDecoration: todo.done ? 'line-through' : 'none' }}> <input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} /> {todo.text} <button onClick={() => onDelete(todo.id)}>삭제</button> </li> )}
// 메인 TodoList 컴포넌트 (state 관리)function TodoList() { const [todos, setTodos] = useState<Todo[]>([]) const [input, setInput] = useState('') const [filter, setFilter] = useState<'all' | 'active' | 'done'>('all')
// 마운트 시 localStorage에서 복원 useEffect(() => { const saved = localStorage.getItem('todos') if (saved) { setTodos(JSON.parse(saved)) } }, [])
// todos 변경 시 localStorage에 저장 useEffect(() => { localStorage.setItem('todos', JSON.stringify(todos)) }, [todos])
const addTodo = () => { if (!input.trim()) return setTodos(prev => [ ...prev, { id: Date.now(), text: input.trim(), done: false } ]) setInput('') }
const toggleTodo = (id: number) => { setTodos(prev => prev.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo ) ) }
const deleteTodo = (id: number) => { setTodos(prev => prev.filter(todo => todo.id !== id)) }
const filtered = todos.filter(todo => { if (filter === 'active') return !todo.done if (filter === 'done') return todo.done return true })
return ( <div> <h1>할 일 목록</h1>
<div> <input value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && addTodo()} placeholder="새 할 일" /> <button onClick={addTodo}>추가</button> </div>
<div> {(['all', 'active', 'done'] as const).map(f => ( <button key={f} onClick={() => setFilter(f)} style={{ fontWeight: filter === f ? 'bold' : 'normal' }} > {f} </button> ))} </div>
<ul> {filtered.map(todo => ( <TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} onDelete={deleteTodo} /> ))} </ul>
<p> {todos.filter(t => !t.done).length}개 남음 / 전체 {todos.length}개 </p> </div> )}
export default TodoList핵심 정리
섹션 제목: “핵심 정리”- 컴포넌트는 props를 받아 JSX를 반환하는 함수다 — 재사용 가능한 UI 단위
- props는 읽기 전용, state는 컴포넌트 내부 메모리다 — 부모가 props를 제어하고, 자식은 state를 관리한다
- useState는 상태와 setter를 반환한다 — setter 호출 시 리렌더링이 트리거된다
- 불변성을 유지한다 — 객체/배열은 직접 변경하지 않고 새 값을 만들어 setter에 전달한다
- useEffect는 외부 세계와의 동기화다 — 의존성 배열로 실행 시점을 제어한다