아래 글은 React 공식문서를 기반으로 작성된 글입니다.
React는 UI 트리에서 어떤 컴포넌트가 어떤 state에 속하는지 추적한다. 또한 state를 언제 보존하고 언제 초기화할지 제어한다.
React UI Tree
브라우저에서는 UI를 렌더링하기 위해 많은 Tree를 사용한다. DOM 트리는 HTML, CSSOM 트리는 CSS, 접근성 트리까지 사용한다.
React 또한, 트리구조를 사용해서 UI를 관리하고 모델링한다. React는 JSX로부터 UI 트리를 생성한다. 이후 React DOM은 해당 UI 트리와 일치하도록 브라우저 DOM 엘리먼트들을 업데이트한다.

state는 트리의 한 위치에 묶입니다.
컴포넌트에 state를 선언할때 우리는 보통 state가 컴포넌트 내부에 존재한다고 생각한다.(아니였어?)
하지만 state는 실제로 React 내부에서 유지된다. React는 UI 트리에서 해당 컴포넌트가 어디에 위치하는지에 따라 React가 관리하는 state를 그 컴포넌트와 연결짓는다.
import { useState } from 'react';
export default function App() {
const counter = <Counter />;
return (
<div>
{counter}
{counter}
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
위의 App 컴포넌트의 <Counter />
JSX 태그가 하나만 있지만 서로 다른 위치에서 렌더링된다.

각 Counter 컴포넌트는 완전히 독립적인 state를 갖는다.
만약 두번째 Counter 컴포넌트의 Rendering을 중지하는 순간, 그 Component가 가지는 state까지 모두 삭제된다. React는 컴포넌트가 렌더링되는 동안 컴포넌트의 state를 유지하고, 컴포넌트가 제거되거나 그 위치에 다른 컴포넌트가 렌더링되면 state를 삭제해버린다.
동일한 위치의 동일한 컴포넌트는 state를 유지한다.
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
이런식으로 같은 위치에 같은 컴포넌트가 props만 다르게 렌더링된다면, 리액트는 Tree에 같은 위치에 있는 같은 컴포넌트라고 판단하기에 state가 초기화되지 않는다.
React에서 중요한것은 UI 트리에서의 위치이다.
React에서 중요한것은 JSX 마크업이 아니라, UI 트리에서의 위치이다. React는 우리가 작성한 코드를 알지 못하고, 그저 UI 트리만 확인하고 판단하는 것이다. 따라서 위의 예제와 같이 같은 위치에 같은 컴포넌트가 렌더링된다면, 다른 컴포넌트라고 판단하지 않고, 같은 컴포넌트라고 판단한다.
동일한 위치의 다른 컴포넌트는 state를 초기화한다.
같은 위치에서 만약 컴포넌트가 아예 다른 컴포넌트로 교체된다면, state는 리액트에 의해 삭제된다. 이후 다시 같은 위치에 첫번째 컴포넌트를 다시 렌더링한다면 이미 state는 리액트에 의해 삭제됐기 때문에 새로 initial된 state를 가진 컴포넌트가 렌더링된다.
이처럼 리렌더링 사이에 state를 유지하려면, UI 트리의 구조가 일치해야한다.
이는 React가 트리에서 컴포넌트를 제거할때 state도 같이 제거하기 때문이다.
동일한 위치에서 state 재설정하기
이제 위에서 계속 알아봤듯이, 리액트는 컴포넌트가 같은 위치에 있는동안엔 state를 유지하고 관리하는것을 알 수 있었다. 하지만 같은 위치에 있는 컴포넌트의 state를 reset하고 싶으면 어떻게 해야할까?
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
위와 같은 컴포넌트에서는 Counter 컴포넌트가 같은 위치에서 렌더링되기 때문에 state가 계속 유지된다. 만약 person이 바뀌면 state를 바꾸고 싶으면 어떻게 해야할까?
- 컴포넌트를 다른 위치에 렌더링 시키기.
- 각 컴포넌트에
key
로 컴포넌트를 구분시켜주기.
Option1. 컴포넌트를 다른 위치에 렌더링 시키기.
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
위와 같이, 선언한다면 리액트 트리에서는 서로 다른 위치라고 판단한다.

Option2. key로 state 재설정하기
리액트의 key
는 list에서만 사용되는 것이 아니다. key
를 이용해서 React가 모든 컴포넌트를 구분하게 할 수 있는데, React는 기본적으로 순서를 이용해서 컴포넌트를 구분한다. 하지만 key
를 사용한다면 각 컴포넌트에 id
를 부여하는 것이라고 생각하면 된다.
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
리액트의 key
key
를 지정하면 React가 순서로 컴포넌트를 판단하는 것이 아닌, key
자체로 컴포넌트를 판단하게끔 한다. 따라서 같은 위치에 같은 컴포넌트를 렌더링해도, key
값이 다르다면 React 관점에서는 다른 컴포넌트로 대체되었다고 판단하고 state를 초기화한다.
그리고 key
를 사용할때 주의할 점은, key
가 전역적으로 사용되지 않는다는 점이다. key
는 오직 부모 내에서만 컴포넌트를 식별하는데 도움을 준다.
제거된 컴포넌트에 대한 state 보존하기.
1. CSS로 숨기기
css로 컴포넌트는 유지한 채, 보이지 않게끔 하는 방법이다.
2. 부모 컴포넌트에서 관리하기
부모 컴포넌트로 state를 끌어올려 보관하는 방법. 가장 일반적인 방법이다.
3. localstorage 사용하기
React state로 관리하는 것이 아닌 localstorage로 저장하고 불러오는 방법이다.