State는 컴포넌트에 고립돼 있다. 리액트는 UI 트리에서 컴포넌트의 위치를 기반으로 어떤 state가 어떤 컴포넌트에 소속돼 있는 녀석인지를 계속 추적한다. 개발자는 리렌더링 사이에서 언제 state를 보존할지, 리셋할지 컨트롤할 수 있다.
이 문서에서는
- 리액트가 컴포넌트 구조를 어떻게 "보는지"
- 리액트가 state를 보존할지 / 리셋할지를 언제 선택하는지
- 강제로 state 리셋시키는 법
- keys와 types가 state의 보존 여부에 미치는 영향
을 배워보겠다.
브라우저는 UI를 모델링하기 위해 많은 트리 구조를 사용한다. DOM은 HTML을 대표하고, CSSOM은 CSS를 대표한다. 심지어 Accessibility tree 도 있다!
리액트 역시 UI를 관리하고 모델링하기 위한 트리를 사용한다. 리액트는 JSX로부터 UI 트리를 만듬. 그 다음 Recat DOM은 그 UI 트리에 맞게끔 브라우저 DOM 요소 를 업데이트한다. (React Native는 이 트리를 각 mobile platform에 맞게 업데이트함 ㄷㄷ.)
리액트 공식문서에서 virtual dom 이라는 표현은 사용되지 않는다. 이 문서의 리액트 DOM이 보통 우리가 알고 있는 virtual dom에 대한 이야기인듯.
컴포넌트에게 state를 부여하면, state가 컴포넌트 안에서 "살고" 있다고 생각할 수 있다. 아니다! state는 리액트 안에 매어있다. 그리고 리액트가 각 state를 컴포넌트에 연결해주는 것임. UI 트리의 어느 부분에 있는지 (위치)를 기반으로!
Counter 컴포넌트에는 score, hover 두 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);
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>
);
}
왼쪽, 오른쪽 컴포넌트는 서로 다른 녀석들이다. 트리에서 다른 위치에 렌더링됐기 때문. 리액트를 사용할 때 이런 위치는 보통 몰라도 되지만, 작동을 이해하는데 도움이 된다.
리액트에서 화면의 각 컴포넌트는 완전히 고립된 state를 가지고 있다. 오른쪽 컴포넌트를 눌러서 score
state를 바꾼다고 왼쪽 score
가 바뀌지 않음.
오른쪽 컴포넌트를 없앴다 다시 그려보자.
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>
);
}
체크박스를 만들어서 누르면 두번째 컴포넌트를 없애봤다. ( {showB && <Counter />
)
원래 score
에 3
이라는 값이 있었어도, 체크박스를 누르면 state가 사라진다. 다시 누르면 0
으로 새로 생김.
리액트는 state를 UI 트리에서 자기 자리에 그려져 있을 때만 보존한다. 컴포넌트가 사라지거나 다른 컴포넌트가 그 자리에 그려지면, 리액트는 그 state를 버림.
Counter 컴포넌트를 다시 두개 그려보자.
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>
);
}
체크박스를 누르면 isFancy
가 바뀌어서 삼항연산자가 작동한다.
삼항연산자에 의해 그 자리에 다른 Counter 컴포넌트가 렌더링되는데, 어찌됐건 UI 트리에서 같은 자리에 같은 함수가 호출되기 때문에, 리액트는 state를 보존함.
마치 같은 "주소 : root의 N번째 자식의 M번째 자식..."를 가진 것과 같다. 리액트가 JSX 값이나 컴포넌트 함수 그 자체를 읽고 판단하는 것이 아님!
삼항연산자 자리에 같은 컴포넌트가 아니라, false 일 경우 <p>
태그를 렌더링한다고 치면, state는 버려진다.
물론 해당 컴포넌트가 자식이 있는 경우, 그 subtree의 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>
</>
);
}
MyComponent
가 부모, MyTextField
는 자식 컴포넌트.setState
가 작동하여 렌더링이 다시 일어남.MyTextField
컴포넌트가 렌더링 된다고 생각함.Default : 같은 위치 같은 컴포넌트면 state 보존. 보통은 이렇게 하기를 원할 것임.
그런데 리셋하고 싶으면 어떡함? 다음 예시를 보자.
플레이어가 두명인데, 턴 돌아가면서도 자신의 score를 기억한다.
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>
);
}
person
props)가 바뀌는데, 컴포넌트 위치는 그대로다.score
state를 렌더링해야 함.score
유지됨.score
state가 하나씩 올라감.그러니까 score
state가 두개여야 한다는 말.
state를 리셋하는 방법은 두가지가 있다 :
key
부여.화면 상 다른 위치에 그리자는 말이 아님.
<div>
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
조건부 렌더링을 활용하면, react DOM 내에서의 위치를 바꿀 수 있는 것이다.
이러면 각 컴포넌트의 state가 DOM에서 내려갈 때마다 삭제된다.
버튼을 누를 때마다 score
state는 초기값 0
으로 리셋될 것.
화면상 같은 위치에 렌더링되는 컴포넌트 개수가 적으면 좋은 방법이다. 이 경우 두개니까 이렇게 해도 괜찮음.
state를 리셋하는데 더 범용성 있는 방법이 있다.
key
는 리스트 렌더링할 때 각 element를 구분하기 위해 사용한 적이 있을 것이다. 그러나 key
는 리스트에만 쓰는 것이 아니라 리액트에서 구분이 필요한 모든 것에 사용함! Default로 리액트는 부모의 N번째 자식으로 컴포넌트를 구별한다. 그러나 key
를 사용하면 이 컴포넌트가 "그냥 첫번째 자식", "두번째 자식" 이 아니라, Taylor의 컴포넌트라는 것을 명시하게 된다!
이러면 트리 내 어디에 그려지든 리액트가 이 컴포넌트를 구별하여 인지할 수 있다.
예시 코드는 이렇다
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
key
를 지정해 주면 리액트한테 key
그 자체를 위치의 일부로 사용하라고 알려주는 셈이다. 그래서 리액트가 같은 위치에 같은 컴포넌트가 렌더링 돼도, 서로 다른 컴포넌트라는 것을 알 수 있다. 이제 컴포넌트가 내려가면 state는 즉시 삭제되고, 그려질 때마다 새로 선언됨.
단!
key
가 전역에서 unique한 것은 아님!
부모 내의 위치만 특정해주는 것임! = 같은 부모 내에서만 unique 유효함
key
로 state를 리셋하는 것이 Form에서 특히 좋다!
는 생략
컴포넌트가 사라져도 state를 보존하려면?
- 모든 컴포넌트를 렌더링은 하되, CSS로 숨겨놓기
ex. display : none
이렇게 해도 react tree에서 사라지지는 않는다!- State 끌어올리기 : 공통 부모에서 state를 선언하면, 자식이 없어져도 prop이 사라지는 것 뿐, state는 부모 컴포넌트에 잘 살아있을 것이다. 가장 많이 쓰는 방법!
- state 외에 다른 source 쓰기 :
ex.localStorage
. 사용자가 브라우저를 껐다 켜도 살아있을 수 있다...