상태 구조 선택하기
상태를 잘 구조화하면 수정과 디버깅이 쉬운 컴포넌트와 지속적인 버그의 원인이 되는 컴포넌트의 차이를 만들 수 있습니다. 상태를 구조화할 때 고려해야 할 몇 가지 팁을 소개합니다.
배울 내용
- 단일 상태 변수 대 다중 상태 변수를 사용하는 경우
- 상태를 구성할 때 피해야 할 사항
- 상태 구조의 일반적인 문제를 해결하는 방법
상태 구조화 원칙
일부 상태를 보유하는 컴포넌트를 작성할 때, 몇 개의 상태 변수를 사용할지와 데이터의 형태가 어떻게 되어야 할지에 대한 선택을 해야 합니다. 최적이 아닌 상태 구조로도 올바른 프로그램을 작성할 수 있지만, 더 나은 선택을 할 수 있도록 안내하는 몇 가지 원칙이 있습니다:
- 관련 상태를 그룹화하세요.두 개 이상의 상태 변수를 항상 동시에 업데이트하는 경우, 단일 상태 변수로 병합하는 것을 고려하세요.
- 상태의 모순을 피하세요.여러 상태 조각이 서로 모순되고 "불일치"할 수 있는 방식으로 상태가 구조화되면 실수할 여지가 생깁니다. 이를 피하도록 노력하세요.
- 중복 상태를 피하세요.렌더링 중에 컴포넌트의 props나 기존 상태 변수에서 일부 정보를 계산할 수 있다면, 해당 정보를 컴포넌트의 상태에 넣지 않아야 합니다.
- 상태의 중복을 피하세요.동일한 데이터가 여러 상태 변수 사이에 또는 중첩된 객체 내에서 중복되면, 이를 동기화 상태로 유지하기 어렵습니다. 가능할 때 중복을 줄이세요.
- 깊게 중첩된 상태를 피하세요.깊은 계층 구조의 상태는 업데이트하기에 매우 불편합니다. 가능하면 상태를 평평한 방식으로 구조화하세요.
이러한 원칙의 목표는 실수를 도입하지 않으면서 상태를 쉽게 업데이트할 수 있도록 하는 것입니다. 상태에서 중복 및 복제된 데이터를 제거하면 모든 부분이 동기화 상태를 유지하도록 보장하는 데 도움이 됩니다. 이는 데이터베이스 엔지니어가 버그 가능성을 줄이기 위해데이터베이스 구조를 "정규화"하려는 것과 유사합니다. 알베르트 아인슈타인의 말을 바꾸면,"상태를 가능한 한 단순하게 만들되, 너무 단순하지 않게 하라."
이제 이러한 원칙이 실제로 어떻게 적용되는지 살펴보겠습니다.
관련 상태 그룹화
단일 상태 변수와 여러 상태 변수 사용 사이에서 확신이 서지 않을 때가 있을 수 있습니다.
이렇게 해야 할까요?
아니면 이렇게 해야 할까요?
기술적으로는 두 접근 방식 중 어느 것이든 사용할 수 있습니다. 하지만두 상태 변수가 항상 함께 변경된다면, 단일 상태 변수로 통합하는 것이 좋은 생각일 수 있습니다.그러면 커서를 이동할 때 빨간 점의 두 좌표를 모두 업데이트하는 이 예제처럼 항상 동기화 상태를 유지하는 것을 잊지 않게 됩니다:
데이터를 객체나 배열로 그룹화하는 또 다른 경우는 필요한 상태 조각이 몇 개인지 알 수 없을 때입니다. 예를 들어, 사용자가 사용자 정의 필드를 추가할 수 있는 양식이 있을 때 유용합니다.
주의사항
상태 변수가 객체인 경우, 다른 필드를 명시적으로 복사하지 않고는 하나의 필드만 업데이트할 수 없다는 점을 기억하세요. 예를 들어, 위 예제에서setPosition({ x: 100 })를 수행할 수 없습니다. 왜냐하면y속성이 전혀 없기 때문입니다! 대신,x만 설정하려면 setPosition({ ...position, x: 100 })를 수행하거나, 두 개의 상태 변수로 분리하여setX(100)를 수행해야 합니다.
상태의 모순 피하기
다음은 isSending 및 isSent상태 변수를 가진 호텔 피드백 양식입니다:
이 코드는 작동하지만, "불가능한" 상태가 발생할 가능성을 열어둡니다. 예를 들어,setIsSent와 setIsSending을 함께 호출하는 것을 잊어버리면,isSending과 isSent가 동시에true인 상황에 빠질 수 있습니다. 컴포넌트가 복잡해질수록 무슨 일이 일어났는지 이해하기 어려워집니다.
isSending과 isSent는 절대 동시에true가 되어서는 안 되므로, 이 둘을status라는 하나의 상태 변수로 대체하고, 이 변수가세 가지유효한 상태 중 하나를 가지도록 하는 것이 더 좋습니다:'typing'(초기값),'sending', 그리고'sent':
가독성을 위해 여전히 몇 가지 상수를 선언할 수 있습니다:
하지만 이들은 상태 변수가 아니므로 서로 동기화가 깨질 걱정을 할 필요가 없습니다.
불필요한 상태 피하기
컴포넌트의 props나 기존 상태 변수들로부터 렌더링 중에 일부 정보를 계산할 수 있다면, 그 정보를 해당 컴포넌트의 상태에넣어서는 안 됩니다.
예를 들어, 이 폼을 살펴보세요. 작동은 하지만, 여기서 불필요한 상태를 찾을 수 있나요?
이 폼에는 세 개의 상태 변수가 있습니다:firstName,lastName, 그리고fullName입니다. 하지만fullName은 중복됩니다.렌더링 중에 항상fullName을 firstName과 lastName으로부터 계산할 수 있으므로, 상태에서 제거하세요.
다음과 같이 할 수 있습니다:
여기서fullName은 상태 변수가 아닙니다. 대신, 렌더링 중에 계산됩니다:
결과적으로 변경 핸들러는 이를 업데이트하기 위해 특별한 작업을 할 필요가 없습니다.setFirstName이나setLastName을 호출하면 리렌더링이 트리거되고, 다음fullName은 새로운 데이터로부터 계산될 것입니다.
state에서 중복 피하기
이 메뉴 목록 컴포넌트는 여러 여행 간식 중 하나를 선택할 수 있게 합니다:
현재 선택된 항목을 selectedItemstate 변수에 객체로 저장하고 있습니다. 그러나 이는 좋지 않습니다: selectedItem의 내용은 items목록 내부의 항목 중 하나와 동일한 객체입니다.이는 항목에 대한 정보가 두 곳에 중복되어 있다는 의미입니다.
왜 이것이 문제일까요? 각 항목을 편집 가능하게 만들어 봅시다:
먼저 항목에서 "Choose"를 클릭한 다음그 후에수정하면,입력 필드는 업데이트되지만 하단의 레이블은 수정 내용을 반영하지 않습니다. 이는 상태가 중복되었고 selectedItem을 업데이트하는 것을 잊었기 때문입니다.
비록 selectedItem도 업데이트할 수 있지만, 더 쉬운 해결책은 중복을 제거하는 것입니다. 이 예제에서는selectedItem 객체(items내부의 객체와 중복을 생성함) 대신, 상태에selectedId를 보관하고,그런 다음해당 ID를 가진 항목을items배열에서 검색하여selectedItem을 얻습니다:
이전에는 상태가 다음과 같이 중복되었습니다:
items = [{ id: 0, title: 'pretzels'}, ...]selectedItem = {id: 0, title: 'pretzels'}
하지만 변경 후에는 다음과 같습니다:
items = [{ id: 0, title: 'pretzels'}, ...]selectedId = 0
중복이 사라지고 필수적인 상태만 유지됩니다!
이제 선택된항목을 편집하면 아래 메시지가 즉시 업데이트됩니다. 이는setItems가 리렌더링을 트리거하고,items.find(...)가 업데이트된 제목을 가진 항목을 찾기 때문입니다.선택된 항목을 상태에 보관할 필요가 없었던 이유는 오직선택된 ID만 필수적이기 때문입니다. 나머지는 렌더링 중에 계산될 수 있습니다.
깊게 중첩된 상태 피하기
행성, 대륙, 국가로 구성된 여행 계획을 상상해 보세요. 다음과 같이 중첩된 객체와 배열을 사용하여 상태를 구성하고 싶을 수 있습니다:
이제 이미 방문한 장소를 삭제하는 버튼을 추가하고 싶다고 가정해 보겠습니다. 어떻게 접근하시겠습니까?중첩된 상태 업데이트는 변경된 부분부터 객체를 복사하는 작업을 포함합니다. 깊게 중첩된 장소를 삭제하려면 해당 장소의 전체 부모 체인을 복사해야 합니다. 이러한 코드는 매우 장황할 수 있습니다.
상태가 너무 중첩되어 업데이트하기 어렵다면, 상태를 '평평하게' 만드는 것을 고려하세요.데이터를 재구성하는 한 가지 방법은 다음과 같습니다. 각place가 자식 장소의 배열을 가지는 트리 구조 대신, 각 장소가자식 장소 ID의 배열을 가지도록 할 수 있습니다. 그런 다음 각 장소 ID를 해당 장소에 매핑하는 저장소를 만듭니다.
이 데이터 재구성은 데이터베이스 테이블을 보는 것을 떠올리게 할 수 있습니다:
이제 상태가 “평평해졌고”(“정규화”되었다고도 함) 중첩된 항목을 업데이트하는 것이 더 쉬워집니다.
이제 장소를 제거하려면 두 단계의 상태만 업데이트하면 됩니다:
- 해당 장소의 부모장소의 업데이트된 버전은 제거된 ID를
childIds배열에서 제외해야 합니다. - 루트 “테이블” 객체의 업데이트된 버전은 업데이트된 부모 장소 버전을 포함해야 합니다.
다음은 이를 수행하는 방법의 예시입니다:
상태를 원하는 만큼 중첩할 수 있지만, 상태를 '평평하게' 만들면 여러 문제를 해결할 수 있습니다. 이렇게 하면 상태를 더 쉽게 업데이트할 수 있고, 중첩된 객체의 다른 부분에 중복이 발생하지 않도록 보장하는 데 도움이 됩니다.
때로는 중첩된 상태의 일부를 자식 컴포넌트로 옮겨서 상태 중첩을 줄일 수도 있습니다. 이 방법은 항목에 마우스를 올렸는지 여부처럼 저장할 필요가 없는 일시적인 UI 상태에 잘 작동합니다.
요약
- 두 상태 변수가 항상 함께 업데이트된다면, 하나로 병합하는 것을 고려하세요.
- "불가능한" 상태를 만들지 않도록 상태 변수를 신중하게 선택하세요.
- 상태를 업데이트할 때 실수할 가능성을 줄이는 방식으로 상태를 구조화하세요.
- 동기화를 유지할 필요가 없도록 중복되고 불필요한 상태를 피하세요.
- 특별히 업데이트를 방지하려는 경우가 아니라면 props를상태에 넣지 마세요.
- 선택과 같은 UI 패턴의 경우 객체 자체 대신 ID나 인덱스를 상태에 유지하세요.
- 깊게 중첩된 상태를 업데이트하는 것이 복잡하다면, 평평하게 만드는 것을 시도해 보세요.
Try out some challenges
Challenge 1 of 4:Fix a component that’s not updating #
This Clock component receives two props: color and time. When you select a different color in the select box, the Clock component receives a different color prop from its parent component. However, for some reason, the displayed color doesn’t update. Why? Fix the problem.
