렌더링과 리렌더링
Article
지금까지 "state가 바뀌면 React가 다시 그린다"고 했다. 편의상 그렇게 말했을 뿐 정확하지 않다.
렌더링은 화면을 그리는 일이 아니다. 컴포넌트 함수를 실행하는 일이다.
세 단계로 나뉜다
React가 화면을 갱신하는 과정은 셋으로 나뉜다.
- 트리거다시 그릴 이유가 생긴다
- 렌더컴포넌트 함수를 실행한다
- 커밋실제 DOM 을 고친다
| 단계 | 무슨 일이 | 누가 |
|---|---|---|
| 트리거 | setState 호출 또는 부모의 리렌더 | 우리 코드 |
| 렌더 | 함수를 실행해 JSX 를 얻는다 | React |
| 커밋 | 이전 결과와 비교해 바뀐 DOM 만 고친다 | React |
렌더 단계에서는 DOM을 건드리지 않는다. 그냥 함수를 부르고 반환된 JSX 객체를 받아둔다. 3강에서 JSX가 그냥 객체라고 했던 게 여기서 쓰인다.
실제 화면이 바뀌는 건 커밋 단계다. 그리고 바뀐 게 없으면 커밋에서 아무것도 안 한다.
언제 다시 실행되나
이유는 두 가지뿐이다.
자기 state 가 바뀜
setState 호출
- 값이 실제로 달라져야 한다
Object.is로 비교한다- 같은 값을 넣으면 건너뛴다
부모가 리렌더링됨
props 와 무관하게
- props 가 안 바뀌어도 다시 실행된다
- 자식의 자식까지 전부
- React.memo 로 막을 수 있다
두 번째가 초보자를 놀라게 한다. 부모가 다시 실행되면 자식도 전부 다시 실행된다. props가 그대로여도 그렇다.
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Child name="지원" />
</div>
);
}
function Child({ name }) {
console.log("Child 실행");
return <p>{name}</p>;
}
버튼을 누를 때마다 콘솔에 "Child 실행"이 찍힌다. name은 한 번도 안 바뀌었는데도.
- Parent
- Child
- GrandChild
- Child
상태는 위치로 기억된다
7강과 11강에서 미뤄둔 이야기다. React는 컴포넌트의 상태를 어디에 매달아 둘까.
화면 트리에서의 위치다.
function App() {
const [showFirst, setShowFirst] = useState(true);
return (
<div>
{showFirst ? <Counter /> : <Counter />}
<button onClick={() => setShowFirst(s => !s)}>전환</button>
</div>
);
}
버튼을 눌러 다른 <Counter />로 바꿔도 카운트가 유지된다. 같은 위치에 같은 타입의 컴포넌트가 있으니 React는 같은 것으로 본다.
반대로 위치가 달라지면 상태가 날아간다.
{showFirst ? <Counter /> : <div><Counter /></div>}
이제는 초기화된다. div가 하나 끼어들어 위치가 달라졌기 때문이다.
- 01
같은 위치에 있는가
부모 기준으로 몇 번째 자식인지가 같아야 한다.
- 02
같은 타입인가
Counter였다가Timer가 되면 다른 것이다. 상태를 버린다. - 03
key 가 같은가
key가 있으면 위치보다key를 먼저 본다. 11강에서 상태를 초기화하던 그 방법이다.<Counter key={userId} />
7강에서 "조건부 렌더링으로 컴포넌트를 없애면 상태가 사라진다"고 했던 이유가 이것이다. 화면에서 빠지면 그 위치가 사라지고, React는 매달아둔 상태를 버린다.
렌더 함수는 순수해야 한다
렌더 단계에서 지켜야 할 규칙이 있다. 같은 입력이면 같은 출력을 내야 하고, 바깥 세상을 건드리면 안 된다.
let count = 0;
function Bad() {
count++;
return <p>{count}</p>;
}
컴포넌트 밖의 변수를 고쳤다. 실행할 때마다 결과가 달라진다. React가 언제 몇 번 실행할지는 우리가 정하지 않으므로, 이런 코드는 예측 불가능해진다.
function AlsoBad({ items }) {
items.push("새 항목");
return <ul>…</ul>;
}
props를 고쳤다. 5강과 12강에서 금지한 그것이다.
| 렌더 중에 하면 안 되는 일 | 대신 |
|---|---|
| 바깥 변수 수정 | state 로 관리한다 |
| props · state 직접 수정 | 새 객체를 만든다 |
| setState 호출 | 이벤트 핸들러나 useEffect 에서 |
| DOM 직접 조작 | useEffect 나 ref 에서 |
| 서버 요청 | useEffect 에서 (15강) |
| Math.random() · Date.now() | 렌더 밖에서 계산해 넘긴다 |
렌더 중에 setState를 부르면 무한 루프가 된다.
function Infinite() {
const [count, setCount] = useState(0);
setCount(count + 1);
return <p>{count}</p>;
}
- Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
렌더 → setState → 트리거 → 렌더 → … 끝이 없다. React가 알아서 멈추고 에러를 던진다.
StrictMode 가 두 번 실행하는 이유
개발 중에 console.log가 두 번 찍혀 당황한 적이 있을 것이다.
<StrictMode>
<App />
</StrictMode>
StrictMode는 개발 환경에서만 컴포넌트 함수를 일부러 두 번 실행한다.
순수하지 않은 컴포넌트를 찾아내려는 것이다. 위의 count++ 같은 코드가 있으면 두 번 실행했을 때 결과가 달라지므로 즉시 티가 난다.
정리하면
- 렌더링은 세 단계다. 트리거 → 렌더(함수 실행) → 커밋(DOM 수정).
- 렌더 단계는 DOM을 건드리지 않는다. 리렌더링이 일어나도 화면이 안 바뀔 수 있다.
- 리렌더링의 원인은 둘뿐이다. 자기 state가 바뀌거나, 부모가 다시 실행되거나.
- 부모가 다시 실행되면 props가 그대로여도 자식이 전부 다시 실행된다.
- React는 상태를 화면 트리의 위치로 기억한다. 위치가 바뀌면 상태를 버린다.
- 렌더 함수는 순수해야 한다. 바깥을 건드리지 않고, 같은 입력에 같은 출력을 낸다.
StrictMode의 이중 실행은 버그가 아니라 순수성 검사다.
다음 강의에서는 커밋 단계에서 React가 무엇을 어떻게 비교하는지 본다. 가상 DOM이다.