📌 상태 업데이트(State update)
- 상태는 즉시 업데이트 되지 않는다 (= 스냅샷)
- 👉 렌더링 시점에서 컴포넌트의 상태 값이 즉시 변경되지 않는다 ❌
- 상태(state)를 설정하더라도 기존 렌더링의 변수는 변경되지 않으며, 대신 새로운 렌더링을 요청한다.
- - 다음 렌더링에 대해서만 변경된다.
- 상태는 불변 데이터(Immutable Data)로 관리된다.
- - 리액트에서는 상태, 배열, 객체가 변경되면 안된다.
✨불변 데이터와 가변 데이터
- 불변 데이터: 원시형 타입 (String, Number, Boolean, undefined, null, Symbol)
- 가변 데이터: 객체형 타입 (Function, Array, Object)
✅ 상태 업데이트 과정
- React가 함수를 다시 호출
- 함수가 새로운 JSX 스냅샷 반환
- React가 함수가 반환한 스냅샷과 일치하도록 화면을 업데이트
✅ 상태 업데이트 (setState API)
⏩ setState((prevState) => nextState)
- 현재 상태를 기반으로 새로운 상태를 계산하여 업데이트
- 콜백 함수를 넣는 방법
- setState()에 콜백 함수를 넣으면 그 함수의 매개변수는 이전 상태값이 들어간다. - 큐(Queue)에 쌓임 ([updateState1, updateState2, updateState3...]
👉 들어간 순서대로 렌더링 됨 (updateState3(updateState2(updateState1)))
👉 마지막으로 계산된 값이 nextState가 된다.
function ExampleComponent() {
const [count, setCount] = useState(0);
const increment = () => {
setCount((prevCount) => prevCount + 1); // 현재 상태(prevCount)를 기반으로 새로운 상태를 계산
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
* 큐(Queue)
: 목록에 대한 모든 추가가 한쪽 끝에서 이루어지고 목록의 모든 삭제가 다른 쪽 끝에서 이루어지는 목록으로 대기열을 정의
- 순서대로 가장 먼저 푸시(push)된 요소에 작업이 먼저 수행
⏩ setState(nextState)
- 상태를 직접 업데이트하는 방식
-> setState()에 전달된 값이 상태의 새로운 값으로 설정 - 값을 바로 주는 방법
function ExampleComponent() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // 현재 상태에 1을 더한 값을 새로운 상태로 설정
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
📌 프롭스 드릴링(Props Drilling)
: 속성(props)를 하위 컴포넌트를 전달하기 위해 중간 컴포넌트로 전달하는 것
→ context 또는 상태 관리 라이브러리로 해결
📌 상태 끌어올리기(Lifting state up)
: 컴포넌트 트리에서 상태를 공통의 상위 컴포넌트로 이동시키는 것
- 동일한 상태를 여러 컴포넌트에서 사용해야 할 때
👉 state를 가장 가까운 공통 상위 컴포넌트로 끌어올린다. - 하위의 컴포넌트끼리 상태를 교류해야 할 필요성이 있으면 그 상태에 대한 관리는 상위 컴포넌트에서 관리하는 게 좋다.
해당 상태(state) 변수는 컴포넌트를 부를 때 매개변수로 전달한다.
- 상위 컴포넌트 ➡ 상태 쓰기(C)/읽기(R)/수정(U)/삭제(D) 진행
- 하위 컴포넌트 ➡ 파생된 상태 사용
예제) 검색창 컴포넌트
- data/users.json 파일에서 사용자 목록 데이터 불러오기
- 사용자 검색 필드 및 목록, 검색 정보를 화면에 렌더링
- 사용자 목록 정보 데이터를 순환해 화면에 리스트 렌더링
- 사용자 정보 검색 시, 검색된 데이터만 사용자 목록 업데이트
- 사용자 정보 검색 시, 검색 정보 업데이트
- 사용자 Enter 키 입력 시, 찾기
- 찾기 실행 후, 검색 입력 필드 초기화
- 사용자 입력 경고 후, 검색 필드에 초점 이동
- 사용자 목록 초기화 기능 추가 (초기화 버튼)
- 사용자 목록 초기화 후, 검색 필드에 초점 이동
- 사용자 입력 즉시, 찾기 기능 추가 (HINT: 리액트 상태 관리)
- 실시간 검색 체크박스 기능 추가 (찾기, 목록 초기화 버튼 토글)
- 잦은 상태 업데이트, 리-렌더 이슈 (확인 후, 조치)
- 사용자 입력 디바운싱(debouncing) or 쓰로틀링 (throttling)
UsersPage.jsx (상위 컴포넌트)
import { useState } from 'react';
import usersData from '@/data/users';
import InstantSearchSwitch from './components/InstantSearchSwitch';
import UserListCount from './components/UserListCount';
import UserSearchBox from './components/UserSearchBox';
import UsersList from './components/UsersList';
function UsersPage() {
// 리액트 컴포넌트 상태 관리
const [users] = useState(usersData);
// (하위 컴포넌트의) 상태 끌어올리기
const [searchTerm, setSearchTerm] = useState('');
const [isInstantSearch, setIsInstantSearch] = useState(false);
// 컴포넌트 상태 업데이트 함수를 실행하는 기능(함수)
const handleSearch = (userInput) => {
setSearchTerm(userInput);
};
const handleReset = () => setSearchTerm('');
const handleToggleInstantSearch = () => setIsInstantSearch(!isInstantSearch);
// 필터링한 사용자 목록(파생된 상태)
const searchedUsersList = users.filter(
(user) =>
user.name.includes(searchTerm) ||
user.email.includes(searchTerm) ||
user.city.includes(searchTerm)
);
// 1. users 목록에 input 검색어를 포함한 users 목록을 필터링하여 searchedUsersList에 저장
// 2. searchedUsersList를 UsersList 컴포넌트에 전달
return (
<div className="UsersPage">
<InstantSearchSwitch
isInstantSearch={isInstantSearch}
onToggle={handleToggleInstantSearch}
/>
<UserSearchBox
searchTerm={searchTerm}
isInstantSearch={isInstantSearch}
onSearch={handleSearch}
onReset={handleReset}
/>
<UsersList users={searchedUsersList} />
<UserListCount
searchedUsersCount={searchedUsersList.length}
totalUsersCount={users.length}
/>
</div>
);
}
export default UsersPage;
UserSearchBox.jsx (하위 컴포넌트 1)
import { useId } from 'react';
import { string, bool, func } from 'prop-types';
import './UserSearchBox.css';
import { throttle } from '@/utils';
// 타입 검사
UserSearchBox.propTypes = {
searchTerm: string.isRequired,
isInstantSearch: bool,
onSearch: func,
onReset: func,
};
function UserSearchBox({ searchTerm, isInstantSearch = false, onSearch, onReset, }) {
// UsersPage의 input 검색어와 검색 이벤트 핸들러 함수 등을 가져옴
// 접근성을 위한 고유 ID 생성(useID API)
const id = useId();
// 검색 버튼 이벤트 핸들러1
const handleSearch = () => {
e.preventDefault();
const input = document.getElementById(id);
const button = input.closest('form').querySelector('[type="submit"]');
const value = input.value.trim();
// input 검색어의 양 옆 공백 제거
if (value.length > 0) {
// 사용자 찾기 기능 실행
onSearch?.(value);
// 실행 이후, 검색 필드 초기화
input.value = '';
// 검색 기능은 찾기 버튼을 눌렀을 때 실행되므로 버튼 요소에 초점 이동
button.focus();
} else {
alert('검색어를 입력해주세요.');
input.value = '';
input.focus();
}
};
// 검색창 초기화
const handleReset = () => {
onReset?.();
const input = document.getElementById(id);
input.focus();
};
// 쓰로틀링
if (isInstantSearch) {
// 리-렌더 쓰로틀링 처리 (사용자가 입력 중이더라도 0.6초마다 검색 실행)
handleChange = throttle((e) => onSearch?.(e.target.value), 600);
// 리-렌더 디바운싱 처리 (사용자가 0.2초라도 멈칫하면 검색 실행)
// handleChange = debounce((e) => onSearch?.(e.target.value), 200);
}
return (
<form
className="UserSearchBox"
onSubmit={handleSearch}
onReset={handleReset}
>
<div className="control">
<label htmlFor={id}>사용자 검색</label>
<input
id={id}
type="search"
placeholder="사용자 정보 입력"
defaultValue={searchTerm}
onChange={handleChange}
/>
</div>
{/* 조건부 표시 */}
<button hidden={isInstantSearch} type="submit">
찾기
</button>
<button hidden={isInstantSearch} type="reset">
목록 초기화
</button>
</form>
);
}
export default UserSearchBox;
UsersList.jsx (하위 컴포넌트 2)
import { UsersListType } from '@/@types/type.d';
import UserDetail from './UserDetail';
// 타입 검사
UsersList.propTypes = {
users: UsersListType.isRequired,
};
function UsersList({ users }) {
// {users} = 필터링 된 사용자 목록(검색어가 없을 땐 전체 목록)
return (
<ul className="UsersList">
{users.map((user) => (
<UserDetail key={user.id} user={user} />
))}
</ul>
);
}
export default UsersList;