1. JSX에 대한 고찰
책에서 JSX는 기본적으로 JSXElement, JSXAttributes, JSXChildren, JSXStrings 라는 4가지 구조적 단위를 기반으로 구성되어 있고 각 의미들에 대해 자세히 설명하고 있다. 그런데, 그다지 중요한 내용들은 아닌 거 같다.
JSX에 대해 몇 가지 알아야 될 중요한 점은 다음과 같다고 본다.
- 브라우저는 JSX를 이해할 수 없으므로 반드시 바벨과 같은 도구를 통해 JS로 트랜스파일을 거쳐야 된다.
- JSX는 당연하게도 주로 리액트에서 사용하지만, 트랜스파일만 가능하다면 다른 곳에서도 사용될 수 있다.
- JSX는 자바스크립트 내부에서 표현하기 까다로웠던 XML 스타일의 트리 구문을 작성하는데 도움을 주는 문법적 설탕이다.
특히 위의 3번에서 리액트가 왜 JSX를 선택했는지가 드러난다.
리액트의 철학은 절차가 아닌 관계에 집중하는 선언형 프로그래밍에 있다.
이 철학을 가장 잘 드러내는 것이 바로 JSX로, UI 간의 관계를 직관적으로 표현하기에 탁월한 문법이다.
// 내부를 보지 않아도 유저의 프로필 안에 유저 사진, 정보가 있다는 것이 관계적으로 드러난다
<UserProfile>
<Avatar />
<UserInfo />
</UserProfile>
// 코드만 봐도 users 배열의 각 객체가 UserCard 컴포넌트 하나로 매핑된다는 사실이 바로 읽힌다.
// JSX를 이용하면 데이터 구조와 UI 구조의 관계를 드러내어 선언적으로 프로그래밍할 수 있다
const UserList = ({ users }) => (
<section>
{users.map(user => (
<UserCard
key={user.id}
name={user.name}
email={user.email}
/>
))}
</section>
);
JSX 트랜스파일링 결과, 브라우저 렌더링 과정, 가상돔에 관한 내용은 이전 블로그 글인 가상돔 만들기에서 보았으므로 여기서 서술하는 것은 스킵한다.
[React] 바닐라JS로 가상돔(VirtualDOM) 만들기
현대 프론트엔드 프레임워크를 대표하는 기술은 단연코 리액트일 것이다. 물론, 리액트는 공식적으로 라이브러리지만 사실상 프레임워크라 해도 어색하지 않다. 일부 시각에서는, 바닐라JS보
junhee1203.tistory.com
2. 리액트 파이버의 도입
React 15까지는 트리 구조인 가상 DOM의 노드는 단순히 React.createElement의 결과로 생성된 평범한 자바스크립트 객체였다.
재조정(Reconciliation) 과정은 이전 가상 DOM과 새로운 가상 DOM을 객체 단위로 비교(diff) 하는 방식으로 동작했다.
따라서 이 비교 과정이 동기적으로 한 번에 수행되었기 때문에, 렌더링 중 사용자 입력이나 애니메이션 같은 UI 이벤트가 발생해도 즉시 처리할 수 없는 문제가 있었다.
이러한 한계를 해결하기 위해 React 16에서 새로운 아키텍처인 Fiber가 도입되었다.
Fiber는 가상 DOM의 노드지만 이전과 달리 평범한 객체가 아니라 작업 단위로 쪼개어 관리할 수 있는 특별한 객체이다.
이를 통해 React는 렌더링을 작은 단위로 나누어 수행하고, 필요할 때 작업을 일시 중단하거나 재개할 수 있게 되었다.
그 결과, 렌더링 중에도 브라우저가 사용자 입력이나 애니메이션 같은 우선순위가 높은 작업을 먼저 처리할 수 있게 되었다.
function App() {
return (
<div>
<Header />
<UserList />
<ChatWindow />
</div>
);
}
이 구조는 내부적으로 다음과 같은 파이버 트리로 관리된다.
App (Fiber)
├─ Header (Fiber)
├─ UserList (Fiber)
└─ ChatWindow (Fiber)
만약 UserList의 업데이트가 매우 크고 오래 걸리면, 이제 React는 파이버를 통해 Header랑 ChatWindow는 우선 렌더링하고,
UserList는 나중에 렌더링을 하며 스케줄링을 조절할 수 있는 것이다.
3. 파이버를 이용한 렌더링 전략
파이버의 도입 전까지 리액트의 렌더링 과정은 아래와 같았다.
- React가 루트부터 전체 가상 DOM 트리를 순회하며 diff 계산을 함.
- 이 작업은 하나의 커다란 동기 함수 호출 (스택 재귀 기반).
- 중간에 멈출 수 없음 → 브라우저가 100ms 이상 멈춰도 React는 끝까지 계산함.
따라서, 렌더링 중에 사용자 인터페이스(클릭, 입력)가 무시되는 현상이 발생하였다.
파이버의 도입 이후 리액트는 렌더링 전략을 "한번에 이전과 달라진 모든 것을 계산하는 방식"에서 "파이버라는 노드 하나하나가 작업 단위가 되며, 부분적으로 쪼개어 이전과 달라진 점을 계산하는 방식"으로 변경하였다.
작업의 단위를 쪼개다 보니, 렌더링 중 사용자 인터페이스가 들어오면 이것을 대응하기 쉬워진 것이다.
파이버를 도입한 이후 리액트의 렌더링 과정은 아래와 같다.
- 루트에서부터 Fiber 노드를 하나씩 처리
- Fiber 하나를 처리할 때마다 “지금 브라우저가 바쁜가?”를 확인
- 만약 브라우저가 바쁘지 않은 상태라면 → 다음 Fiber 처리
- 만약, 브라우저가 사용자 입력/애니메이션 등 더 중요한 일이 있다면 →
React는 현재 작업을 일시 중단 하고, 나중에 이어서 처리
즉 이것은, CPU의 스케줄링과 매우 유사한 방식이다.
4. 파이버로 인해 발생한 오해
파이버를 알아보다 보니 한 가지 의문점이 들었다. 필자가 알기로는 분명 리액트는 상태의 변화를 일괄적으로 모으고 하나의 스냅샷으로 한 번에 배치처리하는 것으로 알고 있었다.
// 배치처리의 유명한 예시
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
console.log(count) //마운트시 : 0 , 클릭시 3이 아니라 1이 출력된다
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>+3</button>
</div>
);
}
그런데 Fiber는 렌더링을 이전과 달리 세밀한 작업 단위로 쪼개어 처리한다고 했다.
이 점이 React의 업데이트가 한 번에 배치 처리(batch update) 된다는 특성과 다소 상충되는 것처럼 보였고, 알아보니
사실 두 개념은 서로 다른 단계에서 작동하는 것을 알게되었다.
React의 렌더링 과정은 크게 두 단계로 나뉜다.
- 렌더 단계(Render Phase)
- 커밋 단계(Commit Phase)
이름만 보면 렌더 단계에서 화면을 그리는 과정이라고 오해하기 쉽지만, 실제로 렌더 단계는 재조정(Reconciliation) 과정에 해당한다.
즉, 이전 가상 DOM과 새로운 가상 DOM을 비교하여 “무엇이 어떻게 달라졌는가”를 계산하는 준비 단계다.
반면, 커밋 단계는 렌더 단계에서 계산된 결과를 바탕으로 실제 DOM을 조작하고, 변경된 내용을 화면에 반영하는 단계다.
우리가 일반적으로 ‘렌더링되어 화면에 그려진다’고 생각하는 일은 바로 이 커밋 단계에서 일어난다.
Fiber는 커밋 단계가 아닌 렌더 단계에 관여한다.
React는 Fiber를 통해 렌더링 작업을 작은 단위로 나누어 처리하며, 필요할 경우 브라우저의 상태에 따라 작업을 일시 중단하거나 나중에 재개할 수 있다. 즉, Fiber는 “렌더 단게에서 diff 계산을 효율적으로 수행하기 위한 내부 스케줄링 메커니즘”이다.
반면, 배치 처리(batch update) 는 Fiber와 달리 커밋 단계에서 이루어진다. Fiber가 렌더 단계에서 계산해낸 여러 변경 사항(diff)을 모두 모아, React는 커밋 단계에서 이들을 한 번에 DOM에 반영한다.
그래서 React는 “계산은 잘게 나누어 처리하지만, 실제 화면 반영은 한 번에 수행”하는 구조를 갖게 된다.
결국 Fiber와 배치 처리는 서로 상충되는 개념이 아니라, React의 렌더링을 효율적이고 부드럽게 만드는 두 축이다.
Fiber는 ‘어떻게 계산할 것인가’ 를, 배치 처리는 ‘언제 반영할 것인가’ 를 다루며, 이 둘이 함께 동작함으로써 React는 성능과 반응성을 모두 잡은 렌더링 모델을 완성하게 된 것이다.