React로 작성한 코드가 실제로 어떻게 웹 브라우저 화면에 그려지는지 궁금하지 않으신가요? useState가 상태를 바꾸면 화면이 마법처럼 바뀌는 것 같지만, 그 뒤에는 웹 브라우저와 JavaScript 엔진, 그리고 React 라이브러리가 긴밀하게 협력하는 정교한 과정이 숨어있습니다.
이번 0주차에서는 React 코드를 작성하는 것을 넘어, 그 코드가 브라우저라는 무대 위에서 어떻게 생명을 얻고 사용자 눈앞에 나타나는지에 대한 근본적인 원리를 탐구해 보려 합니다.
브라우저의 렌더링 과정, JavaScript가 동작하는 방식, 그리고 다양한 웹 렌더링 모델들을 이해하는 것은 React를 더욱 깊이 있고 효과적으로 사용하는 튼튼한 기반이 될 것입니다.
우리가 작성하는 React 코드는 주로 JSX 문법을 포함한 JavaScript 파일입니다. 하지만 웹 브라우저는 JSX를 직접 이해하지 못합니다. 그래서 브라우저가 이해할 수 있는 순수한 HTML, CSS, JavaScript 형태로 변환하는 과정이 필요합니다.
빌드(Build) 과정
: 개발 중에 작성한 코드(React 컴포넌트, JSX, 최신 JavaScript 문법, CSS 등)를 실제 브라우저에서 실행 가능한 정적인 파일들(HTML, CSS, JavaScript 번들)로 변환하는 작업을 말합니다.
도구들
: 이 과정에서 주로
바벨(Babel)
같은 트랜스파일러가 JSX와 최신 JavaScript 문법을 구형 브라우저에서도 호환되는 코드로 변환하고,
웹팩(Webpack)
이나
Parcel
같은 모듈 번들러가 여러 개의 JavaScript 파일과 의존성들을 하나 또는 몇 개의 파일로 묶어주는 역할을 합니다. (Create React App이나 Vite 같은 도구들이 내부적으로 이런 설정들을 편리하게 제공해주죠.)
이렇게 빌드된 결과물(주로 index.html과 여러 개의 .js, .css 파일)이 웹 서버를 통해 사용자 브라우저로 전달되면, 비로소 화면을 그리는 시발점이 됩니다.
들만 포함하며, 각 요소의 스타일 정보도 함께 가지고 있습니다. 예를 들어, display: none; 스타일이 적용된 요소는 DOM 트리에는 존재하지만 화면에 표시되지 않으므로 렌더 트리에는 포함되지 않습니다. <head> 태그처럼 시각적으로 표현되지 않는 요소들도 제외됩니다.
렌더 트리가 생성되면, 브라우저는 각 요소들이 화면의 어디에, 어떤 크기로 배치될지 계산하는
레이아웃
단계를 진행합니다. 각 노드의 정확한 위치와 크기를 결정하는 과정이죠.
퍼센트(%) 값이나 vh, vw 같은 상대적인 단위들은 이 단계에서 실제 픽셀 값으로 변환됩니다.
만약 요소의 크기나 위치가 변경되거나, DOM 구조가 변경되어 레이아웃에 영향을 주면 이 과정이 다시 발생하는데, 이를
자, 그럼 React 애플리케이션의 첫 렌더링은 이 브라우저 렌더링 파이프라인과 어떻게 연결될까요?
JavaScript 실행 및 Virtual DOM 생성
브라우저가 빌드된 JavaScript 번들 파일을 로드하고 실행합니다. React 코드가 실행되면서, 컴포넌트 함수들이 호출되고 JSX는 React.createElement() 호출로 변환됩니다.
이 과정을 통해 React는 메모리 상에 실제 DOM 구조를 본뜬 가벼운 객체 트리, 즉
Virtual DOM
을 생성합니다.
실제 DOM 노드 생성 및 삽입
첫 렌더링 시에는 비교할 이전 Virtual DOM이 없으므로, React는 생성된 Virtual DOM 정보를 바탕으로 실제 DOM 노드들을 만듭니다.
그리고 이 노드들을 index.html 파일에 있는 루트 DOM 노드(보통 <div id="root"></div>)의 자식으로 삽입합니다.
브라우저 렌더링 파이프라인 촉발
이렇게 실제 DOM에 변경(노드 삽입)이 발생하면, 브라우저는 이를 감지하고 위에서 설명한 렌더링 파이프라인(레이아웃 -> 페인팅)을 실행하여 변경된 내용을 화면에 그립니다.
즉, React는 직접 픽셀을 그리는 것이 아니라, DOM을 효율적으로 관리하고 변경하는 역할을 합니다.
실제 화면에 그리는 것은 여전히 브라우저의 몫이죠. React의 역할은 브라우저가 일을 더 효율적으로 할 수 있도록 돕는 것이라고 생각할 수 있습니다.
이후 업데이트 과정에서는 Virtual DOM 비교(재조정)를 통해 최소한의 DOM 변경만 수행하여 성능을 최적화합니다. 이 내용은 다음 시간에 더 자세히 다룰 예정입니다 :)
기반 언어입니다.
즉, 한 번에 하나의 작업만 처리할 수 있습니다. 그런데 웹 페이지에서는 사용자 클릭에 반응하면서 동시에 네트워크 요청을 보내고 애니메이션도 보여줘야 합니다. 어떻게 이게 가능할까요?
바로 JavaScript 실행 환경(브라우저나 Node.js)이 제공하는
이벤트 루프(Event Loop)
덕분입니다.
콜 스택(Call Stack)
현재 실행 중인 함수들의 목록을 관리하는 곳입니다. 함수가 호출되면 스택에 쌓이고, 실행이 끝나면 스택에서 제거됩니다. (자료구조의
스택(Stack)
과 동일)
힙(Heap)
객체나 배열 등 동적으로 생성된 데이터가 저장되는 메모리 영역입니다.
태스크 큐(Task Queue) / 이벤트 큐(Event Queue)
비동기 작업(예: setTimeout 콜백, 이벤트 핸들러, 네트워크 응답 처리 등)이 완료된 후 실행될 콜백 함수들이 대기하는 곳입니다.
마이크로태스크 큐와 매크로태스크 큐(혹은 그냥 태스크 큐)로 나뉘기도 합니다.
이벤트 루프(Event Loop)
콜 스택이 비어 있는지 계속 확인하다가, 비어 있으면 태스크 큐에서 대기 중인 콜백 함수를 하나 꺼내와 콜 스택에 넣고 실행시키는 역할을 합니다. 이 과정을 계속 반복합니다.
이벤트 루프 다이어그램
React에서 버튼 클릭 이벤트 핸들러가 실행되거나, setState가 호출되어 상태 업데이트가 예약되는 것은 모두 이 이벤트 루프 메커니즘 위에서 동작합니다.
예를 들어 setState는 즉시 상태를 바꾸고 리렌더링하는 것이 아니라, 업데이트 작업을 예약하고 이벤트 루프를 통해 적절한 시점에 처리될 수 있습니다 (곧 배울 배치 처리와도 관련이 깊습니다).
Q:"이벤트 루프에 대해 설명해주세요." or "JavaScript는 싱글 스레드인데 어떻게 비동기 처리가 가능한가요?"
A: 콜 스택, 힙, 태스크 큐, 이벤트 루프의 역할과 상호작용을 설명.
동작 방식: 브라우저가 서버로부터 최소한의 HTML 파일과 JavaScript 번들 파일을 받아옵니다. JavaScript 파일이 로드되고 실행되면서 동적으로 DOM을 생성하고 화면을 그립니다. 대부분의 순수 React 애플리케이션(Create React App으로 만든 기본 앱 등)이 이 방식을 따릅니다.
장점: 초기 로딩 후에는 필요한 데이터만 서버와 교환하며 페이지 전환이 빠릅니다. 서버 부담이 적습니다.
단점: 초기 로딩 시 사용자가 빈 화면을 보거나 로딩 표시를 봐야 하는 시간이 깁니다 (Time To First Meaningful Paint 느림). 검색 엔진 최적화(SEO)에 불리할 수 있습니다 (검색 봇이 JavaScript 실행을 기다리지 못하는 경우).
동작 방식: 사용자가 페이지를 요청하면, 서버에서 해당 페이지에 필요한 데이터를 가져와 React 컴포넌트를 렌더링하여 완성된 HTML을 생성한 후 브라우저로 보냅니다. 브라우저는 이 HTML을 즉시 표시하고, 이후 JavaScript 파일을 로드하여 페이지가 상호작용 가능하도록 만듭니다 (이 과정을 Hydration이라고 합니다).
장점: 초기 로딩 속도가 빠릅니다 (Time To First Paint 빠름). 완성된 HTML이 제공되므로 SEO에 유리합니다.
단점: 매 요청마다 서버에서 렌더링해야 하므로 서버 부하가 커집니다. 클라이언트 측 로직과 서버 측 로직을 모두 고려해야 해서 개발 복잡도가 증가할 수 있습니다. Next.js 같은 프레임워크가 SSR 구현을 도와줍니다.