ReactNextCentral

State 구조 선택

Published on
이 글에서는 리액트 애플리케이션에서 상태를 구조화하고 관리하는데 유용한 원칙과 팁을 소개합니다.
Table of Contents

상태를 잘 구조화하는 것은 수정 및 디버깅이 쉬운 컴포넌트와 버그가 계속 발생하는 컴포넌트 간의 차이를 만들어 줄 수 있습니다. 상태를 구조화할 때 고려해야 할 몇 가지 팁이 있습니다.

배울 내용

  • 단일 상태 변수와 여러 상태 변수를 사용할 때의 선택 시기
  • 상태를 구성할 때 피해야 할 사항
  • 상태 구조의 일반적인 문제를 해결하는 방법

상태를 구조화하기 위한 원칙

일부 상태를 보유한 컴포넌트를 작성할 때, 얼마나 많은 상태 변수를 사용하고 그 데이터의 형태는 무엇이어야 하는지에 대한 선택을 해야 합니다. 최적이 아닌 상태 구조로도 올바른 프로그램을 작성할 수는 있지만, 몇 가지 원칙을 따르면 더 나은 선택을 할 수 있습니다.

  1. 관련된 상태를 그룹화합니다. 항상 두 개 이상의 상태 변수를 동시에 업데이트하는 경우, 이들을 하나의 상태 변수로 통합하는 것을 고려해 보세요.
  2. 상태에서 모순을 피합니다. 상태가 서로 모순되고 서로 "일치하지 않는" 여러 상태를 가질 수 있는 방식으로 구조화되어 있다면, 실수할 여지를 남기게 됩니다. 이를 피하기 위해 노력해야 합니다.
  3. 중복 상태를 피합니다. 구성 요소의 props나 기존 상태 변수에서 렌더링 중에 일부 정보를 계산할 수 있다면, 해당 정보를 구성 요소의 상태로 넣지 않아야 합니다.
  4. 상태에서 중복을 피합니다. 동일한 데이터가 여러 상태 변수나 중첩된 객체 내에 중복되어 있는 경우, 이들을 동기화하는 것이 어렵습니다. 가능한 경우 중복을 줄이세요.
  5. 깊게 중첩된 상태를 피합니다. 깊게 중첩된 계층 구조의 상태는 업데이트하기 편리하지 않습니다. 가능한 경우 상태를 평평하게 구조화하는 것이 좋습니다.

이러한 원칙의 목표는 실수를 최소화하면서 상태를 쉽게 업데이트하는 것입니다. 중복된 데이터와 중복을 제거하여 상태의 모든 부분이 동기화되도록하는 것입니다. 이는 데이터베이스 엔지니어가 버그의 가능성을 줄이기 위해 데이터베이스 구조를 "정규화"하려는 방식과 유사합니다. Albert Einstein의 말을 para하면 "상태를 가능한 한 단순하게 만들지만 그 이상으로 단순하게 만들지는 마십시오."

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

관련된 상태 그룹화하기

가끔씩 단일 상태 변수를 사용할지 여러 개의 상태 변수를 사용할지 확신이 서지 않을 수 있습니다.

다음과 같이 해야 할까요?

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

아니면 이렇게 할까요?

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

원칙적으로 이러한 접근 방식을 모두 사용할 수 있습니다. 그러나 두 개 이상의 상태 변수가 항상 함께 변경된다면, 이들을 하나의 상태 변수로 통합하는 것이 좋습니다. 그러면 항상 동기화를 유지하는 것을 잊지 않을 것입니다. 이 예제에서는 커서를 이동하면 빨간 점의 좌표를 모두 업데이트하는 것과 같습니다.

사용자가 커서를 움직이면 좌표의 두 가지 값을 모두 업데이트해야 하는 상황이라면 이렇게 그룹화하는 것이 좋습니다.

Edit mutable-moon-k3wul4

또 다른 경우로는 사용자가 사용자 정의 필드를 추가할 수 있는 폼이 있는 경우입니다.

주의하기

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

모순을 피하기

다음은 isSending과 isSent 상태 변수를 사용한 호텔 피드백 폼의 예입니다.

Edit serene-mclaren-7qx53i

이 코드는 작동하지만 "불가능한" 상태의 가능성을 남겨둡니다. 예를 들어, setIsSent와 setIsSending을 동시에 호출하는 것을 잊어버리면 isSending과 isSent가 동시에 true인 상태가 될 수 있습니다. 컴포넌트가 복잡해질수록 어떤 일이 발생했는지 이해하기가 더 어려워집니다.

isSending과 isSent는 절대로 동시에 true가 되지 않아야 하므로, 이들을 하나의 상태 변수인 status로 대체하는 것이 좋습니다. status는 'typing' (초기값), 'sending', 'sent'와 같은 세 가지 유효한 상태 중 하나를 가질 수 있습니다.

Edit restless-sound-2t7gxo

가독성을 위해 몇 가지 상수를 선언할 수 있습니다.

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

하지만 이들은 상태 변수가 아니므로 서로 동기화되지 않을 걱정할 필요가 없습니다.

중복 상태 피하기

이 폼은 firstName, lastName 및 fullName 세 가지 상태 변수를 가지고 있습니다. 그러나 fullName은 중복입니다. firstName과 lastName을 렌더링 중에 항상 fullName으로 계산할 수 있으므로, fullName을 상태에서 제거하세요.

Edit musing-silence-dnvs2u

이렇게 할 수 있습니다.

Edit amazing-lena-sc71ts

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

const fullName = firstName + ' ' + lastName;

결과적으로 변경 핸들러는 fullName을 업데이트하기 위해 특별한 작업을 수행할 필요가 없습니다. setFirstName 또는 setLastName을 호출하면 다시 렌더링이 트리거되고, 다음 fullName은 새로운 데이터를 기반으로 계산됩니다.

자세히 알아보기: 상태에 props 복제하지 않기

상태에 복제하는 일반적인 예는 다음과 같은 코드입니다.

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

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

이렇게 props을 상태 변수에 "복제"하는 것은 혼란을 야기할 수 있습니다. 대신 코드에서 messageColor props을 직접 사용하세요. 이름을 더 짧게 지정하려면 상수를 사용하세요.

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

이렇게 하면 부모 컴포넌트에서 전달된 props과 동기화되지 않습니다.

props를 상태에 "복제"하는 것은 특정 props의 모든 업데이트를 무시하려는 경우에만 의미가 있습니다. 관례적으로 이러한 케이스의 prop 이름은 initial이나 default로 시작하여 새로운 값이 무시됨을 명확히 합니다.

function Message({ initialColor }) {
  // `color` 상태 변수는 `initialColor`의 *첫 번째* 값을 보유합니다.
  // `initialColor` prop의 후속 변경은 무시됩니다.
  const [color, setColor] = useState(initialColor);

중복 상태 피하기

이 메뉴 목록 컴포넌트는 여러 가지 옵션 중 하나를 선택할 수 있습니다.

Edit nice-mclean-0m3ydb

현재 선택한 항목을 selectedItem 상태 변수에 객체로 저장합니다. 그러나 이는 좋지 않습니다. selectedItem의 내용은 items 목록 내의 항목과 동일한 객체입니다. 이는 항목 자체에 대한 정보가 두 곳에서 중복됨을 의미합니다.

이것이 문제인 이유를 알아봅시다. 각 항목을 편집할 수 있도록 해 보겠습니다.

Edit boring-wu-769ixj

"Choose"를 먼저 클릭한 다음에 편집하면, 입력은 업데이트되지만 하단에 있는 레이블은 편집 사항을 반영하지 않습니다. 이는 상태의 중복으로 인해 발생하며, selectedItem을 업데이트하는 것을 잊었습니다.

selectedItem도 업데이트할 수 있지만, 더 쉬운 해결책은 중복을 제거하는 것입니다. 이 예제에서는 items 목록 내의 객체와 중복을 일으키는 대신 상태로 선택된 ID를 유지하고, 해당 ID와 일치하는 항목을 items 배열에서 검색하여 selectedItem을 가져옵니다. (또는 상태로 선택한 인덱스를 유지할 수도 있습니다.)

Edit tender-frog-ubzyo9

상태가 이렇게 중복되었던 예제는 다음과 같이 변경되었습니다.

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

하지만 변경 후에는 다음과 같이 되었습니다.

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

중복이 제거되었고, 필수적인 상태만 유지되었습니다!

이제 선택한 항목을 편집하면 아래의 메시지가 즉시 업데이트됩니다. setItems는 다시 렌더링을 트리거하고, items.find(...)는 업데이트된 제목을 가진 항목을 찾을 것입니다. 상태를 유지할 필요가 없기 때문에 선택한 항목을 상태에 저장하지 않았습니다. 필수적인 것은 선택한 ID뿐이었고, 나머지는 렌더링 중에 계산될 수 있었습니다.

깊게 중첩된 상태 피하기

행성, 대륙 및 국가로 구성된 여행 계획을 상태로 구조화하려고 할 수 있습니다. 이 예제와 같이 중첩된 객체와 배열을 사용하여 상태를 구조화하려고 할 수 있습니다. Edit admiring-dijkstra-1pun7l

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

상태가 너무 깊게 중첩되어 업데이트하기 어려운 경우, "평평하게" 만드는 것을 고려하세요. 이 데이터를 다시 구조화하는 방법을 살펴보겠습니다. 각 장소가 자식 장소의 배열을 보유하는 트리와 같은 구조 대신, 각 장소가 자식 장소 ID의 배열을 보유하도록 변경하고, 각 장소 ID에 해당하는 장소와의 매핑을 저장합니다.

이 데이터 구조화는 데이터베이스 테이블을 보는 것과 유사합니다. Edit beautiful-solomon-pz7l98

이제 상태가 "평평"해졌습니다(또는 "정규화"되었습니다). 중첩된 항목을 삭제하려면 두 수준의 상태를 업데이트하는 것만으로 충분합니다.

  • 업데이트된 "부모" 장소 버전은 삭제된 ID를 자식 ID 배열에서 제외해야 합니다.
  • 업데이트된 루트 "테이블" 객체 버전에는 업데이트된 부모 장소 버전이 포함되어야 합니다.

이렇게 할 수 있습니다. Edit clever-parm-x9kuzx

원하는 대로 상태를 중첩시킬 수 있지만, "평평"하게 만들면 많은 문제를 해결할 수 있습니다. 상태를 쉽게 업데이트할 수 있게 되며, 중첩된 객체의 서로 다른 부분에서 중복을 가질 수 없도록 보장됩니다.

자세히 알아보기: 메모리 사용량 향상

이상적으로는 삭제된 항목(및 해당 하위 항목!)을 "테이블" 객체에서 제거하여 메모리 사용량을 개선해야 합니다. 이 버전은 그렇게 합니다. 또한 업데이트 로직을 더 간결하게 작성하기 위해 Immer를 사용합니다.

Edit quizzical-noyce-zim79i

때로는 일부 중첩된 상태를 자식 컴포넌트로 이동하여 상태의 중첩을 줄일 수도 있습니다. 이는 저장할 필요가 없는 일시적인 UI 상태에 대해 잘 작동합니다(예: 항목이 호버되었는지 여부 등).

요약

  • 두 상태 변수가 항상 함께 업데이트된다면, 하나로 통합하는 것을 고려하세요.
  • "불가능한" 상태를 만들지 않도록 주의하여 상태 변수를 선택하세요.
  • 상태를 업데이트하는 데 실수를 줄이기 위해 상태를 구조화하세요.
  • 중복과 중복 상태를 피하여 동기화해야 하는 상태를 최소화하세요.
  • 특별한 경우가 아니라면 props을 상태로 넣지 마세요.
  • 선택과 같은 UI 패턴의 경우 객체 자체 대신 ID나 인덱스를 상태로 유지하세요.
  • 깊게 중첩된 상태를 업데이트하는 것이 복잡하다면, 평평하게 만들어 보세요.