5. State - 컴포넌트 내부 상태 관리
State는 컴포넌트 내부에서 관리하는 동적인 데이터임.
Props와 달리 컴포넌트 자신이 state를 변경할 수 있음.
State의 특징:
- 변경 가능: `setState`나 상태 업데이트 함수로 변경 가능
- 비공개: 해당 컴포넌트에서만 접근 가능
- 변경 시 재렌더링: `state`가 변경되면 컴포넌트가 다시 렌더링됨
useState Hook
함수형 컴포넌트에서 state를 사용하는 가장 기본적인 Hook임.
import { useState } from 'react';
function Counter() {
// [현재 상태값, 상태 업데이트 함수] = useState(초기값)
const [count, setCount] = useState(0);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
<button onClick={() => setCount(count - 1)}>감소</button>
<button onClick={() => setCount(0)}>초기화</button>
</div>
);
}
다양한 타입의 State
function StateExamples() {
// 숫자
const [count, setCount] = useState(0);
// 문자열
const [name, setName] = useState('');
// 불리언
const [isOpen, setIsOpen] = useState(false);
// 객체
const [user, setUser] = useState({
name: '',
age: 0,
email: ''
});
// 배열
const [items, setItems] = useState([]);
return <div>{/* 사용 예제 */}</div>;
}
State 업데이트 패턴
1. 직접 값 설정
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(5); // count를 5로 설정
};
return <button onClick={handleClick}>5로 설정</button>;
}
2. 이전 값 기반 업데이트 (함수형 업데이트)
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// 이전 상태값을 받아서 새로운 값을 반환
setCount(prevCount => prevCount + 1);
};
// 여러 번 업데이트할 때 유용
const handleMultipleUpdates = () => {
setCount(prevCount => prevCount + 1); // 1
setCount(prevCount => prevCount + 1); // 2
setCount(prevCount => prevCount + 1); // 3
};
return (
<>
<p>카운트: {count}</p>
<button onClick={handleClick}>+1</button>
<button onClick={handleMultipleUpdates}>+3</button>
</>
);
}
3. 객체 State 업데이트
function UserForm() {
const [user, setUser] = useState({
name: '',
age: 0,
email: ''
});
const handleNameChange = (e) => {
// 스프레드 연산자로 기존 객체 복사 후 변경
setUser({
...user,
name: e.target.value
});
};
const handleAgeChange = (e) => {
setUser(prevUser => ({
...prevUser,
age: parseInt(e.target.value)
}));
};
return (
<form>
<input
type="text"
value={user.name}
onChange={handleNameChange}
placeholder="이름"
/>
<input
type="number"
value={user.age}
onChange={handleAgeChange}
placeholder="나이"
/>
</form>
);
}
4. 배열 State 업데이트
function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
// 추가
const handleAdd = () => {
setTodos([...todos, { id: Date.now(), text: input }]);
setInput('');
};
// 삭제
const handleDelete = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// 수정
const handleUpdate = (id, newText) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={handleAdd}>추가</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
State vs Props 비교
| 특징 | State | Props |
| 정의 | 컴포넌트 내부 데이터 | 부모에서 전달받은 데이터 |
| 변경 가능 여부 | 가능 (setState) | 불가능 (읽기 전용) |
| 소유자 | 해당 컴포넌트 | 부모 컴포넌트 |
| 변경 시 영향 | 해당 컴포넌트 재렌더링 | props를 받는 컴포넌트 재렌더링 |
| 사용 목적 | 동적 데이터 관리 | 데이터 전달 |
State 끌어올리기 (Lifting State Up)
여러 컴포넌트가 같은 데이터를 공유해야 할 때, 공통 부모 컴포넌트로 state를 이동시키는 패턴임.
// 부모 컴포넌트에서 state 관리
function TemperatureConverter() {
const [celsius, setCelsius] = useState('');
const handleCelsiusChange = (value) => {
setCelsius(value);
};
const handleFahrenheitChange = (value) => {
setCelsius((parseFloat(value) - 32) * 5 / 9);
};
const fahrenheit = celsius === '' ? '' : (parseFloat(celsius) * 9 / 5) + 32;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={handleCelsiusChange}
/>
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={handleFahrenheitChange}
/>
</div>
);
}
// 자식 컴포넌트는 props로 데이터와 함수를 받음
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
return (
<fieldset>
<legend>{scale === 'c' ? '섭씨' : '화씨'}</legend>
<input
value={temperature}
onChange={(e) => onTemperatureChange(e.target.value)}
/>
</fieldset>
);
}
6. 컴포넌트 생명주기
React 컴포넌트는 생성부터 소멸까지의 생명주기를 가짐.
함수형 컴포넌트에서는 useEffect Hook으로 생명주기를 관리함.
useEffect Hook
컴포넌트의 부수 효과(Side Effect)를 처리하는 Hook임.
useEffect(() => {
// 실행할 코드 (부수 효과)
return () => {
// 정리(cleanup) 함수 (선택사항)
};
}, [의존성 배열]);
생명주기 단계
1. 마운트 (Mount) - 컴포넌트가 생성될 때
function Component() {
useEffect(() => {
console.log('컴포넌트가 마운트되었습니다');
// API 호출, 이벤트 리스너 등록 등
fetchData();
}, []); // 빈 배열: 마운트 시 한 번만 실행
return <div>컴포넌트</div>;
}
2. 업데이트 (Update) - 컴포넌트가 재렌더링될 때
function Component({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
console.log('userId가 변경되었습니다');
// userId가 변경될 때마다 새로운 데이터 가져오기
fetchUser(userId).then(data => setUser(data));
}, [userId]); // userId가 변경될 때마다 실행
return <div>{user?.name}</div>;
}
3. 언마운트 (Unmount) - 컴포넌트가 제거될 때
function Component() {
useEffect(() => {
// 이벤트 리스너 등록
const handleResize = () => {
console.log('Window resized');
};
window.addEventListener('resize', handleResize);
// cleanup 함수: 언마운트 시 실행
return () => {
console.log('컴포넌트가 언마운트되었습니다');
window.removeEventListener('resize', handleResize);
};
}, []);
return <div>컴포넌트</div>;
}
useEffect 사용 패턴
패턴 1: 마운트 시 한 번만 실행
useEffect(() => {
// 초기 데이터 로딩
fetchInitialData();
}, []); // 의존성 배열이 비어있음
패턴 2: 특정 값이 변경될 때마다 실행
useEffect(() => {
// searchTerm이 변경될 때마다 검색
performSearch(searchTerm);
}, [searchTerm]); // searchTerm을 감시
패턴 3: 모든 렌더링마다 실행
useEffect(() => {
// 매 렌더링마다 실행 (권장하지 않음)
console.log('렌더링됨');
}); // 의존성 배열 없음
패턴 4: 여러 값 감시
useEffect(() => {
// userId나 postId가 변경될 때마다 실행
fetchUserPost(userId, postId);
}, [userId, postId]);
실전 예제 - API 호출
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 로딩 상태 초기화
setLoading(true);
setError(null);
// API 호출
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
// cleanup 함수 (필요한 경우)
return () => {
// 이전 요청 취소 등의 정리 작업
};
}, [userId]); // userId가 변경되면 다시 실행
if (loading) return <div>로딩 중...</div>;
if (error) return <div>에러: {error}</div>;
if (!user) return <div>사용자를 찾을 수 없습니다</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
클래스형 컴포넌트의 생명주기 메서드:
참고용으로, 클래스형 컴포넌트에서는 다음과 같은 생명주기 메서드를 사용함:
- `componentDidMount()`: 마운트 후
- `componentDidUpdate()`: 업데이트 후
- `componentWillUnmount()`: 언마운트 전
- `shouldComponentUpdate()`: 업데이트 여부 결정
- `getDerivedStateFromProps()`: props로부터 state 계산
7. 컴포넌트 구조화 및 설계 원칙
단일 책임 원칙 (Single Responsibility Principle)
하나의 컴포넌트는 하나의 역할만 담당해야 함. 컴포넌트가 너무 커지면 분리하는 것이 좋음.
나쁜 예
function UserDashboard() {
// 사용자 정보, 통계, 설정, 알림 등 모든 것을 한 컴포넌트에서 처리
return (
<div>
{/* 너무 많은 기능 */}
</div>
);
}
좋은 예
function UserDashboard() {
return (
<div>
<UserProfile />
<UserStatistics />
<UserSettings />
<NotificationList />
</div>
);
}
```
**컴포넌트 계층 구조:**
```
App
├── Header
│ ├── Logo
│ ├── Navigation
│ └── UserMenu
├── MainContent
│ ├── Sidebar
│ └── ContentArea
│ ├── PostList
│ │ └── PostItem
│ └── Pagination
└── Footer
컴포넌트 분리 기준:
- 재사용 가능성: 여러 곳에서 사용될 수 있는가?
- 독립성: 독립적으로 테스트하고 관리할 수 있는가?
- 복잡도: 컴포넌트가 너무 복잡해지지 않았는가?
- 관심사 분리: 명확한 단일 목적을 가지는가?
컨테이너 컴포넌트 vs 프레젠테이셔널 컴포넌트
컨테이너 컴포넌트 (Container Component)
- 데이터와 로직을 담당함
- state를 관리함
- API 호출 등의 부수 효과를 처리함
- 스타일링은 최소화함
function UserListContainer() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUsers().then(data => {
setUsers(data);
setLoading(false);
});
}, []);
return <UserList users={users} loading={loading} />;
}
프레젠테이셔널 컴포넌트 (Presentational Component):
- UI 표시에만 집중함
- props를 통해 데이터를 받음
- state가 거의 없음 (있어도 UI 상태만)
- 재사용 가능하도록 설계됨
function UserList({ users, loading }) {
if (loading) return <div>로딩 중...</div>;
return (
<ul>
{users.map(user => (
<UserItem key={user.id} user={user} />
))}
</ul>
);
}
function UserItem({ user }) {
return (
<li>
<img src={user.avatar} alt={user.name} />
<span>{user.name}</span>
</li>
);
}
```
**컴포넌트 네이밍 규칙:**
1. PascalCase 사용: UserProfile, PostList
2. 명확하고 설명적인 이름: Button이 아니라 SubmitButton
3. 역할을 나타내는 이름: UserCard, CommentForm
4. Container/Wrapper 접미사: UserListContainer, ModalWrapper
**폴더 구조 예시:**
```
src/
├── components/
│ ├── common/ # 공통 컴포넌트
│ │ ├── Button.jsx
│ │ ├── Input.jsx
│ │ └── Modal.jsx
│ ├── layout/ # 레이아웃 컴포넌트
│ │ ├── Header.jsx
│ │ ├── Footer.jsx
│ │ └── Sidebar.jsx
│ └── features/ # 기능별 컴포넌트
│ ├── user/
│ │ ├── UserProfile.jsx
│ │ ├── UserList.jsx
│ │ └── UserForm.jsx
│ └── post/
│ ├── PostList.jsx
│ ├── PostItem.jsx
│ └── PostForm.jsx
├── hooks/ # Custom Hooks
│ ├── useAuth.js
│ └── useFetch.js
├── utils/ # 유틸리티 함수
└── App.jsx