콘텐츠로 이동

컴포넌트, 상태, 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은 기본값 '방문자' */}
</>
)
}
구분PropsState
소유자부모 컴포넌트컴포넌트 자신
변경 가능 여부읽기 전용setter로 변경
목적데이터 전달시간에 따라 변하는 값
예시사용자 이름, 색상입력값, 로딩 여부, 카운트

props는 외부에서 주어지는 “설정값”이고, state는 컴포넌트 내부의 “메모리”다.

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: '새 이름' }))

컴포넌트 렌더링 이후 “외부 세계”와 동기화할 때 사용한다. 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>
}

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는 외부 세계와의 동기화다 — 의존성 배열로 실행 시점을 제어한다