상태는 컴포넌트들에서 독립되어 있어요. 리액트는 어떤 상태가 어떤 컴포넌트에 들어가있는지를 UI 트리 내에서의 위치에 기반하여 추적해요. 여러분은 리렌더링 사이에서 언제 상태를 보존하고 언제 상태를 초기화 해야하는지를 통제할 수 있어요.
이 페이지에서는
- 리액트가 언제 상태를 보존하는지 혹은 언제 상태를 초기화하는지
- 리액트가 컴포넌트의 상태를 어떻게 강제로 초기화하는지
- 키와 타입이 상태가 보존되는데 어떤 영향을 미치는지
를 알아볼 거예요.
State is tied to a position in the render tree | 상태는 렌더트리에서의 위치와 연결되어 있다
리액트는 UI에서 컴포넌트 구조로 렌더 트리를 만들어요.
컴포넌트의 상태를 만들 때, 상태는 컴포넌트 안에 "거주한다"라고 생각해야해요. 그러나 실제로 상태는 리액트의 내부에서 유지돼요. 리액트는 자신이 갖고 있는 각각의 상태 조각들을 렌더 트리에서의 컴포넌트 위치에 따라 알맞는 컴포넌트와 연결해요.
아래 예시를 보면 <Counter />
JSX 태그가 있지만 서로 다른 두 곳에서 렌더링돼요.
이 예시를 트리 구조로 옮기면 아래와 같아요.

리액트 트리
트리에서 각 카운터는 각자의 위치에서 렌더링 되기 때문에 이 두 개의 카운터는 별개의 카운터에요. 보통 리액트를 사용하면서 이런 위치에 대해 신경쓸 필요는 없지만 리액트가 어떻게 동작하는지를 이해하는데 유용해요.
리액트에서 화면에 보이는 각각의 컴포넌트는 완벽히 독립된 상태에요. 예를 들어, 만약 두 Counter
컴포넌트를 나란히 렌더링한다면 이들은 각각 독립된 score
와 hover
상태를 가져요.
두 카운터를 모두 클릭하고 서로 영향을 미치지 않다는 점을 확인해보세요.
보시다시피 하나의 카운터가 업데이트되면 해당 컴포넌트의 상태만 업데이트 돼요.

상태 업데이트
리액트는 트리의 동일한 위치에서 동일한 컴포넌트를 렌더링하는 한 상태를 유지해요. 이를 확인하려면 두 카운터를 중가시키고 나서 "두 번째 카운터 렌더링하기"의 체크박스에서 선택을 해제하여 두 번째 컴포넌트를 제거하세요. 그리고 나서 다시 클릭하여 다시 추가하세요.
두 번째 카운터를 렌더링하지 않는 순간 상태는 완벽히 사라진다는 점을 기억하세요. 왜냐하면 리액트가 컴포넌트를 지울 때 상태도 지우기 때문이에요.

컴포넌트 지우기
"두 번째 카운터 렌더링하기"를 클릭할 때, 두 번째 Counter
와 그 안의 상태들은 스크래치(score = 0
)에서 초기화되고 DOM에 추가돼요.

컴포넌트 추가하기
컴포넌트가 UI 트리의 해당 위치에서 렌더링되는 한 리액트는 컴포넌트의 상태를 보존해요. 만약 컴포넌트가 제거되거나 다른 컴포넌트가 해당 위치에 렌더링된다면 리액트는 이전 컴포넌트의 상태를 버려요.
Same component at the same position preserves state | 같은 위치의 같은 컴포넌트는 상태를 보존한다
이 예시에는 두 개의 <Counter />
태그가 있어요.
체크박스를 선택하거나 선택을 해제할 때 카운터의 상태는 초기화되지 않아요. isFancy
가 true
든 false
든 <Counter />
는 루트 App
컴포넌트에서 반환된 div
의 첫 번째 자식이에요.

Counter
는 동일한 위치에 계속 있기 때문에 App
상태를 업데이트해도 Counter
는 초기화되지 않아요.
함정
리액트에 영향을 미치는 부분은 JSX 마크업에서의 위치가 아니라 UI 트리에서의 위치라는 점을 잊지 마세요! 이 컴포넌트는if
의 바깥쪽과 안쪽에서<Counter />
JSX 태그를 반환하는 두 개의return
구문을 갖고 있어요.
체크박스를 선택하면 상태가 초기화될 거라고 예상했겠지만 그렇지 않아요. 이<Counter />
들은 같은 위치에서 렌더링되기 때문이에요. 리액트는 함수 안에서 어떤 조건문 안에 위치했는지는 알지 못해요. 리액트가 "보는" 것은 반환한 트리에요.
두 경우에서App
컴포넌트는<Counter />
를 첫 번째 자식으로 가지는<div>
를 반환해요. 리액트에서 이 두 가지 카운터는 루트의 첫 번째 자식의 첫 번째 자식이라는 동일한 "주소"를 가져요. 이를 통해 리액트는 이전 렌더링과 다음 렌더링 사이에서 로직의 구조와 상관 없이 이들을 매칭해요.
Different components at the same position reset state | 같은 위치의 다른 컴포넌트는 상태를 초기화한다
이 예시에서 체크박스를 선택하면 <Counter>
는 <p>
로 대체돼요.
이 예시에서 다른 컴포넌트 타입은 같은 위치에서 변환돼요. 처음에는 <div>
의 첫 번째 자식이 Counter
를 포함했어요. rmfjsk p
로 변환할 때, 리액트는 Counter
를 UI 트리에서 제거하고 상태를 없애요.

Counter
가 p
로 변경될 때, Counter
는 삭제되고 p
가 추가돼요.

다시 이전으로 바꾸면 p
는 삭제되고 Counter
가 추가돼요.
또한 동일한 위치에서 다른 컴포넌트를 렌더링할 때, 서브트리 전체에 있는 상태가 초기화돼요. 어떻게 동작하는지를 보려면 아래 예시에서 카운터를 증가시키고 체크박스를 선택해보세요.
체크박스를 클릭하면 카운터의 상태는 초기화돼요. Counter
를 렌더링했음에도 불구하고 div
의 첫 번째 자식은 div
에서 section
으로 변경돼요. 자식 div
가 DOM에서 제거되면 그 아래에 있는 (Counter
과 상태들을 포함한) 트리 전체도 제거돼요.

section
이 div
로 바뀌면 section
은 삭제되고 새로운 div
가 추가돼요.

다시 이전으로 바꾸면 div
는 삭제되고 새로운 section
이 추가돼요.
경험 법칙에 따라 만약 여러분이 리렌더링 사이에 상태를 보존하고 싶다면 트리의 구조는 하나의 렌더링이 다른 렌더링과 "매칭"되어야 해요. 만약 구조가 변경되면 트리에서 컴포넌트를 제거할 때 리액트가 상태도 같이 제거해요.
함정
이는 컴포넌트 함수 정의를 중첩하면 안되는 이유에요.
여기를 보면MyTextField
컴포넌트 함수가MyComponent
내부에서 정의돼요.
버튼을 누를 때마다 입력값에 대한 상태는 사라져요! 왜냐하면 다른MyTextField
함수는MyComponent
의 모든 렌더링에서 만들어지기 때문이에요. 같은 위치에서 다른 컴포넌트를 렌더링하면 리액트는 그 아래의 모든 상태를 초기화해요. 이러한 동작은 버그와 성능 문제를 발생시켜요. 이를 해결하기 위해서 컴포넌트 함수는 항상 최상위에서 선언하고 정의하는 부분을 중첩하지 마세요.
Resetting state at the same position | 같은 위치에서 상태 초기화 하기
컴포넌트가 동일한 위치에 있다면 기본적으로 리액트는 그것의 상태를 보존해요. 보통 이는 여러분들이 정확하게 원하는 동작이기 때문에 기본 동작인 것이 합리적이에요. 하지만 때때로 컴포넌트의 상태를 초기화하고 싶을 거예요. 이 앱의 카운터는 각자의 턴 동안 그들의 점수를 계속 추적해요.
현재 플레이어가 바뀌어도 점수가 유지돼요. 두 개의 Counter
들은 같은 위치에서 나타나기 때문에 리액트는 이들을 person
prop이 변경됭 동일한 Counter
라고 생각해요.
하지만 개념적으로는 이 앱에서 이들은 별개의 카운터들이에요. UI에서는 같은 곳에서 보이지만 하나는 Taylor의 카운터고 다른 하나는 Sarah의 카운터에요.
플레이어를 변경할 때 상태를 초기화하는 방법에는 두 가지가 있어요.
- 다른 위치에서 컴포넌트 렌더링하기
- 각각의 컴포넌트에
key
를 사용하여 명시적인 식별자 제공하기
Option 1: Rendering a component in different positions | 옵션 1: 다른 위치에서 컴포넌트 렌더링하기
만약 이 두 Counter
를 각각 독립시키고 싶다면 다른 위치에서 렌더링하세요.
- 처음에
isPlayerA
는true
에요. 따라서 첫 번쨰 위치는Counter
상태를 포함하고 두 번째는 비어있어요. - "Next player" 버튼을 클릭하면 첫 번째 위치는 사라지지만 두 번째 위치는 이제
Counter
를 포함해요.
초기 상태
"next" 클릭하기
다시 "next" 클릭하기
각 Counter
의 상태는 DOM에서 지워지는 순간마다 제거돼요. 버튼을 클릭할 때마다 초기화되는 이유가 바로 이것이에요.
이 해결책은 같은 곳에서 렌더링되는 독립적인 컴포넌트가 적을 때 편리해요. 예시는 두 개의 컴포넌트만을 가지고 있기 때문에 JSX에서 이들을 각각 렌더링하는 것은 번거로운 일이 아니에요.
Option 2: Resetting state with a key | 옵션 2: 키를 사용하여 상태 초기화하기
조금 더 일반적인 또 다른 방법도 있어요.
리스트 렌더링하기 문서에서 key
를 본 적이 있을 거예요. 키는 리스트만을 위한 것이 아니에요! 리액트가 여러 컴포넌트들을 구별하도록 만들기 위해 사용할 수도 있어요. 기본적으로 리액트는 컴포넌트를 분별하기 위해 부모 컴포넌트 내부에서 ("첫 번째 카운터", "두 번째 카운터"처럼) 순서를 사용해요. 그러나 키는 리액트에게 이것이 단순한 첫 번째 카운터나 두 번째 카운터가 아니라 Taylor의 카운터처럼 특정 카운터임을 알려줘요. 이 방법을 사용하면 리액트는 트리의 어디에서 컴포넌트가 나타나든, 그 컴포넌트가 Taylor의 카운터임을 알게돼요.
이 예시에서 두 <Counter />
들은 JSX에서 동일한 위치에 있지만 상태를 공유하지 않아요.
Taylor와 Sarah를 바꾸는 것은 상태를 보존하지 않아요. 왜냐하면 다른 key
를 주었기 때문이에요.
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
key
를 지정하면 리액트에게 부모 컴포넌트 내부에서의 순서 대신 key
자체를 위치의 부분으로 사용하라고 알려주는 거예요.이를 통해 여러분이 JSX 안의 동일한 위치에서 이들을 렌더링 했음에도 불구하고 리액트가 두 가지 다른 카운터로 이들을 보고 상태를 절대 공유하지 않는 이유에요. 카운터가 화면에 나타날 때마다 상태를 생겨나요. 카운터가 제거될 때마다 상태도 제거돼요. 이 과정을 반복하는 것은 그들의 상태를 계속해서 초기화해요.
노트
키는 전역적으로 고유하지 않다는 것을 기억하세요. 키는 오직 부모 컴포넌트 내부의 위치만을 지정해요.
Resetting a form with a key | 키를 사용하여 폼 초기화하기
키를 사용하여 상태를 초기화하는 것은 폼을 다룰 때 특히나 유용해요.
아래의 채팅 앱에서 <Chat>
컴포넌트는 텍스트 입력창의 상태를 포함하고 있어요.
입력창에서 무언가 입력하고나서 수신인을 바꾸기 위해 "Alice"나 "Bob"을 누르세요. <Chat>
은 트리의 동일한 위치에서 렌더링되기 때문에 입력의 상태값을 보존된다는 것을 알아차릴 거예요.
많은 앱에서 이러한 동작을 원하겠지만 채팅앱은 아니에요! 사용자는 실수로 다른 사람에게 이미 작성한 메시지를 전송하고 싶지 않을 거예요. 이를 고치려면 key
를 추가하세요.
<Chat key={to.id} contact={to} />
이렇게 하면 다른 수신인을 선택했을 때 Chat
컴포넌트는 이 컴포넌트 안에 있는 상태를 포함하여 스크래치에서 다시 만들어져요. DOM 엘리먼트를 재사용하는 대신 새롭게 만들어요.
이제 수신인을 바꾸면 입력창이 항상 지워져요.
제거된 컴포넌트의 상태 보존하기
더 알아보기실제 채팅 앱에서 사용자가 이전 수신인을 다시 선택했을 때, 입력창의 상태가 복원되길 원할 거예요. 더 이상 보이지 않는 컴포넌트의 상태를 "살아있는" 상태로 유지하는 몇 가지 방법이 있어요.
- 현재 채팅만을 렌더링하는 것이 아니라 모든 채팅을 렌더링하고 CSS로 다른 것들을 숨기세요. 트리에서 채팅들이 제거되지 않았기 때문에 지역 상태 또한 유지돼요. 이 방법은 정말 단순한 UI에서 동작해요. 그러나 만약 숨겨진 트리가 크고 많은 DOM 노드들을 포함하고 있다면 굉장히 느려질 수 있어요.
- 상태를 끌어올리고 부모 컴포넌트에서 각 수신인에 대한 대기 메시지를 갖고 있으세요. 이 방법을 사용하면 자식 컴포넌트가 제거되어도 상관이 없어요. 왜냐하면 부모 컴포넌트가 중요한 정보를 갖고 있기 때문이에요. 이것은 제일 자주 사용되는 방법이에요.
- 또한 리액트 상태 외에도 다른 소스를 사용할 수 있어요. 예를 들어 사용자가 실수로 페이지를 닫았더라도 메시지 초안이 계속 유지되길 원할 수도 있어요. 이를 구현하려면Chat
컴포넌트가 로컬 스토리지에서 읽어온 값으로 상태를 초기화시키고 초안을 그곳에 저장하도록 만들면 돼요.
어떤 전략을 선택하든 Alice와의 채팅은 개념적으로 Bob과의 채팅과 구분되기 때문에 현재 수신인에 기반한key
를<Chat>
트리에 주는 것은 합리적인 선택이에요.
Recap | 요약
- 리액트는 동일한 컴포넌트가 동일한 위치에서 렌더링 되는 한 상태를 유지해요.
- 상태는 JSX 태그에서 유지되지 않아요. 상태는 JSX를 삽입한 트리의 위치와 관련이 있어요.
- 다른 키를 제공하여 서브트리가 상태를 초기화하도록 만들 수 있어요.
- 컴포넌트의 정의를 중첩하지 마세요. 만약 중첩한다면 의도치 않게 상태가 초기화 될 거예요.
Challenges | 도전과제
1. 사라진 입력 텍스트 고치기
이 예시는 버튼을 눌렀을 때 메시지를 보여줘요. 그러나 버튼을 누르면 의도치 않게 입력창이 초기화돼요. 왜 이런 일이 발생할까요? 이 문제를 해결하여 버튼을 눌러도 입력된 텍스트가 사라지지 않도록 만드세요.
Solution
문제는 Form
이 다른 위치에서 렌더링된다는 점이에요. if
문 안에서는 Form
이 <div>
의 두 번째 자식이지만 else
문 안에서는 첫 번째 자식이에요. 그러므로 각 위치에서의 컴포넌트 타입은 달라요. 첫 번째 위치는 p
와 Form
을 번갈아서 갖고 있고, 두 번째 상태는 Form
과 button
을 번갈아서 갖고 있어요. 리액트는 상태를 컴포넌트 타입이 변할 때 마다 초기화해요.
가장 쉬운 방법은 분기를 통합하여 Form
이 항상 같은 위치에서 렌더링되도록 만드는 거예요.
기술적으로 if
문의 구조와 맞추기 위해 else
문 안의 <Form />
전에 null
을 추가할 수도 있어요.
이 방법에서 Form
은 항상 두 번째 자식이기 때문에 같은 위치에 머무르고 상태는 유지돼요. 그러나 이 방법은 덜 명확하고 누군가 null
을 제거할 위험성이 있어요.
2. 두 폼의 필드 바꾸기
이 폼은 성과 이름을 입력할 수 있는 폼이에요. 어떤 필드가 먼저 올지를 선택할 수 있는 체크박스도 있어요. 만약 체크박스를 선택하면 "Last name" 필드는 "First name" 필드 뒤에 나타나요.
거의 모든 기능이 잘 작동하지만 한 가지 버그가 있어요. "First name" 입력창을 채우고 체크박스를 선택하면 텍스트는 (이제는 "Last name"이 된) 첫 번째 입력 창에 여전히 머물러 있어요. 이를 해결하여 순서를 바꾸면 입력 텍스트도 바뀌도록 만드세요.
Hint
이 필드들의 경우부모 컴포넌트에서의 위치만으로는 충분하지 않아요. 리렌더링 간의 상태를 어떻게 매칭할지를 리액트에게 말해주는 방법이 있을까요?
Solution
if
문과 else
문 안의 모든 <Field>
컴포넌트에 key
를 추가하세요. 이 키는 부모 컴포넌트에서 순서가 변하더라도 각 <Field>
에 맞는 상태를 매칭하는 방법을 리액트에게 말해줘요.
3. 디테일 폼 초기화 하기
수정할 수 있는 연락처 목록이 있어요. 선택한 연락처의 세부사항을 수정할 수 있고 "Save" 버튼을 눌러 저장하거나 "Reset" 버튼을 눌러 수정사항을 되돌릴 수 있어요.
다른 연락처(예를 들면 Alice)를 선택할 때, 상태는 업데이트 되지만 폼은 이전 연락처의 세부사항을 계속해서 보여줘요. 이를 수정하여 선택된 연락처가 변경되면 폼이 초기화되도록 만드세요.
Solution
key={selectedId}
를 EditContact
컴포넌트에 추가하세요. 이 방법을 사용하면 다른 연락처로 바꿀 때 폼을 초기화할 수 있어요.
4. 로딩되는 동안 이미지 초기화하기
"Next" 버튼을 누를 때, 브라우저는 다음 이미지를 로딩하기 시작해요. 그러나 같은 <img>
태그 안에서 보여지기 때문에 기본적으로 다음 이미지가 로드되기 전까지 이전 이미지를 보게 돼요. 만약 텍스트가 이미지와 매칭되는 것이 중요하다면 이는 원치 않는 동작이에요. "Next" 버튼을 누르면 이전 이미지가 즉시 사라지도록 만들어보세요.
Hint
DOM을 재사용하는 대신 재생성하라고 리액트에 알려주는 방법은 없을까요?
Solution
<img>
태그에 key
를 제공할 수 있어요. key
가 변경되면 리액트는 <img>
DOM 노드를 스크래치에서 재생성해요. 이런 방법은 각 이미지를 로드할 때 깜빡임을 유발하기 때문에 여러분의 앱에서 사용하고 싶지는 않을 거예요. 그러나 만약 이미지가 항상 텍스트와 매칭되도록 만들고 싶다면 합리적일 거예요.
5. 목록에서 잘못된 곳에 위치한 상태 고치기
이 목록에서 각 Contact
는 "Show email"이 눌렸는지를 결정하는 상태가 있어요. Alice의 "Show email"을 누르고 "Show in reverse order" 체크박스를 체킹해보세요. 그러면 _Taylor_의 이메일은 보이지만 아래로 이동한 Alice의 이메일은 접힌다는 점을 확인할 수 있어요.
선택된 순서에 상관 없이 펼쳐진 상태가 각 연락처와 연관이 있도록 수정하세요.
Solution
key
로 인덱스를 사용했기 때문에 발생한 문제에요.
{displayedContacts.map((contact, i) =>
<li key={i}>
그러나 특정한 연락처에 상태를 연관시켜야해요.
연락처의 ID를 key
로 사용하면 이 문제를 해결할 수 있어요.
상태는 트리에서의 위치와 연관되어 있어요. key
는 순서에 의존하는 대신 명명된 위치를 지정하도록 만들어줘요.