About→

← Studies

06. React

JavaScript와 브라우저 위에서 React는 무엇을 하는가?

지금까지 브라우저에서 웹페이지가 만들어지고 동작하는 과정을 살펴봤다.

처음에는 네트워크부터 시작했다.

text
URL
 ↓
DNS
 ↓
IP
 ↓
TCP / QUIC
 ↓
TLS
 ↓
HTTP
 ↓
HTML / CSS / JS

그리고 브라우저가 HTML과 CSS를 해석하고 화면을 만드는 과정도 살펴봤다.

text
HTML
 ↓
DOM

CSS
 ↓
CSSOM

DOM + CSSOM
 ↓
Render Tree
 ↓
Layout
 ↓
Paint
 ↓
Composite

JavaScript는 그 위에서 실행된다.

text
JavaScript
 ↓
Execution Context
 ↓
Call Stack
 ↓
Event Loop
 ↓
Web API
 ↓
Task / Microtask

그리고 무거운 작업은 필요하다면 Worker로 분리할 수 있다.

그렇다면 React는 어디에 있을까?

React는 브라우저를 대신하는 것도 아니고,

JavaScript 엔진을 대신하는 것도 아니다.

React는 기본적으로 JavaScript로 UI를 구성하고 관리하기 위한 라이브러리다.

따라서 React를 이해하려면 먼저

React는 브라우저 위에서 동작한다.

는 사실부터 잡아야 한다.

1. React는 브라우저를 대신하지 않는다

React를 처음 배울 때 흔히

text
React가 화면을 그린다.

라고 생각하기 쉽다.

하지만 정확하게 보면 React가 브라우저의 렌더링 엔진을 대신하는 것은 아니다.

React가 하는 일과 브라우저가 하는 일은 다르다.

text
React

컴포넌트
State
Props
Render
DOM 변경 사항 계산
        ↓
──────────────────
        ↓
Browser

DOM
Layout
Paint
Composite
        ↓
화면

즉,

React는 "무엇이 바뀌어야 하는가"를 관리하고, 브라우저는 실제 DOM을 바탕으로 화면을 그린다.

이 차이를 이해하는 것이 React를 제대로 이해하는 첫 번째 단계다.

2. React가 없던 시절에는 어떻게 UI를 만들었을까?

React를 이해하려면 먼저 일반적인 JavaScript로 DOM을 직접 조작하는 방식을 생각해보자.

예를 들어

html
<div id="counter">0</div>
<button id="button">+</button>

가 있다고 하자.

JavaScript로 직접 처리하면

jsx
const counter = document.querySelector("#counter");
const button = document.querySelector("#button");

let count = 0;

button.addEventListener("click", () => {
  count += 1;
  counter.textContent = count;
});

이렇게 작성할 수 있다.

여기서는 개발자가 직접

text
상태 변경
 ↓
DOM 변경

을 연결해야 한다.

count가 변경되면

jsx
counter.textContent = count;

를 직접 호출해야 한다.

UI가 복잡해지면 이 연결도 점점 복잡해진다.

3. React는 UI를 상태의 결과로 생각한다

React에서는 조금 다른 방식으로 생각한다.

jsx
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>
        +
      </button>
    </div>
  );
}

여기서 개발자가 직접

jsx
document.querySelector(...)
element.textContent = ...

같은 DOM 조작을 하지 않는다.

대신

text
state
 ↓
UI

라는 관계를 선언한다.

즉,

"현재 state가 이 값이라면 UI는 이렇게 보여야 한다."

라고 작성하는 것이다.

4. React에서 중요한 것은 State다

React의 핵심적인 흐름을 단순하게 표현하면

text
State
 ↓
React
 ↓
UI

이다.

예를 들어

jsx
const [count, setCount] = useState(0);

초기 상태가

text
count = 0

이라면 UI는

text
0

을 보여준다.

그리고

jsx
setCount(1);

이 실행되면 상태가 변경된다.

그러면 React는 이 변경을 바탕으로 UI를 다시 계산한다.

text
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과 스타일 등을 바탕으로

text
Layout
Paint
Composite

등을 수행해서 화면을 만드는 과정이다.

둘을 구분하면

text
React Render
↓
React가 UI 변경을 계산
↓
DOM 반영
↓
Browser Rendering
↓
실제 화면

이라는 흐름이 된다.

6. React Render가 발생했다고 화면이 바로 그려지는 것은 아니다

이 부분도 중요한 오해다.

text
React Render
=
화면 다시 그리기

가 아니다.

React Render는 React가 UI를 계산하는 과정이다.

그 이후 실제 DOM에 필요한 변경이 반영되고, 브라우저가 필요에 따라 렌더링 작업을 수행한다.

그래서

text
State 변경
 ↓
React Render
 ↓
Commit
 ↓
DOM 변경
 ↓
Browser Rendering
 ↓
화면

처럼 구분해서 생각하면 좋다.

7. Render와 Commit

React의 업데이트 과정을 이해할 때 Render와 Commit을 구분하는 것이 중요하다.

간단하게 보면

text
Render Phase
↓
무엇이 바뀌었는지 계산

Commit Phase
↓
실제 DOM에 변경 반영

이다.

예를 들어

jsx
function Counter() {
  const [count, setCount] = useState(0);

  return <div>{count}</div>;
}

처음에는

text
count = 0

이다.

사용자가 버튼을 눌러

jsx
setCount(1);

이 실행되었다고 하자.

React는 업데이트된 상태를 기준으로 컴포넌트를 다시 실행하고 UI를 계산한다.

text
count = 1
   ↓
React Render
   ↓
새로운 UI 계산

그리고 필요한 변경을 Commit 단계에서 실제 DOM에 반영한다.

text
Commit
 ↓
DOM 변경

그다음 브라우저가 화면 업데이트에 필요한 작업을 수행한다.

8. 그렇다면 Virtual DOM은 무엇일까?

React를 설명할 때 빠지지 않고 등장하는 것이 Virtual DOM이다.

Virtual DOM을 처음 접하면

"실제 DOM을 복사해놓은 가짜 DOM"

이라고 생각하기 쉽다.

하지만 이것만으로 설명하면 React의 동작을 오해하기 쉽다.

React는 컴포넌트가 반환하는 UI 구조를 바탕으로 내부적으로 UI에 대한 표현을 관리하고, 업데이트 과정에서 이전 결과와 새로운 결과를 비교해 필요한 변경을 계산한다.

개념적으로 단순화하면

text
이전 UI
   ↓
새로운 UI
   ↓
비교
   ↓
필요한 변경 계산
   ↓
DOM 반영

이라고 볼 수 있다.

예를 들어

jsx
<div>
  <h1>Hello</h1>
  <p>Count: 0</p>
</div>

에서

text
Count: 0

이

text
Count: 1

로 변경되었다고 하자.

React 입장에서는 전체 DOM을 무조건 다시 만드는 것이 아니라 변경이 필요한 부분을 찾아 실제 DOM에 반영하는 방향으로 업데이트를 수행한다.

9. React의 핵심은 "DOM을 직접 조작하지 않는다"가 아니다

React를 배우면서 흔히 이런 말을 듣는다.

"React는 DOM을 직접 조작하지 않는다."

이 표현도 정확하게 이해할 필요가 있다.

React도 결국 브라우저의 DOM을 변경해야 화면을 업데이트할 수 있다.

차이는

text
개발자가 직접 DOM 변경을 관리

하는 대신

text
개발자
 ↓
상태와 UI 관계를 선언
 ↓
React
 ↓
필요한 DOM 변경 계산 및 반영

하도록 만든다는 것이다.

즉,

DOM을 사용하지 않는 것이 아니라, DOM 변경을 직접 관리해야 하는 부담을 React가 추상화한다.

라고 이해하는 것이 좋다.

10. React 컴포넌트는 결국 JavaScript 함수다

React의 컴포넌트를 다시 생각해보자.

jsx
function Greeting() {
  return <h1>Hello</h1>;
}

겉으로 보면 React 컴포넌트지만 JavaScript 관점에서는 함수다.

React는 이 함수를 실행하면서 UI에 대한 결과를 얻는다.

text
Greeting()
   ↓
<h1>Hello</h1>

그래서 React를 이해할 때 JavaScript의 기본 개념이 중요하다.

앞에서 배웠던

text
Execution Context
Call Stack
Event Loop
Main Thread

가 모두 연결된다.

React도 결국 JavaScript 위에서 실행되기 때문이다.

11. 컴포넌트가 실행된다는 것

다음 코드를 보자.

jsx
function User({ name }) {
  return <div>{name}</div>;
}

React가 이 컴포넌트를 렌더링할 때 개념적으로는

text
User({ name: "Yonghee" })
        ↓
<div>Yonghee</div>

처럼 컴포넌트의 결과를 계산한다.

그리고 상태가 변경되어 다시 렌더링되면 컴포넌트 함수가 다시 실행될 수 있다.

text
첫 번째 Render
     ↓
User()
     ↓
UI 계산

State 변경
     ↓
두 번째 Render
     ↓
User()
     ↓
새로운 UI 계산

그래서 React 개발에서는

컴포넌트 함수가 언제 다시 실행되는가?

가 매우 중요한 질문이 된다.

이 질문이 다음 글에서 다룰 Re-render와 React 성능으로 이어진다.

12. State가 변경되면 무슨 일이 일어날까?

가장 중요한 흐름을 하나 만들어보자.

jsx
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

처음 화면에는

text
0

이 보인다.

사용자가 버튼을 클릭한다.

text
사용자 클릭
 ↓
이벤트 핸들러 실행
 ↓
setCount(count + 1)

그러면 React에 상태 업데이트가 요청된다.

text
State Update
 ↓
React Render

React는 새로운 상태를 기준으로 UI를 계산한다.

text
count = 1
 ↓
<button>1</button>

이전 결과와 비교하고 필요한 변경을 Commit한다.

text
Commit
 ↓
DOM의 텍스트 변경

그리고 브라우저가 필요한 화면 업데이트를 수행한다.

text
Browser Rendering
 ↓
화면에 1 표시

전체 과정은

text
Click
 ↓
Event Handler
 ↓
State Update
 ↓
React Render
 ↓
Commit
 ↓
DOM Update
 ↓
Browser Rendering

이다.

이 흐름을 이해하면 React의 많은 개념이 연결되기 시작한다.

13. State가 변경되면 DOM 전체를 다시 만드는가?

그렇지는 않다.

예를 들어

jsx
function App() {
  return (
    <div>
      <h1>Hello</h1>
      <p>Count: 1</p>
    </div>
  );
}

여기서 Count만 변경되었다고 하자.

text
Count: 0
      ↓
Count: 1

React는 업데이트 과정에서 필요한 변경을 계산하고 DOM에 반영한다.

개념적으로

text
기존 UI
┌─────────────┐
│ Hello       │
│ Count: 0    │ ← 변경
└─────────────┘

새 UI
┌─────────────┐
│ Hello       │
│ Count: 1    │ ← 변경
└─────────────┘

이렇게 차이를 찾아 필요한 부분을 반영한다고 생각하면 된다.

다만 여기서도

"React는 항상 DOM 변경을 최소화한다."

라고 단순하게 단정하기보다는, React의 reconciliation과 commit 과정을 통해 필요한 변경을 계산하고 반영한다고 이해하는 것이 좋다.

14. Props는 무엇일까?

React에서는 State뿐만 아니라 Props도 중요한 역할을 한다.

jsx
function User({ name }) {
  return <div>{name}</div>;
}

function App() {
  return <User name="Yonghee" />;
}

여기서

text
App
 ↓
User

이고,

text
name="Yonghee"

가 Props다.

Props는 부모 컴포넌트가 자식 컴포넌트에 전달하는 값이다.

text
Parent
   │
   │ props
   ↓
Child

그리고 Props가 변경되면 자식 컴포넌트의 결과도 달라질 수 있다.

15. State와 Props를 함께 보면

React의 UI는 대략

text
State
   +
Props
   ↓
Component
   ↓
UI

라고 생각할 수 있다.

예를 들어

jsx
function User({ name }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{name}</p>
      <p>{count}</p>
    </div>
  );
}

이 컴포넌트의 UI는

text
Props
  name
   +
State
  count
   ↓
Render
   ↓
UI

의 결과라고 볼 수 있다.

16. Re-render는 무엇일까?

이제 React 성능에서 가장 중요한 개념 중 하나로 넘어갈 수 있다.

Re-render는 컴포넌트가 다시 렌더링되는 것이다.

예를 들어

jsx
setCount(count + 1);

이 실행되면 해당 업데이트에 영향을 받는 React 트리가 다시 렌더링될 수 있다.

이때 중요한 것은

text
Re-render
≠
DOM 전체 다시 그리기

라는 것이다.

Re-render는 React가 컴포넌트 함수를 다시 실행하고 새로운 UI 결과를 계산하는 과정과 관련된다.

그 후 React가 필요한 DOM 변경을 Commit한다.

17. Re-render와 Browser Rendering을 구분하자

이것은 이번 글에서 반드시 기억해야 하는 부분이다.

text
React Re-render
→ React가 UI를 다시 계산

Browser Rendering
→ 브라우저가 실제 화면을 업데이트

둘은 서로 다른 과정이다.

예를 들어 State가 변경되었다고 해서

text
State 변경
 ↓
무조건 전체 화면 다시 Paint

가 되는 것은 아니다.

React가 업데이트를 계산하고 실제 DOM에 필요한 변경이 반영된 후, 브라우저가 필요한 렌더링 작업을 수행한다.

18. 그렇다면 React가 빠른 이유는 무엇일까?

React를 소개할 때 흔히

"Virtual DOM이라서 빠르다."

라고 설명한다.

하지만 이것만으로는 부족하다.

React의 성능을 이해하려면 여러 요소를 함께 봐야 한다.

text
필요한 UI 업데이트 계산
        +
효율적인 DOM 업데이트
        +
렌더링 작업 관리
        +
개발자가 컴포넌트 구조와 상태를 잘 설계

등이 함께 작용한다.

그리고 중요한 것은 React가 모든 경우에 자동으로 빠른 것은 아니라는 것이다.

예를 들어

jsx
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 애플리케이션에서는

text
React
 ↓
JavaScript
 ↓
Main Thread

의 관계가 성립한다.

그래서

jsx
function App() {
  const result = hugeCalculation(data);

  return <Viewer result={result} />;
}

처럼 무거운 계산을 컴포넌트의 렌더링 과정에서 수행하면 Main Thread가 오래 점유될 수 있다.

이 문제는 React 최적화에서 중요한 출발점이다.

20. React 성능 최적화는 어디서 시작해야 할까?

React 성능을 이야기하면 바로

text
memo
useMemo
useCallback

부터 떠올리기 쉽다.

하지만 먼저 확인해야 하는 것은

text
무엇이 다시 렌더링되는가?
왜 다시 렌더링되는가?
렌더링 비용이 얼마나 큰가?

이다.

예를 들어

text
State 변경
 ↓
Parent Re-render
 ↓
Child Re-render
 ↓
GrandChild Re-render

가 발생한다고 하자.

이때 모든 컴포넌트의 렌더링 비용이 낮다면 문제가 아닐 수도 있다.

반대로

text
State 변경
 ↓
Parent Re-render
 ↓
10,000개의 Row 계산
 ↓
Main Thread 점유
 ↓
UI 지연

이라면 최적화를 고민할 필요가 있다.

즉,

React 성능 최적화는 "무조건 Re-render를 없애는 것"이 아니라 "불필요하거나 비용이 큰 작업을 찾아 줄이는 것"이다.

21. 지금까지 배운 개념이 React에서 어떻게 연결될까?

이제 전체 구조를 연결해보자.

브라우저에서 사용자가 버튼을 클릭한다.

text
사용자 클릭
 ↓
Browser Event
 ↓
JavaScript Event Handler
 ↓
React State Update

State가 변경된다.

text
State Update
 ↓
React Render

React가 새로운 UI를 계산한다.

text
Render
 ↓
Reconciliation
 ↓
Commit

실제 DOM이 변경된다.

text
DOM Update
 ↓
Browser Rendering
 ↓
Layout / Paint / Composite
 ↓
화면

전체를 합치면

text
사용자
  ↓
Browser Event
  ↓
JavaScript
  ↓
React State Update
  ↓
React Render
  ↓
Reconciliation
  ↓
Commit
  ↓
DOM
  ↓
Browser Rendering
  ↓
화면

이것이 이번 글에서 가장 중요한 흐름이다.

22. React는 브라우저 렌더링을 추상화한다

앞에서 브라우저 렌더링을 배웠다.

text
DOM
 ↓
Layout
 ↓
Paint
 ↓
Composite

개발자가 DOM을 직접 관리한다면

jsx
element.textContent = "Hello";
element.classList.add("active");

처럼 직접 변경해야 한다.

React를 사용하면

jsx
<div className={active ? "active" : ""}>
  {message}
</div>

처럼 UI가 어떤 상태에서 어떻게 보여야 하는지를 선언한다.

React가 상태 변화에 따라 필요한 UI 업데이트를 계산하고 DOM에 반영한다.

즉 React는

개발자가 UI의 현재 상태와 DOM 변경 과정을 일일이 연결하지 않아도 되도록 추상화해주는 역할

을 한다.

23. 그렇다면 React를 왜 사용하는가?

React의 핵심 가치를 단순히

text
Virtual DOM

하나로 설명하기는 어렵다.

React를 사용하면 UI를 컴포넌트 단위로 나누고,

상태와 UI의 관계를 선언적으로 표현하며,

상태 변화에 따른 UI 업데이트를 React가 관리하도록 만들 수 있다.

예를 들어

text
사용자 상태
    ↓
State
    ↓
Component
    ↓
UI

라는 구조로 애플리케이션을 설계할 수 있다.

그리고 복잡한 애플리케이션에서는

text
Header
Sidebar
WorkList
Viewer
Modal
Form
Table

처럼 UI를 여러 컴포넌트로 분리해 관리할 수 있다.

24. 이제 React 성능 문제를 볼 준비가 됐다

여기까지 오면 다음 질문이 자연스럽게 생긴다.

text
State가 변경되면
왜 컴포넌트가 다시 렌더링될까?

어떤 컴포넌트가 다시 렌더링될까?

부모가 렌더링되면
자식도 다시 렌더링될까?

같은 결과를 계산하는데
매번 다시 계산해야 할까?

함수나 객체가 새로 만들어지는 것이
렌더링에 영향을 줄까?

이 질문들이 바로 다음 단계인

text
Render
↓
Re-render
↓
memo
↓
useMemo
↓
useCallback

으로 이어진다.

마무리

이번 글에서는 React를 별도의 마법 같은 시스템으로 보기보다,

브라우저와 JavaScript 위에서 동작하는 UI 라이브러리라는 관점에서 살펴봤다.

전체 흐름을 다시 보면

text
브라우저
   ↓
JavaScript
   ↓
React
   ↓
Component
   ↓
State / Props
   ↓
Render
   ↓
Reconciliation
   ↓
Commit
   ↓
DOM
   ↓
Browser Rendering
   ↓
Layout
   ↓
Paint
   ↓
Composite
   ↓
화면

그리고 사용자의 상호작용이 발생하면 다시

text
사용자 입력
   ↓
Event
   ↓
State Update
   ↓
React Render
   ↓
Commit
   ↓
DOM
   ↓
Browser Rendering

이라는 흐름이 반복된다.

여기서 꼭 구분해야 할 것이 있다.

text
React Render
≠
Browser Rendering

React Render는 React가 새로운 UI 결과를 계산하는 과정이고,

Browser Rendering은 브라우저가 실제 화면을 업데이트하는 과정이다.

그리고 React 역시 결국 JavaScript이기 때문에 무거운 계산을 Render 과정에서 수행하면 Main Thread를 점유할 수 있다.

따라서 React 성능을 이해하려면

text
React만 볼 것이 아니라

JavaScript
+
Main Thread
+
Browser Rendering

을 함께 봐야 한다.

다음 글에서는 이 흐름에서 가장 많이 접하게 되는 문제를 다룬다.

text
State 변경
   ↓
Re-render
   ↓
어떤 컴포넌트가 다시 실행되는가?
   ↓
왜 다시 실행되는가?
   ↓
불필요한 계산은 무엇인가?
   ↓
memo
useMemo
useCallback

즉, React 성능 최적화는 무엇을 외워서 적용하는 것이 아니라 "어디에서 비용이 발생하고 있는가"를 찾는 것부터 시작한다.