파생 상태와 계산된 값
다른 state나 prop으로부터 계산될 수 있는 값은 state로 관리하면 안 되는 이유, 파생 상태의 판단 기준, 그리고 useMemo로 비싼 계산을 최적화하는 패턴을 설명합니다.
지난 글에서 불변성 업데이트로 React의 변경 감지를 올바르게 동작시키는 방법을 살펴봤습니다. 이번에는 state를 얼마나 적게 가져야 하는가에 관한 이야기입니다. 다른 state로부터 계산될 수 있는 값을 별도 state로 관리하면 동기화 버그가 생깁니다.
파생 상태란
**파생 상태(derived state)**는 기존 state나 prop으로부터 계산할 수 있는 값입니다. 이 값을 별도 useState로 관리하면 두 값이 불일치하는 버그가 발생할 위험이 생깁니다.
// ❌ 동기화 위험 — items에서 이미 계산 가능
function Cart() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0); // 중복
const [count, setCount] = useState(0); // 중복
function addItem(item) {
setItems(prev => [...prev, item]);
setTotal(prev => prev + item.price); // 항상 함께 갱신해야 함
setCount(prev => prev + 1); // 하나 빠뜨리면 버그!
}
}
대신, items만 state로 두고 나머지는 렌더링 중에 계산합니다.
// ✓ items 하나만 state
function Cart() {
const [items, setItems] = useState([]);
const total = items.reduce((sum, i) => sum + i.price, 0); // 자동 동기화
const count = items.length; // 자동 동기화
function addItem(item) {
setItems(prev => [...prev, item]); // 이것만 변경하면 됨
}
}
파생 상태 판단 기준
“이 값을 기존 state나 prop으로부터 계산할 수 있는가?”
Yes라면 state가 아닌 계산된 변수로 선언합니다.
대표적인 파생 상태 케이스들:
// 1. 필터된 목록 — filter 조건만 state
const [filter, setFilter] = useState('all');
const visible = todos.filter(t =>
filter === 'all' || t.done === (filter === 'done')
);
// 2. 검색 결과 — 검색어만 state
const [query, setQuery] = useState('');
const results = users.filter(u =>
u.name.toLowerCase().includes(query.toLowerCase())
);
// 3. 폼 유효성 — 입력값만 state
const [email, setEmail] = useState('');
const isEmailValid = email.includes('@') && email.length > 4;
// 4. 선택된 항목 — ID만 state
const [selectedId, setSelectedId] = useState(null);
const selected = items.find(i => i.id === selectedId);
useMemo: 비싼 계산 메모이제이션
계산 비용이 크다면 useMemo로 감싸 불필요한 재계산을 피할 수 있습니다.
// 수천 건의 필터링 — 렌더마다 재실행하면 비쌀 수 있음
const filteredList = useMemo(
() => hugeList.filter(item => item.category === selectedCategory),
[hugeList, selectedCategory] // 의존성이 바뀔 때만 재계산
);
하지만 대부분의 계산은 useMemo 없이도 충분히 빠릅니다. 먼저 계산식으로 작성하고, 프로파일링 후 필요한 경우에만 useMemo를 추가하는 것이 권장 방법입니다.
진짜 state인지 파생 상태인지 구분하는 질문
- 시간이 지나도 변하는가? — No라면 상수
- 부모가 prop으로 전달하는가? — Yes라면 state가 아님
- 다른 state나 prop으로 계산되는가? — Yes라면 파생 상태
세 질문 모두 No일 때만 state로 관리합니다.
// 판단 예시
function ProductPage({ products, userId }) {
// ✓ state: 시간에 따라 변하고, 계산 불가
const [searchQuery, setSearchQuery] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
// ✓ 파생: searchQuery, sortOrder, products로부터 계산
const filtered = products
.filter(p => p.name.includes(searchQuery))
.sort((a, b) =>
sortOrder === 'asc'
? a.price - b.price
: b.price - a.price
);
// ✓ 파생: products에서 계산
const totalCount = products.length;
const avgPrice = products.reduce((s, p) => s + p.price, 0) / totalCount;
}
흔한 실수: Effect로 동기화하기
파생 상태를 잘못 처리하면 useEffect로 동기화하려는 시도가 생깁니다.
// ❌ Effect로 동기화 — 한 렌더 늦게 반영되고, 무한 루프 위험
useEffect(() => {
setFilteredItems(items.filter(i => i.active));
}, [items]);
// ✓ 렌더링 중 직접 계산
const filteredItems = items.filter(i => i.active);
useEffect는 외부 시스템과 동기화할 때 쓰는 도구입니다. 같은 컴포넌트 안의 state끼리 동기화하는 데 쓰면 거의 항상 파생 상태 문제입니다.
지난 글: 불변성 업데이트 패턴
다음 글: 상태 배치(State Colocation)
읽어주셔서 감사합니다. 😊