06. React
JavaScript와 브라우저 위에서 React는 무엇을 하는가?
지금까지 브라우저에서 웹페이지가 만들어지고 동작하는 과정을 살펴봤다.
처음에는 네트워크부터 시작했다.
URL
↓
DNS
↓
IP
↓
TCP / QUIC
↓
TLS
↓
HTTP
↓
HTML / CSS / JS그리고 브라우저가 HTML과 CSS를 해석하고 화면을 만드는 과정도 살펴봤다.
HTML
↓
DOM
CSS
↓
CSSOM
DOM + CSSOM
↓
Render Tree
↓
Layout
↓
Paint
↓
CompositeJavaScript는 그 위에서 실행된다.
JavaScript
↓
Execution Context
↓
Call Stack
↓
Event Loop
↓
Web API
↓
Task / Microtask그리고 무거운 작업은 필요하다면 Worker로 분리할 수 있다.
그렇다면 React는 어디에 있을까?
React는 브라우저를 대신하는 것도 아니고,
JavaScript 엔진을 대신하는 것도 아니다.
React는 기본적으로 JavaScript로 UI를 구성하고 관리하기 위한 라이브러리다.
따라서 React를 이해하려면 먼저
React는 브라우저 위에서 동작한다.
는 사실부터 잡아야 한다.
1. React는 브라우저를 대신하지 않는다
React를 처음 배울 때 흔히
React가 화면을 그린다.라고 생각하기 쉽다.
하지만 정확하게 보면 React가 브라우저의 렌더링 엔진을 대신하는 것은 아니다.
React가 하는 일과 브라우저가 하는 일은 다르다.
React
컴포넌트
State
Props
Render
DOM 변경 사항 계산
↓
──────────────────
↓
Browser
DOM
Layout
Paint
Composite
↓
화면즉,
React는 "무엇이 바뀌어야 하는가"를 관리하고, 브라우저는 실제 DOM을 바탕으로 화면을 그린다.
이 차이를 이해하는 것이 React를 제대로 이해하는 첫 번째 단계다.
2. React가 없던 시절에는 어떻게 UI를 만들었을까?
React를 이해하려면 먼저 일반적인 JavaScript로 DOM을 직접 조작하는 방식을 생각해보자.
예를 들어
<div id="counter">0</div>
<button id="button">+</button>가 있다고 하자.
JavaScript로 직접 처리하면
const counter = document.querySelector("#counter");
const button = document.querySelector("#button");
let count = 0;
button.addEventListener("click", () => {
count += 1;
counter.textContent = count;
});이렇게 작성할 수 있다.
여기서는 개발자가 직접
상태 변경
↓
DOM 변경을 연결해야 한다.
count가 변경되면
counter.textContent = count;를 직접 호출해야 한다.
UI가 복잡해지면 이 연결도 점점 복잡해진다.
3. React는 UI를 상태의 결과로 생각한다
React에서는 조금 다른 방식으로 생각한다.
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>
+
</button>
</div>
);
}여기서 개발자가 직접
document.querySelector(...)
element.textContent = ...같은 DOM 조작을 하지 않는다.
대신
state
↓
UI라는 관계를 선언한다.
즉,
"현재 state가 이 값이라면 UI는 이렇게 보여야 한다."
라고 작성하는 것이다.
4. React에서 중요한 것은 State다
React의 핵심적인 흐름을 단순하게 표현하면
State
↓
React
↓
UI이다.
예를 들어
const [count, setCount] = useState(0);초기 상태가
count = 0이라면 UI는
0을 보여준다.
그리고
setCount(1);이 실행되면 상태가 변경된다.
그러면 React는 이 변경을 바탕으로 UI를 다시 계산한다.
count = 0
↓
UI
↓
setCount(1)
↓
count = 1
↓
React Render
↓
필요한 변경사항 반영
↓
DOM
↓
Browser Rendering여기서 중요한 것이 React Render다.
5. React Render란 무엇인가?
React에서 "Render"라는 단어가 자주 등장한다.
그런데 여기서 조심해야 한다.
React의 Render와 브라우저의 Rendering은 같은 의미가 아니다.
React Render
React가 현재 상태와 Props를 바탕으로
어떤 UI 구조가 필요한지 계산하는 과정
이다.
Browser Rendering
브라우저가 실제 DOM과 스타일 등을 바탕으로
Layout
Paint
Composite등을 수행해서 화면을 만드는 과정이다.
둘을 구분하면
React Render
↓
React가 UI 변경을 계산
↓
DOM 반영
↓
Browser Rendering
↓
실제 화면이라는 흐름이 된다.
6. React Render가 발생했다고 화면이 바로 그려지는 것은 아니다
이 부분도 중요한 오해다.
React Render
=
화면 다시 그리기가 아니다.
React Render는 React가 UI를 계산하는 과정이다.
그 이후 실제 DOM에 필요한 변경이 반영되고, 브라우저가 필요에 따라 렌더링 작업을 수행한다.
그래서
State 변경
↓
React Render
↓
Commit
↓
DOM 변경
↓
Browser Rendering
↓
화면처럼 구분해서 생각하면 좋다.
7. Render와 Commit
React의 업데이트 과정을 이해할 때 Render와 Commit을 구분하는 것이 중요하다.
간단하게 보면
Render Phase
↓
무엇이 바뀌었는지 계산
Commit Phase
↓
실제 DOM에 변경 반영이다.
예를 들어
function Counter() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}처음에는
count = 0이다.
사용자가 버튼을 눌러
setCount(1);이 실행되었다고 하자.
React는 업데이트된 상태를 기준으로 컴포넌트를 다시 실행하고 UI를 계산한다.
count = 1
↓
React Render
↓
새로운 UI 계산그리고 필요한 변경을 Commit 단계에서 실제 DOM에 반영한다.
Commit
↓
DOM 변경그다음 브라우저가 화면 업데이트에 필요한 작업을 수행한다.
8. 그렇다면 Virtual DOM은 무엇일까?
React를 설명할 때 빠지지 않고 등장하는 것이 Virtual DOM이다.
Virtual DOM을 처음 접하면
"실제 DOM을 복사해놓은 가짜 DOM"
이라고 생각하기 쉽다.
하지만 이것만으로 설명하면 React의 동작을 오해하기 쉽다.
React는 컴포넌트가 반환하는 UI 구조를 바탕으로 내부적으로 UI에 대한 표현을 관리하고, 업데이트 과정에서 이전 결과와 새로운 결과를 비교해 필요한 변경을 계산한다.
개념적으로 단순화하면
이전 UI
↓
새로운 UI
↓
비교
↓
필요한 변경 계산
↓
DOM 반영이라고 볼 수 있다.
예를 들어
<div>
<h1>Hello</h1>
<p>Count: 0</p>
</div>에서
Count: 0이
Count: 1로 변경되었다고 하자.
React 입장에서는 전체 DOM을 무조건 다시 만드는 것이 아니라 변경이 필요한 부분을 찾아 실제 DOM에 반영하는 방향으로 업데이트를 수행한다.
9. React의 핵심은 "DOM을 직접 조작하지 않는다"가 아니다
React를 배우면서 흔히 이런 말을 듣는다.
"React는 DOM을 직접 조작하지 않는다."
이 표현도 정확하게 이해할 필요가 있다.
React도 결국 브라우저의 DOM을 변경해야 화면을 업데이트할 수 있다.
차이는
개발자가 직접 DOM 변경을 관리하는 대신
개발자
↓
상태와 UI 관계를 선언
↓
React
↓
필요한 DOM 변경 계산 및 반영하도록 만든다는 것이다.
즉,
DOM을 사용하지 않는 것이 아니라, DOM 변경을 직접 관리해야 하는 부담을 React가 추상화한다.
라고 이해하는 것이 좋다.
10. React 컴포넌트는 결국 JavaScript 함수다
React의 컴포넌트를 다시 생각해보자.
function Greeting() {
return <h1>Hello</h1>;
}겉으로 보면 React 컴포넌트지만 JavaScript 관점에서는 함수다.
React는 이 함수를 실행하면서 UI에 대한 결과를 얻는다.
Greeting()
↓
<h1>Hello</h1>그래서 React를 이해할 때 JavaScript의 기본 개념이 중요하다.
앞에서 배웠던
Execution Context
Call Stack
Event Loop
Main Thread가 모두 연결된다.
React도 결국 JavaScript 위에서 실행되기 때문이다.
11. 컴포넌트가 실행된다는 것
다음 코드를 보자.
function User({ name }) {
return <div>{name}</div>;
}React가 이 컴포넌트를 렌더링할 때 개념적으로는
User({ name: "Yonghee" })
↓
<div>Yonghee</div>처럼 컴포넌트의 결과를 계산한다.
그리고 상태가 변경되어 다시 렌더링되면 컴포넌트 함수가 다시 실행될 수 있다.
첫 번째 Render
↓
User()
↓
UI 계산
State 변경
↓
두 번째 Render
↓
User()
↓
새로운 UI 계산그래서 React 개발에서는
컴포넌트 함수가 언제 다시 실행되는가?
가 매우 중요한 질문이 된다.
이 질문이 다음 글에서 다룰 Re-render와 React 성능으로 이어진다.
12. State가 변경되면 무슨 일이 일어날까?
가장 중요한 흐름을 하나 만들어보자.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}처음 화면에는
0이 보인다.
사용자가 버튼을 클릭한다.
사용자 클릭
↓
이벤트 핸들러 실행
↓
setCount(count + 1)그러면 React에 상태 업데이트가 요청된다.
State Update
↓
React RenderReact는 새로운 상태를 기준으로 UI를 계산한다.
count = 1
↓
<button>1</button>이전 결과와 비교하고 필요한 변경을 Commit한다.
Commit
↓
DOM의 텍스트 변경그리고 브라우저가 필요한 화면 업데이트를 수행한다.
Browser Rendering
↓
화면에 1 표시전체 과정은
Click
↓
Event Handler
↓
State Update
↓
React Render
↓
Commit
↓
DOM Update
↓
Browser Rendering이다.
이 흐름을 이해하면 React의 많은 개념이 연결되기 시작한다.
13. State가 변경되면 DOM 전체를 다시 만드는가?
그렇지는 않다.
예를 들어
function App() {
return (
<div>
<h1>Hello</h1>
<p>Count: 1</p>
</div>
);
}여기서 Count만 변경되었다고 하자.
Count: 0
↓
Count: 1React는 업데이트 과정에서 필요한 변경을 계산하고 DOM에 반영한다.
개념적으로
기존 UI
┌─────────────┐
│ Hello │
│ Count: 0 │ ← 변경
└─────────────┘
새 UI
┌─────────────┐
│ Hello │
│ Count: 1 │ ← 변경
└─────────────┘이렇게 차이를 찾아 필요한 부분을 반영한다고 생각하면 된다.
다만 여기서도
"React는 항상 DOM 변경을 최소화한다."
라고 단순하게 단정하기보다는, React의 reconciliation과 commit 과정을 통해 필요한 변경을 계산하고 반영한다고 이해하는 것이 좋다.
14. Props는 무엇일까?
React에서는 State뿐만 아니라 Props도 중요한 역할을 한다.
function User({ name }) {
return <div>{name}</div>;
}
function App() {
return <User name="Yonghee" />;
}여기서
App
↓
User이고,
name="Yonghee"가 Props다.
Props는 부모 컴포넌트가 자식 컴포넌트에 전달하는 값이다.
Parent
│
│ props
↓
Child그리고 Props가 변경되면 자식 컴포넌트의 결과도 달라질 수 있다.
15. State와 Props를 함께 보면
React의 UI는 대략
State
+
Props
↓
Component
↓
UI라고 생각할 수 있다.
예를 들어
function User({ name }) {
const [count, setCount] = useState(0);
return (
<div>
<p>{name}</p>
<p>{count}</p>
</div>
);
}이 컴포넌트의 UI는
Props
name
+
State
count
↓
Render
↓
UI의 결과라고 볼 수 있다.
16. Re-render는 무엇일까?
이제 React 성능에서 가장 중요한 개념 중 하나로 넘어갈 수 있다.
Re-render는 컴포넌트가 다시 렌더링되는 것이다.
예를 들어
setCount(count + 1);이 실행되면 해당 업데이트에 영향을 받는 React 트리가 다시 렌더링될 수 있다.
이때 중요한 것은
Re-render
≠
DOM 전체 다시 그리기라는 것이다.
Re-render는 React가 컴포넌트 함수를 다시 실행하고 새로운 UI 결과를 계산하는 과정과 관련된다.
그 후 React가 필요한 DOM 변경을 Commit한다.
17. Re-render와 Browser Rendering을 구분하자
이것은 이번 글에서 반드시 기억해야 하는 부분이다.
React Re-render
→ React가 UI를 다시 계산
Browser Rendering
→ 브라우저가 실제 화면을 업데이트둘은 서로 다른 과정이다.
예를 들어 State가 변경되었다고 해서
State 변경
↓
무조건 전체 화면 다시 Paint가 되는 것은 아니다.
React가 업데이트를 계산하고 실제 DOM에 필요한 변경이 반영된 후, 브라우저가 필요한 렌더링 작업을 수행한다.
18. 그렇다면 React가 빠른 이유는 무엇일까?
React를 소개할 때 흔히
"Virtual DOM이라서 빠르다."
라고 설명한다.
하지만 이것만으로는 부족하다.
React의 성능을 이해하려면 여러 요소를 함께 봐야 한다.
필요한 UI 업데이트 계산
+
효율적인 DOM 업데이트
+
렌더링 작업 관리
+
개발자가 컴포넌트 구조와 상태를 잘 설계등이 함께 작용한다.
그리고 중요한 것은 React가 모든 경우에 자동으로 빠른 것은 아니라는 것이다.
예를 들어
function Component({ data }) {
const result = heavyCalculation(data);
return <div>{result}</div>;
}처럼 Render 과정에서 무거운 계산을 계속 수행하면 React 역시 결국 JavaScript이기 때문에 Main Thread를 점유할 수 있다.
19. React도 결국 Main Thread에서 실행된다
앞에서 Web Worker를 살펴봤다.
React도 결국 브라우저에서 실행되는 JavaScript다.
따라서 일반적인 React DOM 애플리케이션에서는
React
↓
JavaScript
↓
Main Thread의 관계가 성립한다.
그래서
function App() {
const result = hugeCalculation(data);
return <Viewer result={result} />;
}처럼 무거운 계산을 컴포넌트의 렌더링 과정에서 수행하면 Main Thread가 오래 점유될 수 있다.
이 문제는 React 최적화에서 중요한 출발점이다.
20. React 성능 최적화는 어디서 시작해야 할까?
React 성능을 이야기하면 바로
memo
useMemo
useCallback부터 떠올리기 쉽다.
하지만 먼저 확인해야 하는 것은
무엇이 다시 렌더링되는가?
왜 다시 렌더링되는가?
렌더링 비용이 얼마나 큰가?이다.
예를 들어
State 변경
↓
Parent Re-render
↓
Child Re-render
↓
GrandChild Re-render가 발생한다고 하자.
이때 모든 컴포넌트의 렌더링 비용이 낮다면 문제가 아닐 수도 있다.
반대로
State 변경
↓
Parent Re-render
↓
10,000개의 Row 계산
↓
Main Thread 점유
↓
UI 지연이라면 최적화를 고민할 필요가 있다.
즉,
React 성능 최적화는 "무조건 Re-render를 없애는 것"이 아니라 "불필요하거나 비용이 큰 작업을 찾아 줄이는 것"이다.
21. 지금까지 배운 개념이 React에서 어떻게 연결될까?
이제 전체 구조를 연결해보자.
브라우저에서 사용자가 버튼을 클릭한다.
사용자 클릭
↓
Browser Event
↓
JavaScript Event Handler
↓
React State UpdateState가 변경된다.
State Update
↓
React RenderReact가 새로운 UI를 계산한다.
Render
↓
Reconciliation
↓
Commit실제 DOM이 변경된다.
DOM Update
↓
Browser Rendering
↓
Layout / Paint / Composite
↓
화면전체를 합치면
사용자
↓
Browser Event
↓
JavaScript
↓
React State Update
↓
React Render
↓
Reconciliation
↓
Commit
↓
DOM
↓
Browser Rendering
↓
화면이것이 이번 글에서 가장 중요한 흐름이다.
22. React는 브라우저 렌더링을 추상화한다
앞에서 브라우저 렌더링을 배웠다.
DOM
↓
Layout
↓
Paint
↓
Composite개발자가 DOM을 직접 관리한다면
element.textContent = "Hello";
element.classList.add("active");처럼 직접 변경해야 한다.
React를 사용하면
<div className={active ? "active" : ""}>
{message}
</div>처럼 UI가 어떤 상태에서 어떻게 보여야 하는지를 선언한다.
React가 상태 변화에 따라 필요한 UI 업데이트를 계산하고 DOM에 반영한다.
즉 React는
개발자가 UI의 현재 상태와 DOM 변경 과정을 일일이 연결하지 않아도 되도록 추상화해주는 역할
을 한다.
23. 그렇다면 React를 왜 사용하는가?
React의 핵심 가치를 단순히
Virtual DOM하나로 설명하기는 어렵다.
React를 사용하면 UI를 컴포넌트 단위로 나누고,
상태와 UI의 관계를 선언적으로 표현하며,
상태 변화에 따른 UI 업데이트를 React가 관리하도록 만들 수 있다.
예를 들어
사용자 상태
↓
State
↓
Component
↓
UI라는 구조로 애플리케이션을 설계할 수 있다.
그리고 복잡한 애플리케이션에서는
Header
Sidebar
WorkList
Viewer
Modal
Form
Table처럼 UI를 여러 컴포넌트로 분리해 관리할 수 있다.
24. 이제 React 성능 문제를 볼 준비가 됐다
여기까지 오면 다음 질문이 자연스럽게 생긴다.
State가 변경되면
왜 컴포넌트가 다시 렌더링될까?
어떤 컴포넌트가 다시 렌더링될까?
부모가 렌더링되면
자식도 다시 렌더링될까?
같은 결과를 계산하는데
매번 다시 계산해야 할까?
함수나 객체가 새로 만들어지는 것이
렌더링에 영향을 줄까?이 질문들이 바로 다음 단계인
Render
↓
Re-render
↓
memo
↓
useMemo
↓
useCallback으로 이어진다.
마무리
이번 글에서는 React를 별도의 마법 같은 시스템으로 보기보다,
브라우저와 JavaScript 위에서 동작하는 UI 라이브러리라는 관점에서 살펴봤다.
전체 흐름을 다시 보면
브라우저
↓
JavaScript
↓
React
↓
Component
↓
State / Props
↓
Render
↓
Reconciliation
↓
Commit
↓
DOM
↓
Browser Rendering
↓
Layout
↓
Paint
↓
Composite
↓
화면그리고 사용자의 상호작용이 발생하면 다시
사용자 입력
↓
Event
↓
State Update
↓
React Render
↓
Commit
↓
DOM
↓
Browser Rendering이라는 흐름이 반복된다.
여기서 꼭 구분해야 할 것이 있다.
React Render
≠
Browser RenderingReact Render는 React가 새로운 UI 결과를 계산하는 과정이고,
Browser Rendering은 브라우저가 실제 화면을 업데이트하는 과정이다.
그리고 React 역시 결국 JavaScript이기 때문에 무거운 계산을 Render 과정에서 수행하면 Main Thread를 점유할 수 있다.
따라서 React 성능을 이해하려면
React만 볼 것이 아니라
JavaScript
+
Main Thread
+
Browser Rendering을 함께 봐야 한다.
다음 글에서는 이 흐름에서 가장 많이 접하게 되는 문제를 다룬다.
State 변경
↓
Re-render
↓
어떤 컴포넌트가 다시 실행되는가?
↓
왜 다시 실행되는가?
↓
불필요한 계산은 무엇인가?
↓
memo
useMemo
useCallback즉, React 성능 최적화는 무엇을 외워서 적용하는 것이 아니라 "어디에서 비용이 발생하고 있는가"를 찾는 것부터 시작한다.