본문 바로가기
React/Documentation

리액트 공식 문서 번역-17. 상태 구조 선택

by 똘이토스 2023. 7. 13.

상태(state)를 잘 구조화하는 것은 수정과 디버그를 쉽게 하는 컴포넌트와 상시적인 버그를 일으키는 컴포넌트의 차이를 만들어낼 수 있습니다. 상태를 구조화할 때 고려해야 할 몇 가지 팁이 있습니다.

 

상태를 구조화하는 원칙

일부 상태를 유지하는 컴포넌트를 작성할 때는 상태 변수를 사용하는 수와 그 데이터의 형태를 어떻게 할지 선택해야합니다. 하위 최적 상태 구조로도 올바른 프로그램을 작성할 수 있지만, 더 나은 선택을 돕는 몇 가지 원칙이 있습니다.

 

  1. 관련된 상태를 그룹화하십시오. 항상 두 개 이상의 상태 변수를 동시에 업데이트하는 경우 이들을 하나의 상태 변수로 병합하는 것이 좋습니다.
  2. 상태에 모순을 피하십시오. 상태가 구조화되어 여러 상태 조각이 서로 모순되거나 "일치하지 않는" 경우 오류 여지가 있습니다. 이를 피하려고 노력하십시오.
  3. 중복되는 상태를 피하십시오. 구성 요소의 속성이나 기존 상태 변수를 통해 정보를 계산할 수 있다면 해당 구성 요소의 상태에 그 정보를 넣지 마십시오.
  4. 중복을 피하십시오. 동일한 데이터가 여러 상태 변수 또는 중첩된 객체 내에서 중복되면 이들을 동기화하는 것이 어렵습니다. 가능한 경우 중복을 줄입니다.
  5. 깊게 중첩된 상태를 피하십시오. 깊이 계층적인 상태는 업데이트하기가 매우 불편합니다. 가능한 경우 상태를 평면적으로 구조화하는 것이 좋습니다.

 

이러한 원칙의 목표는 실수를 방지하면서 상태를 쉽게 업데이트하는 것입니다. 상태에서 중복되고 중복되는 데이터를 제거하면 모든 조각이 동기화되도록 보장할 수 있습니다.이것은 데이터베이스 엔지니어가 버그 가능성을 줄이기 위해 데이터베이스 구조를 "정규화"하려는 방식과 유사합니다. 알버트 아인슈타인의 말을 응용하면, "상태를 가능한 한 간단하게 만드십시오 - 그러나 그 이상은 아닙니다."

 

이제 이러한 원칙이 어떻게 적용되는지 살펴보겠습니다.

 

그룹 관련 상태

가끔 단일 상태 변수를 사용할지 여러 개의 상태 변수를 사용할지 결정하기 어려울 때가 있습니다.

 

다음중 어떤 것을 사용해야 할까요?

 

const [x, setX] = useState(0);
const [y, setY] = useState(0);

 

아니면?

 

const [position, setPosition] = useState({ x: 0, y: 0 });

 

기술적으로는 둘 중 어떤 방식을 사용하더라도 상관 없습니다. 그러나 두 개의 상태 변수가 항상 함께 변경된다면, 이들을 하나의 상태 변수로 통합하는 것이 좋을 수 있습니다. 그러면 항상 동기화되도록 잊어버리지 않을 것입니다. 이 예제에서 커서를 이동하면 빨간 점의 두 좌표가 모두 업데이트되는 것과 같습니다

 

https://codesandbox.io/s/13x7mv?file=/App.js&utm_medium=sandpack

 

epic-johnson-13x7mv - CodeSandbox

epic-johnson-13x7mv using react, react-dom, react-scripts

codesandbox.io

 

사용자가 사용자 정의 필드를 추가할 수 있는 폼이 있는 경우와 같이 얼마나 많은 상태 조각이 필요한지 모를 때는 데이터를 객체나 배열에 그룹화하는 것이 유용합니다.

 

💡 만약 상태 변수가 객체인 경우, 다른 필드를 명시적으로 복사하지 않고는 하나의 필드만 업데이트할 수 없다는 것을 기억하세요. 예를 들어 위의 예에서 setPosition({ x: 100 })와 같이 x만 설정할 수 없습니다. 왜냐하면 y 속성이 전혀 없기 때문입니다! 대신 x만 설정하려면 setPosition({ ...position, x: 100 })와 같이 하거나 두 개의 상태 변수로 분리하여 setX(100)와 같이 수행해야 합니다.

 

상태에 모순이 없도록 하세요.

여기에는 호텔 피드백 양식과 isSendingisSent 상태 변수가 있습니다.

 

https://codesandbox.io/s/pkwdmp?file=/App.js&utm_medium=sandpack

 

heuristic-hawking-pkwdmp - CodeSandbox

heuristic-hawking-pkwdmp using react, react-dom, react-scripts

codesandbox.io

 

이 코드는 작동하지만 "불가능한" 상태가 발생할 가능성이 있습니다. 예를 들어, setIsSentsetIsSending을 함께 호출하는 것을 잊으면 isSendingisSent가 동시에 true인 상황이 발생할 수 있습니다. 컴포넌트가 더 복잡할수록 무엇이 일어났는지 이해하기가 어려워집니다.

 

isSendingisSent는 동시에 true일 수 없기 때문에, 'typing'(초기값), 'sending', 'sent' 중 하나의 유효한 상태를 취할 수 있는 하나의 status 상태 변수로 대체하는 것이 좋습니다.

 

https://codesandbox.io/s/ue8v0i?file=/App.js&utm_medium=sandpack

 

laughing-mendel-ue8v0i - CodeSandbox

laughing-mendel-ue8v0i using react, react-dom, react-scripts

codesandbox.io

 

상수 변수는 가독성을 위해 여전히 선언할 수 있습니다.

 

const isSending = status === 'sending';
const isSent = status === 'sent';

 

하지만 이것들은 상태 변수가 아니므로 서로 동기화되지 않아도 괜찮습니다.

 

중복 상태를 피하세요

컴포넌트의 프롭스나 이미 존재하는 상태 변수에서 정보를 계산할 수 있다면, 해당 정보를 컴포넌트의 상태에 넣어두지 말아야 합니다.

 

예를 들어 다음 폼을 봅시다. 동작은 잘 되지만, 여기서 중복된 상태 변수를 찾을 수 있나요?

 

https://codesandbox.io/s/3w14pb?file=/App.js&utm_medium=sandpack

 

adoring-andras-3w14pb - CodeSandbox

adoring-andras-3w14pb using react, react-dom, react-scripts

codesandbox.io

 

이 폼에는 firstName, lastName, fullName 세 개의 상태 변수가 있습니다. 하지만 fullName은 중복된 변수입니다. 렌더링 중에 firstNamelastName에서 항상 fullName을 계산할 수 있으므로 상태에서 제거하십시오.

 

다음과 같이 할 수 있습니다.

 

https://codesandbox.io/s/b8ive6?file=/App.js&utm_medium=sandpack

 

bold-mountain-b8ive6 - CodeSandbox

bold-mountain-b8ive6 using react, react-dom, react-scripts

codesandbox.io

 

여기서 fullName은 상태 변수가 아닙니다. 대신 렌더링 중에 계산됩니다

 

결과적으로 변경 핸들러는 fullName을 업데이트하기 위해 특별한 작업을 수행할 필요가 없습니다. setFirstName 또는 setLastName을 호출하면 다시 렌더링되어 최신 데이터에서 다음 fullName이 계산됩니다.

 

프롭(props)을 상태(state)에 반영하지 마세요.

 

더보기

다음과 같은 코드는 일반적인 불필요한 상태의 예입니다.

 

function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);

 

여기서 color 상태 변수는 messageColor 프롭으로 초기화됩니다. 문제는 부모 컴포넌트가 나중에 messageColor의 다른 값을 전달하면 (예: 'blue' 대신에 'red'와 같이), color 상태 변수가 업데이트되지 않을 것이라는 것입니다! 상태는 첫 번째 렌더링에서만 초기화됩니다.

 

이것이 상태 변수에 "미러링"하는 것이 혼란스러울 수 있는 이유입니다. 대신 코드에서 messageColor 프롭을 직접 사용하세요. 더 짧은 이름을 사용하려면 상수를 사용하세요.

 

function Message({ messageColor }) {
  const color = messageColor;

 

이렇게 하면 부모 컴포넌트에서 전달된 프롭과 일치하지 않는 상태로 업데이트되는 일이 없습니다.

 

프롭을 상태로 “복사”하는 것은 해당 프롭의 업데이트를 무시하고 싶을 때만 유용합니다. 약속 상, 프롭 이름을 initial 또는 default로 시작하여 새 값이 무시된다는 것을 명확히할 수 있습니다.

 

function Message({ initialColor }) {
  // The `color` state variable holds the *first* value of `initialColor`.
  // Further changes to the `initialColor` prop are ignored.
  const [color, setColor] = useState(initialColor);

 

상태에서 중복을 피하세요

이 메뉴 목록 컴포넌트는 여러 가지 항목 중 하나의 여행 간식을 선택할 수 있게 해줍니다.

 

https://codesandbox.io/s/3ll5lu?file=/App.js&utm_medium=sandpack

 

gracious-butterfly-3ll5lu - CodeSandbox

gracious-butterfly-3ll5lu using react, react-dom, react-scripts

codesandbox.io

 

현재, 선택한 항목을 selectedItem 상태 변수에 객체 형태로 저장합니다. 하지만 이것은 좋지 않습니다: selectedItem의 내용은 items 목록 안에 있는 항목과 동일한 객체입니다. 이것은 항목 자체에 대한 정보가 두 곳에서 중복되기 때문에 문제입니다.

 

이것이 문제인 이유는 무엇일까요? 각 항목을 편집할 수 있도록 만들어 보겠습니다

 

https://codesandbox.io/s/bnggym?file=/App.js&utm_medium=sandpack

 

pedantic-hill-bnggym - CodeSandbox

pedantic-hill-bnggym using react, react-dom, react-scripts

codesandbox.io

 

"Choose"를 누르고 수정을 하면 입력란은 업데이트되지만 하단의 레이블은 수정 내용을 반영하지 않습니다. 이것은 중복된 상태로 인해 발생하며 selectedItem도 업데이트를 잊어버렸기 때문입니다.

 

selectedItem도 업데이트할 수 있겠지만, 중복을 제거하는 것이 더 간단한 해결책입니다. 이 예제에서는 selectedItem 객체(이것은 items 내부 객체와 중복됨) 대신에 선택된 ID를 상태에 저장하고, 그 ID에 해당하는 selectedItemitems 배열에서 검색하여 가져옵니다

 

https://codesandbox.io/s/5b25vk?file=/App.js&utm_medium=sandpack

 

cool-sunset-5b25vk - CodeSandbox

cool-sunset-5b25vk using react, react-dom, react-scripts

codesandbox.io

 

(또는 선택된 인덱스를 상태에 보관할 수 있습니다.)

 

이전에는 이렇게 상태가 중복되었습니다

 

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

 

하지만 변경 후에는 이렇게 됩니다

 

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

 

중복이 제거되었고, 필수적인 상태만 보관하게 되었습니다!

 

이제 선택된 아이템을 편집하면 아래 메시지가 즉시 업데이트됩니다. setItems는 다시 렌더링을 트리거하며, items.find(...)가 업데이트된 제목을 가진 아이템을 찾게 됩니다. 선택된 ID만 필수적이기 때문에, 선택된 아이템을 상태에 보관할 필요가 없었습니다. 나머지는 렌더링 중에 계산할 수 있었습니다.

 

깊게 중첩된 상태를 피하라

행성, 대륙, 국가로 구성된 여행 계획을 상상해보세요. 이것의 상태를 중첩된 객체와 배열을 사용하여 구성하려는 유혹이 있을 수 있습니다. 아래 예제와 같이요.

 

https://codesandbox.io/s/1n8twj?file=/places.js&utm_medium=sandpack

 

zen-forest-1n8twj - CodeSandbox

zen-forest-1n8twj using react, react-dom, react-scripts

codesandbox.io

 

이제 이미 방문한 장소를 삭제하는 버튼을 추가하려고 합니다. 어떻게 해야할까요? 중첩된 상태를 업데이트하려면 변경된 부분의 모든 상위 장소를 복사해야합니다. 깊이 중첩된 장소를 삭제하는 경우, 해당 장소의 부모 장소 체인 전체를 복사해야합니다. 이러한 코드는 매우 상세할 수 있습니다.

 

상태가 너무 중첩되어 업데이트하기 어렵다면 "플랫"하게 만드는 것이 좋습니다. 이 데이터를 재구성하는 한 가지 방법은 각 장소가 자식 장소 ID의 배열을 보유하도록하고 각 장소 ID에 해당하는 장소와의 매핑을 저장하는 것입니다.

 

이러한 데이터 재구성은 데이터베이스 테이블을 보는 것과 유사합니다.

 

https://codesandbox.io/s/cdkq4k?file=/places.js&utm_medium=sandpack

 

quirky-paper-cdkq4k - CodeSandbox

quirky-paper-cdkq4k using react, react-dom, react-scripts

codesandbox.io

 

이제 상태가 "flat"(또는 "normalized"라고도 함)이므로 중첩 항목을 업데이트하는 것이 더 쉬워졌습니다.

 

이제 장소를 제거하기 위해 상태를 두 단계 업데이트해야 합니다

 

  • 업데이트된 부모 장소 버전은 자식 ID 배열에서 제거된 ID를 제외해야 합니다.
  • 루트 "table" 객체의 업데이트된 버전은 부모 장소의 업데이트된 버전을 포함해야 합니다.

 

다음은 이를 수행하는 방법의 예입니다

 

https://codesandbox.io/s/qf818i?file=/App.js&utm_medium=sandpack

 

jovial-resonance-qf818i - CodeSandbox

jovial-resonance-qf818i using react, react-dom, react-scripts

codesandbox.io

 

상태를 원하는 만큼 중첩시킬 수 있지만, 상태를 "flat"으로 만들면 여러 문제를 해결할 수 있습니다. 이렇게 하면 상태를 업데이트하기가 더 쉬워지며 중첩된 객체의 서로 다른 부분에서 중복이 발생하지 않도록 보장할 수 있습니다.

 

메모리 사용 개선

더보기

최적의 경우, 메모리 사용을 개선하기 위해 삭제된 아이템(그리고 그들의 자식들!)을 "table" 오브젝트에서 제거하는 것이 이상적입니다. 이 버전은 그렇게 하며, 업데이트 로직을 더 간결하게 만들기 위해 Immer를 사용합니다.

https://codesandbox.io/s/zim79i?file=/App.js&utm_medium=sandpack

 

quizzical-noyce-zim79i - CodeSandbox

quizzical-noyce-zim79i using immer, react, react-dom, react-scripts, use-immer

codesandbox.io

 

때로는 하위 컴포넌트로 일부 중첩된 상태를 이동하여 중첩된 상태를 줄일 수도 있습니다. 이 방법은 저장할 필요가 없는 단기간 UI 상태 (예 : 항목에 마우스 오버되었는지 여부)에 대해 잘 작동합니다.

 

요약

  • 만약 두 개의 state 변수가 항상 함께 업데이트 된다면 하나로 병합하는 것을 고려해보세요.
  • "불가능한" 상태를 만들지 않도록 주의하여 state 변수를 선택하세요.
  • state를 구조화 할 때 실수를 줄이도록 구조화하세요.
  • 중복되거나 중복된 state를 피해 동기화할 필요가 없도록하세요.
  • 업데이트를 방지하려는 경우가 아니라면 props를 state에 넣지 마세요.
  • 선택과 같은 UI 패턴에서는 객체 자체 대신 ID 또는 index를 state에 유지하세요.
  • 깊게 중첩된 state를 업데이트하는 것이 복잡한 경우 평면화해보세요.

 

2023.07.13 - [React/Documentation] - 리액트 공식 문서 번역-18. 컴포넌트간 상태 공유하기

 

리액트 공식 문서 번역-18. 컴포넌트간 상태 공유하기

때로는 두 컴포넌트의 상태가 항상 함께 변경되기를 원할 때가 있습니다. 이를 위해서는 두 컴포넌트에서 상태를 제거하고 가장 가까운 공통 부모 컴포넌트로 이동시킨 다음, props를 통해 하위

ddor2.tistory.com