ReactNextCentral

State 보존과 재설정

Published on
이 문서는 리액트에서 컴포넌트 간 상태를 보존하고 리셋하는 방법에 대한 설명을 제공합니다.
Table of Contents

상태는 컴포넌트 간에 격리되어 있습니다. 리액트는 UI 트리 내의 위치를 기반으로 어떤 상태가 어떤 컴포넌트에 속하는지 추적합니다. 상태를 언제 보존하고 언제 리셋할지를 제어할 수 있습니다.

배울 내용

  • 리액트가 컴포넌트 구조를 "보는" 방식
  • 리액트가 상태를 보존하거나 리셋하는 시점
  • 리액트에게 컴포넌트 상태를 강제로 리셋시키는 방법
  • 키와 타입이 상태 보존 여부에 어떤 영향을 미치는지

UI 트리

브라우저는 UI를 모델링하기 위해 여러 트리 구조를 사용합니다. DOM은 HTML 요소를 나타내고, CSSOM은 CSS를 나타냅니다. 심지어 접근성 트리도 있습니다!

리액트도 UI를 관리하고 모델링하기 위해 트리 구조를 사용합니다. 리액트는 JSX로부터 UI 트리를 만듭니다. 그런 다음 리액트 DOM은 브라우저 DOM 요소를 해당 UI 트리와 일치하도록 업데이트합니다. (리액트 Native는 이러한 트리를 모바일 플랫폼에 특화된 요소로 변환합니다.)

리액트 Sharing Data

컴포넌트로부터, 리액트는 UI 트리를 생성하고 리액트 DOM은 이를 사용하여 DOM을 렌더링합니다.

상태는 트리 내의 위치에 연결됩니다

컴포넌트에 상태를 제공할 때, 상태가 컴포넌트 내에 "존재"한다고 생각할 수 있습니다. 하지만 실제로 상태는 리액트 내부에 보관됩니다. 리액트는 상태 조각을 컴포넌트가 UI 트리 내의 어떤 위치에 있는지에 따라 올바른 컴포넌트와 연결합니다.

여기에서는 <Counter /> JSX 태그가 하나만 있지만, 서로 다른 위치에 두 개의 별도의 Counter가 렌더링됩니다. Edit elated-frog-zcc3dp

이것이 트리로 표시된 모습입니다.

리액트 Sharing Data

리액트 트리

이것은 두 개의 별도의 카운터입니다. 각각은 고유한 위치에 렌더링되기 때문입니다. 리액트를 사용할 때는 일반적으로 이러한 위치를 고려할 필요는 없지만, 작동 방식을 이해하는 데 유용할 수 있습니다.

리액트에서 화면의 각 컴포넌트는 완전히 격리된 상태를 가지고 있습니다. 예를 들어, 두 개의 Counter 컴포넌트를 옆에 렌더링한다면, 각각에는 독립적인 점수(score)와 호버(hover) 상태가 있게 됩니다.

두 개의 카운터를 모두 클릭해보고, 서로에게 영향을 주지 않는다는 점을 확인하세요:

Edit inspiring-pike-3jz97w

보시다시피 한 컴포넌트가 업데이트될 때 해당 컴포넌트의 상태만 업데이트됩니다.

리액트 Sharing Data

상태 업데이트

리액트는 동일한 컴포넌트가 동일한 위치에서 렌더링되는 한 상태를 계속 유지합니다. 이를 확인하려면 두 개의 카운터를 증가시킨 다음 "두 번째 카운터 렌더링" 확인란을 선택 해제하여 두 번째 컴포넌트를 제거한 다음 다시 선택하여 추가하세요:

Edit romantic-wildflower-wxsc2o

두 번째 카운터를 더 이상 렌더링하지 않게되면, 해당 상태가 완전히 사라집니다. 그 이유는 리액트가 컴포넌트를 제거할 때 상태를 파괴하기 때문입니다.

리액트 Sharing Data

컴포넌트 삭제

리액트는 동일한 컴포넌트가 UI 트리에서 동일한 위치에 렌더링되는 한 상태를 유지합니다.

리액트 Sharing Data

컴포넌트 추가

동일한 위치에 있는 동일한 컴포넌트는 상태를 보존합니다

이 예제에서는 두 개의 다른 <Counter /> 태그가 있습니다. Edit bold-einstein-8swdmv

체크박스를 선택하거나 선택 해제할 때, 카운터 상태는 리셋되지 않습니다. isFancy가 true이든 false이든, 항상 root App 컴포넌트에서 반환된 div의 첫 번째 자식으로 <Counter />가 있습니다.

리액트 Sharing Data

앱 상태를 업데이트해도 Counter는 리셋되지 않습니다. 이는 Counter가 동일한 위치에 남아 있기 때문입니다.

리액트의 관점에서 동일한 컴포넌트이므로 구분됩니다.

주의하기

리액트에서 중요한 점은 JSX 마크업이 아니라 UI 트리 내의 위치가 중요하다는 것입니다! 이 컴포넌트는 if문 내부와 외부에 서로 다른 <Counter /> JSX 태그를 포함하는 두 개의 return 절이 있습니다.

Edit nostalgic-alex-18c6we

체크박스를 선택할 때 상태가 리셋될 것으로 예상할 수 있지만, 그렇지 않습니다! 이는 이 두 개의 <Counter /> 태그가 동일한 위치에서 렌더링되기 때문입니다. 리액트는 함수 내에서 조건을 어디에 두었는지 알지 못합니다. 리액트가 "보는" 것은 반환한 트리뿐입니다.

두 경우 모두 App 컴포넌트가 <Counter />를 첫 번째 자식으로 포함하는 <div>를 반환합니다. 리액트는 이 두 카운터가 동일한 "주소"를 가지고 있다고 간주합니다. 즉, root의 첫 번째 자식의 첫 번째 자식입니다. 이것은 리액트가 이전과 다음 렌더링 사이에서 이들을 일치시키기 위해 사용하는 방법이며, 로직을 구조화하는 방식과 상관없이 동작합니다.

동일한 위치에 있는 다른 컴포넌트는 상태를 리셋합니다

이 예제에서는 체크박스를 선택하면 <Counter><p>로 바꿉니다.

Edit sharp-pine-6gbws5

여기에서 동일한 위치에서 다른 컴포넌트 유형으로 전환합니다. 초기에는 <div>의 첫 번째 자식에 Counter가 포함되어 있습니다. 그러나 p로 교체할 때, 리액트는 Counter를 UI 트리에서 제거하고 해당 상태를 파괴합니다.

리액트 Sharing Data

Counter가 p로 변경되면 Counter는 삭제되고 p가 추가됩니다.

리액트 Sharing Data

다시 전환할 때, p가 삭제되고 Counter가 추가됩니다.

또한, 동일한 위치에 다른 컴포넌트를 렌더링하면 하위 트리 전체의 상태가 리셋됩니다. 이 작동 방식을 확인하기 위해 카운터를 증가시킨 다음 체크박스를 선택하세요:

Edit dawn-grass-meowu5

체크박스를 클릭하면 카운터 상태가 리셋됩니다. Counter를 렌더링하더라도, 첫 번째 자식 div가 div에서 section으로 변경되면서 하위 트리 전체(카운터 및 해당 상태 포함)가 파괴됩니다.

리액트 Sharing Data

section이 div로 변경될 때, section은 삭제되고 새로운 div가 추가됩니다.

리액트 Sharing Data

다시 전환할 때, div는 삭제되고 새로운 section이 추가됩니다.

일반적으로 재렌더링 사이에서 상태를 보존하려면 트리의 구조가 한 렌더링에서 다른 렌더링으로 "일치"해야 합니다. 구조가 다르면 리액트는 컴포넌트를 트리에서 제거할 때 상태를 파괴합니다.

주의하기

이것이 컴포넌트 함수 정의를 중첩해서 사용하지 않아야 하는 이유입니다.

여기에서 MyTextField 컴포넌트 함수가 MyComponent 내에서 정의되었습니다. Edit lucid-napier-0p1vil

버튼을 클릭할 때마다 입력 상태가 사라집니다! 이는 MyComponent의 각 렌더링마다 다른 MyTextField 함수가 생성되기 때문입니다. 동일한 위치에서 다른 컴포넌트를 렌더링하고 있으므로 리액트는 하위 모든 상태를 리셋합니다. 이는 버그와 성능 문제를 유발합니다. 이 문제를 피하기 위해 항상 컴포넌트 함수를 최상위 수준에서 선언하고 중첩하여 정의하지 않도록 해야 합니다.

동일한 위치에서 상태를 리셋하기

기본적으로 리액트는 동일한 위치에서 컴포넌트가 유지되는 동안 해당 컴포넌트의 상태를 보존합니다. 보통 이것이 원하는 동작이므로 기본 동작으로 의미가 있습니다. 그러나 때로는 컴포넌트의 상태를 리셋해야 할 수도 있습니다. 각 턴마다 두 플레이어의 점수를 기록할 수 있는 이 앱을 생각해보세요:

Edit zen-sunset-fi5b4y

현재, 플레이어를 변경하면 점수가 보존됩니다. 두 개의 Counters는 동일한 위치에 나타나므로 리액트는 동일한 Counter로 인식합니다.

그러나 개념적으로 이 앱에서는 두 개의 별도 카운터가 있어야 합니다. UI에서 동일한 위치에 표시될 수 있지만, 하나는 Taylor의 점수를, 다른 하나는 Sarah의 점수를 나타냅니다.

이들 간에 상태를 리셋하는 두 가지 방법이 있습니다.

  1. 서로 다른 위치에 컴포넌트 렌더링하기
  2. 각 컴포넌트에 key를 명시적으로 부여하기

옵션 1: 서로 다른 위치에 컴포넌트 렌더링하기

이 두 Counters가 독립적이기를 원한다면, 두 개의 서로 다른 위치에 렌더링할 수 있습니다.

Edit gallant-nobel-pzdeny

  • 초기에는 isPlayerA가 true이므로 첫 번째 위치에 Counter 상태가 있고 두 번째 위치는 비어 있습니다.
  • "다음 플레이어" 버튼을 클릭하면 첫 번째 위치가 지워지고 두 번째 위치에는 이제 Counter가 포함됩니다.
리액트 Sharing Data

초기상태

리액트 Sharing Data

"다음" 클릭

리액트 Sharing Data

다시, "다음" 클릭

각 Counter의 상태는 DOM에서 제거되는 경우마다 파괴됩니다. 이것이 버튼을 클릭할 때마다 카운터가 리셋되는 이유입니다.

이 솔루션은 동일한 위치에 렌더링되는 독립적인 컴포넌트가 몇 개만 있는 경우에 편리합니다. 이 예제에서는 두 개뿐이므로 JSX에서 각각을 별도로 렌더링하는 것은 부담이 되지 않습니다.

옵션 2: 키를 사용하여 상태 리셋하기

상태를 키를 사용하여 리셋하는 더 일반적인 방법도 있습니다.

리스트를 렌더링할 때 키를 사용하는 것을 본 적이 있을 것입니다. 키는 리스트에만 해당하는 것이 아닙니다! 키를 사용하여 리액트에게 컴포넌트 간의 구별을 알려줄 수 있습니다. 기본적으로 리액트는 부모 내에서 순서("첫 번째 카운터", "두 번째 카운터")를 사용하여 컴포넌트를 구별합니다. 그러나 키를 사용하면 이것이 그냥 첫 번째 카운터나 두 번째 카운터가 아니라 특정한 카운터(예: Taylor의 카운터)임을 리액트에게 알릴 수 있습니다. 이렇게 하면 리액트는 트리의 어느 위치에 나타나더라도 Taylor의 카운터로 인식합니다!

이 예제에서는 JSX에서 동일한 위치에 나타나지만 각 <Counter />가 상태를 공유하지 않습니다. import React from 'react';

Edit little-framework-vlg047

Taylor와 Sarah 사이를 전환해도 상태가 보존되지 않습니다. 이는 서로 다른 키를 부여했기 때문입니다.

{isPlayerA ? (
  <Counter key="Taylor" person="Taylor" />
) : (
  <Counter key="Sarah" person="Sarah" />
)}

키를 명시하면 리액트가 순서 대신 키 자체를 위치의 일부로 사용하도록 지시합니다. 이렇게 하면 JSX에서 동일한 위치에 렌더링되지만 리액트는 두 개의 다른 카운터로 인식하므로 상태를 공유하지 않습니다. 카운터가 화면에 나타날 때마다 해당 상태가 생성됩니다. 제거될 때마다 해당 상태가 파괴됩니다. 상태를 반복해서 리셋합니다.

참고 키는 전역적으로 고유할 필요가 없습니다. 키는 부모 내에서 위치를 지정하는 데 사용됩니다.

키를 사용하여 폼 리셋하기

키를 사용하여 상태를 리셋하는 것은 폼을 다룰 때 특히 유용합니다.

이 채팅 앱에서 <Chat> 컴포넌트는 텍스트 입력 상태를 포함합니다. import React from 'react';

Edit silent-hill-e2gbfe

입력란에 내용을 입력한 다음 "Alice" 또는 "Bob"을 눌러 다른 수신자를 선택해보세요. 입력 상태가 보존되는 것을 알 수 있습니다. <Chat>이 UI 트리에서 동일한 위치에 렌더링되므로 일반적으로 이는 원하는 동작입니다.

많은 앱에서 이것이 원하는 동작일 수 있지만 채팅 앱에서는 그렇지 않습니다! 사용자가 실수로 클릭하여 이미 입력한 메시지를 잘못된 사람에게 보내지 않도록 해야합니다. 이를 수정하기 위해 키를 추가하세요:

<Chat key={to.id} contact={to} />

이렇게하면 다른 수신자를 선택할 때마다 Chat 컴포넌트가 처음부터 다시 생성되며, 해당 트리 아래에있는 모든 상태와 함께 재생성됩니다. 리액트는 또한 DOM 요소를 재생성하게 됩니다.

이제 수신자를 전환하면 항상 텍스트 필드가 지워집니다.

Edit upbeat-joliot-s2dg76

자세히 알아보기: 제거된 컴포넌트의 상태 보존

실제 채팅 앱에서 이전 수신자를 다시 선택할 때 입력 상태를 복구하고 싶을 것입니다. 이전 수신자가 다시 선택될 때 컴포넌트가 더 이상 표시되지 않는 상태에서 "alive"한 상태를 유지하는 몇 가지 방법이 있습니다.

  • 모든 채팅을 렌더링하고 CSS로 모든 다른 채팅을 숨깁니다. 채팅은 트리에서 제거되지 않으므로 로컬 상태가 보존됩니다. 이 방법은 단순한 UI에는 잘 작동하지만, 숨겨진 트리가 크고 DOM 노드가 많은 경우에는 매우 느려질 수 있습니다.
  • 상태를 상위 컴포넌트로 "올리고" 각 수신자에 대한 보류 중인 메시지를 부모 컴포넌트에 보관합니다. 이렇게하면 하위 컴포넌트가 제거되어도 상관없습니다. 중요한 정보를 유지하는 것은 부모 컴포넌트입니다. 이것이 가장 일반적인 솔루션입니다.
  • 리액트 상태 외에도 다른 소스를 사용할 수도 있습니다. 예를 들어, 사용자가 실수로 페이지를 닫아도 메시지 초안이 유지되기를 원할 것입니다. 이를 구현하기 위해 Chat 컴포넌트는 localStorage에서 읽어 상태를 초기화하고 초안을 저장할 수 있습니다.

어떤 전략을 선택하든, Alice와의 대화는 Bob과의 대화와 개념적으로 다른 것이므로 현재 수신자를 기준으로 <Chat> 트리에 키를 부여하는 것이 합리적입니다.

요약

  • 리액트는 동일한 컴포넌트가 동일한 위치에 렌더링되는 한 상태를 유지합니다.
  • 상태는 JSX 태그에 저장되는 것이 아닙니다. JSX에 배치한 위치와 관련이 있습니다.
  • 다른 키를 부여하여 하위 트리를 강제로 리셋시킬 수 있습니다.
  • 컴포넌트 정의를 중첩하지 마세요. 그렇게 하면 실수로 상태가 리셋됩니다.