React는 UI 트리에서 어떤 컴포넌트가 어떤 state에 속하는지를 추적한다. 따라서, state를 언제 보존하고 언제 초기화할지를 제어할 수 있다.
The UI tree(UI 트리)
브라우저는 UI를 모델링 하기 위해 많은 트리구조를 사용한다. DOM은 HTML 요소를, CSSOM은 CSS에 대해 동일한 역할을 한다.
React 또한 트리 구조를 사용하여 사용자가 만든 UI를 관리하고 모델링한다. React는 JSX로부터 UI 트리를 만들고, ReactDOM은 해당 UI 트리와 일치하도록 브라우저 DOM 엘리먼트를 업데이트한다.(React Native는 이러한 트리를 모바일 플랫폼에 맞는 엘리먼트로 변환한다.)
React는 같은 컴포넌트를 같은 위치에 렌더링하는 한 그 state를 유지한다.
import { useState } from 'react';
export default function App() {
const [showB, setShowB] = useState(true);
return (
<div>
<Counter />
{showB && <Counter />}
<label>
<input
type="checkbox"
checked={showB}
onChange={e => {
setShowB(e.target.checked)
}}
/>
Render the second counter
</label>
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
위 예제에서, Render the second counter의 숫자를 증가시킨 뒤에, label을 클릭해서 두 번째 Counter를 삭제한 뒤에 다시 label을 클릭해서 두 번째 Counter를 렌더링하는 경우에는 숫자가 다시 초기값으로 설정되어 있는 것을 확인할 수 있다.
즉, React가 컴포넌트를 제거하면 그 state가 사라지기 때문이고, React는 컴포넌트가 UI 트리의 해당 위치에서 렌더링되는 동안 컴포넌트의 state를 유지한다.(컴포넌트가 제거되거나 같은 위치에 다른 컴포넌트가 렌더링되면 React는 해당 컴포넌트의 state를 삭제한다.)
Same component at the same position preserves state(동일한 위치의 동일한 컴포넌트는 state를 유지합니다)
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
위 예제에서 체크박스를 선택하거나 선택 취소해도 카운터 state는 재설정되지 않는다. isFancy의 상태와 별개로 루트 App 컴포넌트에서 반환된 div의 첫 번째 자식에는 항상 <Counter />가 있기 때문이다.
즉, 같은 위치에 있는 같은 컴포넌트이므로 React의 관점에서 보면 같은 카운터 컴포넌트이다.
* React에서 중요한 것은 JSX 마크업이 아닌, UI 트리에서의 위치이다.
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
if (isFancy) {
return (
<div>
<Counter isFancy={true} />
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
return (
<div>
<Counter isFancy={false} />
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
위 예제에서 if 내부와 외부에 서로 다른 < Counter / > JSX 태그가 있는 두 개의 return 절이 있다. checkbox를 선택했을 때, state가 재설정될 것으로 예상되지만, 이 두 < Counter / > 태그가 모두 같은 위치에 렌더링되기 때문에 React는 동일한 < Counter / > 컴포넌트라고 인식한다.
Different components at the same position reset state(동일한 위치의 다른 컴포넌트는 state를 초기화합니다)
React는 리렌더링 사이에 state를 유지하려면 트리의 구조가 "일치"해야한다. 트리의 구조가 다르면 React는 트리에서 컴포넌트를 제거할 때 state를 파괴한다.
import { useState } from 'react';
export default function App() {
const [isPaused, setIsPaused] = useState(false);
return (
<div>
{isPaused ? (
<p>See you later!</p>
) : (
<Counter />
)}
<label>
<input
type="checkbox"
checked={isPaused}
onChange={e => {
setIsPaused(e.target.checked)
}}
/>
Take a break
</label>
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<div>
<Counter isFancy={true} />
</div>
) : (
<section>
<Counter isFancy={false} />
</section>
)}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
위 두 예제에서 checkbox 선택에 따라 동일한 위치에 다른 컴포넌트가 렌더링되기 때문에 React는 컴포넌트의 state를 초기화한다.
* 컴포넌트 함수를 중첩하면 안되는 이유
import { useState } from 'react';
export default function MyComponent() {
const [counter, setCounter] = useState(0);
function MyTextField() {
const [text, setText] = useState('');
return (
<input
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
return (
<>
<MyTextField />
<button onClick={() => {
setCounter(counter + 1)
}}>Clicked {counter} times</button>
</>
);
}
위 예제에서 MyTextField 컴포넌트 함수가 MyComponent 안에 정의되어 있는데, 이때 버튼을 클릭할 때마다 입력 state가 사라진다. 그 이유는 MyComponent를 렌더링 할 때마다 MyTextField 함수가 생성되기 때문이다.
즉, 같은 위치에 다른 컴포넌트를 렌더링하기 때문에 React는 아래의 모든 state를 초기화한다.
Resetting state at the same position(동일한 위치에서 state 재설정하기)
컴포넌트가 동일한 위치에 있는 동안 컴포넌트의 state는 보존된다. 하지만, 컴포넌트의 state를 reset을 하는 경우가 있다.
import { useState } from 'react';
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>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
위 예제에서 플레이어를 변경하는 경우 점수가 보존되는 문제가 있다. Counter는 동일한 위치에 표시되므로 React는 person prop이 변경된 동일한 Counter로 간주하기 때문이다.
그럼 두 개의 별도 카운터를 만드는 방법이 무엇이 있을까?
1. 컴포넌트를 다른 위치에 렌더링하기
2. 각 컴포넌트에 key로 명시된 아이덴티티를 부여하기
Option 1 : Rendering a component in different positions(컴포넌트를 다른 위치에 렌더링하기)
import { useState } from 'react';
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>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
위 예제를 보면, 처음에는 isPlayerA가 true이기 때문에, 첫 번째 위치에 Counter state가 포함되고 두 번째 위치는 비어있다. Next player 버튼을 클릭하면 첫 번째 위치는 지워지지만 두 번째 위치에는 이제 Counter가 포함된다.
즉, 각 Counter의 state는 DOM에서 제거될 때마다 소멸되고 버튼을 클릭할 때마다 초기화된다.
Option 2 : Resetting state with a key(key로 state 재설정하기)
컴포넌트의 state를 재설정하는 더 일반적인 방법은 key를 사용하는 방법이다.
key는 목록에만 사용되는 것이 아닌, React가 모든 컴포넌트를 구분할 수 있게 한다.
React는 기본적으로 부모 내의 순서를 사용해서 컴포넌트를 구분하지만 key를 사용하면 이것이 첫 번째, 두 번째 컴포넌트가 아닌 특정 컴포넌트임을 React에게 알릴 수 있고 React는 특정 컴포넌트가 트리에 어디에 나타나든 알 수 있다.
import { useState } from 'react';
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>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
즉, key를 부여하면 JSX에서 같은 위치에 렌더링하더라도 React 관점에서 보면 다른 컴포넌트이고 state를 공유하지 않는다.
* key는 전역으로 고유하지 않다. 즉, 부모 내에서의 위치만 지정한다.
Resetting a form with a key(키로 form 재설정하기)
key로 state를 설정하는 것은 form을 다룰 때 특히 유용하다.
예를 들어, 채팅 앱을 만든다고 가정했을 때, 수신자를 변경했음에도 불구하고 동일한 입력 내용(state)가 유지되는 경우가 있다. 이럴 때 key를 사용하면 수신자를 전환했을 때, 항상 텍스트 필드가 지워지게 할 수 있다.(state 초기화)
* 예제
// App.js
import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';
export default function Messenger() {
const [to, setTo] = useState(contacts[0]);
return (
<div>
<ContactList
contacts={contacts}
selectedContact={to}
onSelect={contact => setTo(contact)}
/>
<Chat key={to.id} contact={to} />
</div>
)
}
const contacts = [
{ id: 0, name: 'Taylor', email: 'taylor@mail.com' },
{ id: 1, name: 'Alice', email: 'alice@mail.com' },
{ id: 2, name: 'Bob', email: 'bob@mail.com' }
];
// ContactList.js
export default function ContactList({
selectedContact,
contacts,
onSelect
}) {
return (
<section className="contact-list">
<ul>
{contacts.map(contact =>
<li key={contact.id}>
<button onClick={() => {
onSelect(contact);
}}>
{contact.name}
</button>
</li>
)}
</ul>
</section>
);
}
// Chat.js
import { useState } from 'react';
export default function Chat({ contact }) {
const [text, setText] = useState('');
return (
<section className="chat">
<textarea
value={text}
placeholder={'Chat to ' + contact.name}
onChange={e => setText(e.target.value)}
/>
<br />
<button>Send to {contact.email}</button>
</section>
);
}
* Preserving state for removed components(제거된 컴포넌트에 대한 state 보존)
위 예제와 달리 실제 채팅 앱에서는 수신자를 다시 선택했을 때, state를 복구하고 싶을 것이다. 이런 경우 state를 살아있게 하는 몇가지 방법이 있다.
1. 모든 채팅을 렌더링하되 다른 모든 채팅을 CSS로 숨기는 방법. 간단한 UI에 적합하지만 숨겨진 트리가 크고 많은 DOM 노드를 포함하는 경우 속도가 매우 느려질 수 있다.
2. 부모 컴포넌트에서 각 수신자에 대한 보류중인 메시지를 state를 끌어올려서 보관할 수 있다.
3. React state 외에 다른 소스를 사용할 수 있다. 예를 들어 localStorage에서 읽어서 state를 초기화하고 초안도 저장할 수 있다.
* 참고 : React 공식문서(https://react-ko.dev/learn)
'이것저것 스터디📚 > React - 공식 문서' 카테고리의 다른 글
React 공식문서 -Passing Data Deeply with Context(context로 데이터 깊숙이 전달하기) (0) | 2023.08.09 |
---|---|
React 공식문서 -Extracting State Logic into a Reducer(State 로직을 Reducer로 추출하기) (0) | 2023.08.09 |
React 공식문서 -Sharing State Between Components(컴포넌트 간의 state 공유) (0) | 2023.08.02 |
React 공식문서 -Choosing the State Structure(State 구조 선택) (0) | 2023.08.02 |
React 공식문서 - Reacting to Input with State(state로 입력에 반응하기) (0) | 2023.08.02 |