[React] 리액트 렌더링 동작 과정
리액트 렌더링 동작은 3가지 과정으로 나뉜다
1. 렌더링 트리거
2. 컴포넌트 렌더링
3. DOM 커밋
렌더링이 일어나는 이유
리액트에서 컴포넌트 렌더링이 일어나는 이유는 2가지가 있습니다.
1. 컴포넌트의 초기 렌더링
2. 컴포넌트의 state가 없데이트 된 경우
1. 렌더링 트리거
렌더링 트리거란 리액트에 렌더링을 요청하는 과정을 의미합니다.
컴포넌트 초기 렌더링이란 아래 코드와 같이 앱을 시작할 때의 초기 렌더링을 의미합니다.
따라서 리액트는 앱을 시작할 때 초기 렌더링을 트리거 합니다.
import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Image />);
컴포넌트의 state가 없데이트 된 경우는 초기 렌더링 이후 set 함수를 통해 상태를 업데이트 함으로써 발생한 리렌더링을 의미합니다.
이 때 상태(state)가 업데이트 되면 렌더링이 트리거 되고 자동으로 렌더링 대기열에 추가됩니다.
2. 컴포넌트 렌더링
렌더링을 트리거한 후 React는 컴포넌트를 호출하여 화면에 표시할 내용을 파악합니다.
“렌더링”은 React에서 컴포넌트를 호출하는 것입니다.
컴포넌트 렌더링은 재귀적 단계로 진행됩니다.
쉽게 말하자면 렌더링 트리거 된 컴포넌트를 렌더링 할 때 해당 컴포넌트 내부에 있는 컴포넌트도 렌더링합니다.
컴포넌트 렌더링은 초기 렌더링과 리렌더링으로 나뉩니다.
초기 렌더링에서는 컴포넌트 내부 태그에 대한 DOM노드를 생성합니다.
리렌더링에서는 이전 렌더링 이후 변경된 속성을 저장해놓습니다.(해당 정보는 커밋단계까지는 사용되지 않습니다.)
DOM 커밋
초기 렌더링의 경우 리액트는 appendChild() DOM API를 사용하여 생성한 모든 DOM 노드를 화면에 표시합니다.
리렌더링의 경우 리액트는 필요한 최소한의 작업(이전 렌더링과 변경된 속성)을 적용하여 DOM이 최신 렌더링 출력과 일치하도록 합니다.
리액트는 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경합니다.
예를 들어 아래 코드에서는 매초 부모로부터 전달된 다른 props로 다시 렌더링하는 컴포넌트가 있습니다.
<input>에 텍스트를 입력하여 value를 업데이트 하지만 컴포넌트가 리렌더링될 때 텍스트가 사라지지 않습니다.
이는 리액트가 <h1>의 내용만 새로운 time으로 업데이트하기 때문입니다. <input>이 JSX에서 이전과 같은 위치로 확인되므로 React는 <input> 또는 value를 건드리지 않습니다!
export default function Clock({ time }) {
return (
<>
<h1>{time}</h1>
<input />
</>
);
}
렌더링 과정 최종 정리
아래 코드를 예시로 설명하겠습니다.
export default function Gallery() {
const [text, setText] = useState("first Text");
return (
<section>
<h1>{text}</h1>
<Image />
<Image />
<Image />
</section>
);
}
1. [렌더링 트리거]앱을 시작할 때 초기 렌더링이 트리거됩니다.
2. [컴포넌트 렌더링]초기 렌더링 시 리액트는 루트 컴포넌트를 호출하고 루트 컴포넌트 내부 컴포넌트를 재귀적으로 초기 렌더링합니다.(
초기 렌더링을 하는 동안 리액트는 내부 태그에 대한 DOM 노드를 생성합니다.
3. [렌더링 트리거]어떤 컴포넌트의 상태(text)가 set함수로 인해 업데이트 된다면 리액트는 Gallery 컴포넌트를 렌더링 트리거합니다.
4. [렌더링 트리거]렌더링 트리거 된 컴포넌트는 자동으로 렌더링 대기열에 추가됩니다.
5. [컴포넌트 렌더링]Gallery 컴포넌트가 리렌더링되며 리렌더링 과정에서 리액트는 이전 렌더링 이후 변경된 속성을 저장해놓습니다.(해당 정보는 커밋단계까지는 사용되지 않습니다.)
6. [DOM 커밋] 리액트는 이전 렌더링과 변경된 속성을 적용하여 DOM이 최신 렌더링 출력과 일치하도록 합니다.
이 때, 리액트는 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경합니다.
추가
설명을 쉽게 이해할 수 있도록, React의 초기 렌더링과 리렌더링 과정에 대해 예시를 들어 설명해드리겠습니다.
초기 렌더링
초기 렌더링 시, React는 컴포넌트를 처음 화면에 그릴 때 `appendChild()` 같은 DOM API를 사용하여 컴포넌트에 포함된 모든 DOM 노드를 화면에 추가합니다.
export default function Clock({ time }) {
return (
<>
<h1>{time}</h1>
<input />
</>
);
}
위 코드를 예로 들어 설명하면, 처음 `<Clock time="10:00" />`를 렌더링하면 다음과 같은 DOM이 생성됩니다.
<h1>10:00</h1>
<input />
React는 이 DOM 노드를 생성하고 `appendChild()`를 통해 화면에 표시합니다.
리렌더링
리렌더링 시, React는 현재 DOM과 새로운 렌더링 결과 간의 차이를 계산합니다. 이 과정은 "Virtual DOM"이라는 개념을 사용하여 수행됩니다. 리렌더링이 필요한 경우, React는 변경된 부분만 업데이트합니다. 즉, 필요하지 않은 부분은 다시 렌더링하지 않습니다.
다음과 같이 `time` props가 변경되는 경우를 생각해봅시다.
<Clock time="10:01" />
이 경우, React는 `<h1>10:00</h1>`이 `<h1>10:01</h1>`으로 변경되어야 한다는 것을 알고, 이 부분만 업데이트합니다. `<input />` 요소는 변경되지 않으므로 그대로 유지됩니다.
<h1>10:01</h1>
<input />
왜 `<input>`의 텍스트가 사라지지 않을까요?
리렌더링 시, `<input>` 요소가 다시 그려지지 않는 이유는 `<input>`의 `value` 속성이 props나 state로 제어되지 않기 때문입니다. 즉, `<input>` 요소의 내부 상태는 사용자가 입력한 텍스트를 그대로 유지합니다.
만약 `value` 속성을 props나 state로 제어했다면, 리렌더링 시 텍스트가 사라지지 않도록 추가 작업이 필요할 수 있습니다. 하지만, 현재 예제에서는 `<input>` 요소가 비제어 컴포넌트이므로 사용자의 입력값은 React 상태와는 별도로 유지됩니다.
요약
1. **초기 렌더링**: React는 `appendChild()`를 사용하여 모든 DOM 노드를 화면에 표시합니다.
2. **리렌더링**: React는 Virtual DOM을 사용하여 변경된 부분만 업데이트합니다. 변경되지 않은 부분은 그대로 유지됩니다.
3. **비제어 컴포넌트**: `<input>` 요소와 같이 비제어 컴포넌트의 경우, 사용자가 입력한 값은 React 상태와 별개로 유지됩니다. 따라서 리렌더링 시에도 텍스트가 사라지지 않습니다.