내 포트폴리오 주소를 입력한 뒤, 첫 화면이 나오기까지 글 표지
WebBrowserHTTPReactNext.js

내 포트폴리오 주소를 입력한 뒤, 첫 화면이 나오기까지

내 사이트의 HTML 응답과 Next.js 코드를 놓고, 주소 입력부터 브라우저가 화면을 그리기까지 따라가 봤다.

연재 중

화면 뒤에서 일어나는 일

읽는 중01

브라우저, React Native, WebView에서 생긴 질문을 따라 네트워크와 렌더링, 스레드와 상태 관리의 원리를 실제 코드와 작은 실험으로 살펴봅니다.

전체 목차
  1. 01내 포트폴리오 주소를 입력한 뒤, 첫 화면이 나오기까지현재 글
  2. 02HTML을 받았는데도 화면이 비어 있다면작성 예정

React와 React Native로 화면을 만들고 있다. 컴포넌트를 나누고, API를 연결하고, 앱 안의 WebView와 네이티브 사이에 데이터를 주고받는 일을 한다.

화면을 구현하다 보면 그 아래에서 일어나는 일까지 따라가야 할 때가 있다. 앱 안의 웹 화면은 언제 준비됐다고 볼지, 로그인 정보는 누가 보관할지, 같은 상태를 보고 있어야 하는 두 화면이 왜 어긋나는지 같은 질문들이다.

이런 질문을 하나씩 꺼내 「화면 뒤에서 일어나는 일」이라는 이름으로 기록해 보려고 한다. 브라우저에서 시작해서 RN이 네이티브 화면을 만드는 과정, WebView와 앱 사이의 통신까지 이어갈 생각이다. 코드를 보다가 네트워크나 스레드, 메모리 이야기가 필요해지면 거기까지 내려가 보는 식으로.

첫 글은 컴포넌트 코드보다 조금 앞에서 출발한다.

주소창에 내 포트폴리오 주소를 입력하고 엔터를 누른다. 잠시 뒤 소개 문구와 프로젝트 목록이 나타난다. 이 사이에서 내가 작성한 코드는 언제 실행되고, 브라우저는 무엇을 받아서 화면을 만드는 걸까.

예시는 지금 운영 중인 포트폴리오로 잡았다. 공개 사이트의 HTML 응답과 Next.js App Router로 작성한 홈 화면 코드를 함께 확인했다. 이번 글에서는 각 단계에 걸린 시간을 재기보다, 첫 화면까지 어떤 일이 이어지는지 살펴본다.

주소를 입력하면, 먼저 어디로 갈지 정한다

입력한 주소는 https://www.junsob.kim/이다.

https는 사용할 통신 방식, www.junsob.kim은 호스트 이름, 마지막 /는 요청할 경로다. 브라우저는 필요한 경우 DNS를 통해 호스트 이름에 대응하는 IP 주소를 알아내고, 그곳과 통신할 연결을 준비한다. HTTPS 연결에서는 인증서를 확인하고 암호화 통신을 위한 협의도 한다.

다만 방문할 때마다 이 과정을 전부 처음부터 밟지는 않는다. DNS 조회 결과가 남아 있을 수도 있고, 이미 열려 있는 연결을 다시 사용할 수도 있다. 흔히 보는 주소 입력부터의 순서도는 전체 흐름을 이해하는 데 도움이 되지만, 매 요청의 작업 목록과 같지는 않다. MDN의 브라우저 동작 설명에서도 캐시와 연결 과정을 함께 다룬다.

연결 방식도 하나로 고정할 수 없다. HTTPS를 HTTP/1.1이나 HTTP/2로 주고받을 때는 TCP와 TLS를 설명하게 되지만, HTTP/3는 QUIC을 사용한다. 이번 확인에서는 실제 브라우저가 협상한 HTTP 버전을 기록하지 않았으므로, 내 사이트가 어떤 방식으로 연결됐다고 단정하지는 않겠다. MDN의 HTTP/3 설명

여기까지는 화면을 그리는 재료를 받기 위한 준비다.

응답하는 곳이 꼭 내 애플리케이션 서버일까

통신할 준비가 되면 브라우저는 문서를 요청한다. 첫 페이지를 여는 상황이라면 해당 경로의 HTML 문서를 요청하고, 응답에서는 상태 코드와 헤더, 본문을 받는다. 문서에 CSS나 이미지 같은 별도 리소스가 있으면 추가로 가져와야 한다. MDN의 HTTP 개요

공개 사이트에 보낸 요청에서 다음 응답을 확인했다. 설명에 필요한 항목만 옮겼다.

상태 코드: 200
Content-Type: text/html; charset=utf-8
Server: Vercel
X-Vercel-Cache: HIT

여기서 눈여겨본 건 HIT였다. Vercel 문서상 이 값은 캐시에서 응답을 제공했다는 뜻이다. 브라우저 내부 캐시와는 별개의 이야기다. Vercel 응답 헤더 문서

따라서 이번 요청을 “접속할 때마다 Next.js 서버가 홈 화면을 새로 계산해 HTML을 만들었다”라고 설명하면 맞지 않는다. 이미 준비된 응답을 캐시에서 받은 경우다. HTML을 미리 생성했는지, 요청 시 생성했는지까지 이 헤더 하나로 구분할 수는 없지만, 적어도 이번 응답이 어디서 나왔는지는 확인할 수 있다.

내가 서버 컴포넌트를 작성했다는 사실과, 그 코드가 방문할 때마다 서버에서 실행된다는 주장은 구분해서 봐야 한다.

여유롭게 쉬고 있는 톰과 제리
캐시가 응답해 줬을 때의 원본 서버.

받은 HTML 안에는 이미 내 이름이 있었다

다음으로 응답 본문을 확인했다. 개발자 도구의 Elements에 보이는 최종 DOM이 아니라, 사이트가 내려준 HTML이다.

그 안에 첫 화면의 소개 문구가 들어 있었다. 읽기 쉽도록 클래스 속성을 빼고 줄바꿈만 정리하면 다음과 같다.

<h1 id="hero-heading">
  웹과 앱을 만드는<br />
  <span>김준섭</span>입니다.
</h1>

홈 화면의 Hero 컴포넌트에 작성한 내용과 같다. 브라우저에서 JavaScript를 실행해 봐야만 알 수 있는 문구가 아니라, 받은 문서 안에서 바로 찾을 수 있었다.

HTML에는 /_next/static/ 아래의 CSS 파일을 가리키는 link 태그와 JavaScript 파일을 가리키는 script 태그도 있었다. HTML 한 개에 화면에 필요한 것이 전부 담겨 있는 구조는 아니다.

직접 확인하고 싶다면 터미널에서 아래처럼 문서를 받아 볼 수 있다. 이 명령은 응답을 파일로 저장할 뿐, 페이지의 JavaScript를 실행하지 않는다.

curl -sSL https://www.junsob.kim/ -o portfolio.html

저장한 파일에서 hero-heading을 찾으면 된다. 사이트를 수정하면 결과도 달라질 수 있지만, 글을 작성하는 시점의 응답에서는 위 제목을 확인했다.

이 차이는 React로 만든 사이트를 설명할 때 꽤 중요하다. React를 쓴다는 이유만으로 첫 응답을 빈 컨테이너라고 가정할 수는 없다. 적어도 내 포트폴리오의 첫 응답은 그렇지 않았다.

이제 브라우저가 문서를 화면으로 만든다

HTML 안에서 글자를 찾았다고 해서, 그 글자가 곧바로 화면의 픽셀이 되는 것은 아니다. 브라우저가 처리할 일이 남아 있다.

HTML을 읽으며 CSS와 JavaScript를 가져오고, 문서와 스타일을 바탕으로 화면을 만드는 흐름
역할을 나눈 그림이다. 실제 작업은 일부 겹쳐 진행되며, 화면도 여러 번 갱신된다.

브라우저는 전달받는 HTML을 해석해 DOM을 만든다. DOM은 제목, 문단, 링크 같은 문서 요소를 다룰 수 있도록 만든 구조다. 문서 전체가 도착할 때까지 무조건 기다리는 게 아니라, 받은 부분부터 처리할 수 있다.

이 과정에서 CSS나 이미지, JavaScript 같은 외부 리소스도 발견한다. 이들을 가져오는 작업은 HTML 해석과 겹쳐 진행될 수 있다. 모든 파일을 하나씩 순서대로 받아 놓은 뒤 화면을 그리는 방식으로 생각하면 실제 동작을 설명하기 어렵다.

문서 구조와 스타일을 바탕으로 요소에 적용할 스타일을 계산하고, 각 요소의 위치와 크기를 정한다. 이 위치와 크기 계산이 레이아웃이다. 내 소개 문구가 몇 줄로 나뉘는지, 그 아래 버튼이 어디에 놓이는지도 여기와 연결된다.

이후 페인트 단계에서 무엇을 어떤 순서로 그릴지 정리하고, 이를 실제 픽셀로 만드는 래스터화와 레이어를 합치는 합성을 거쳐 화면에 표시한다. DOM이 있다고 끝나는 일이 아니다. Chrome의 렌더러 내부 동작 설명

창구에서 나무늘보가 천천히 업무를 처리하는 모습
받았으니까 이제 그려주세요.

예를 들어 “서버는 빨리 응답했는데 화면이 늦게 나온다”는 상황이라면, 응답 이후에 필요한 CSS나 JavaScript, 브라우저의 처리 과정도 살펴봐야 한다. 이번 포트폴리오에서 그런 지연을 측정했다는 뜻은 아니다. HTTP 응답 시간만으로 화면이 준비되는 시간을 전부 설명할 수 없다는 이야기다.

그러면 React는 언제 일을 할까

내 포트폴리오는 Next.js App Router를 사용한다. 처음 주소로 접속하는 경우, Next.js가 준비한 HTML로 화면을 먼저 보여 줄 수 있다. 이후 브라우저에서 JavaScript를 실행하며 Client Component의 하이드레이션을 진행한다. Client Component도 첫 방문용 HTML을 미리 만드는 데 참여할 수 있다. 'use client'를 썼다고 그 영역이 반드시 빈 HTML로 내려오는 것은 아니다. Next.js 15의 Server·Client Component 설명

하이드레이션은 서버에서 만들어진 HTML에 React의 컴포넌트 로직을 연결하는 과정이다. 기존 DOM을 바탕으로 이벤트 처리와 상태 변경이 동작하도록 이어 붙인다고 생각하면 된다. React의 hydrateRoot 설명

내 코드에서는 상단의 모바일 메뉴가 구체적인 예다. 헤더는 Client Component이고, 메뉴 버튼에는 다음과 같은 처리가 들어 있다. 관련 부분만 줄였다.

const [isActive, setIsActive] = useState(false);
 
<button onClick={() => setIsActive((active) => !active)}>Menu</button>;

버튼의 모양과 Menu라는 글자가 있다는 것만으로 이 상태 변경 코드까지 준비됐다고 볼 수는 없다. 클릭했을 때 메뉴를 여는 동작에는 JavaScript와 React의 연결이 필요하다.

빨간 버튼을 쉬지 않고 연달아 누르는 손
버튼은 있는데 왜 일을 안 하니.

그렇다고 하이드레이션 전에는 웹 페이지에서 아무것도 할 수 없다는 뜻도 아니다. 일반적인 링크 이동이나 입력 같은 HTML 본래의 동작과, React 이벤트 핸들러가 처리하는 동작을 나눠 봐야 한다.

여기서 범위도 하나 정해 두고 싶다. 지금 따라간 것은 주소를 입력해 새 문서를 여는 과정이다. 이미 열린 Next.js 사이트에서 내부 링크로 이동할 때는 클라이언트 라우팅과 프리페치가 개입할 수 있어, 매번 첫 HTML부터 똑같이 받는 흐름으로 설명할 수 없다.

다음에는 어느 구간을 볼까

이번에 내 사이트에서 직접 확인한 사실은 세 가지다. 공개 요청이 Vercel 캐시에서 응답됐고, 응답 HTML 안에 소개 문구가 있었고, 별도로 가져올 CSS와 JavaScript 경로가 포함돼 있었다. 코드에서는 모바일 메뉴의 동작이 React 상태와 클릭 이벤트에 연결돼 있다는 것도 확인했다.

이것만으로 첫 화면이 빠르거나 느리다고 평가할 수는 없다. 대신 화면이 늦게 나올 때 무엇부터 구분해야 할지는 정리할 수 있다.

확인하려는 것먼저 살펴볼 곳
첫 문서를 받는 데 오래 걸리는가Network의 문서 요청과 Timing
문서는 왔는데 필요한 리소스를 기다리는가CSS·JavaScript·이미지 요청의 시작과 완료 시점
자료를 받은 뒤 브라우저가 오래 일하는가Performance의 스크립트 실행과 렌더링 작업
화면은 보이지만 특정 동작이 준비되지 않았는가해당 동작의 JavaScript 실행, 하이드레이션, 초기화 조건

React Native 안의 WebView를 다룰 때도 이 구분을 가져갈 수 있겠다. 네이티브 쪽의 로딩 표시를 언제 끝낼지 정하려면, 문서를 받았다는 시점과 웹 화면의 필요한 동작이 준비됐다는 시점을 어떻게 연결할지 생각해야 한다. 로그인 세션을 복원하는 과정까지 얹히면 더 그렇다.

RN의 <View>로 만든 화면은 이 과정과 무엇이 다를까. 그 화면 안에 WebView를 넣으면 어디까지 RN이 맡고, 어디부터 웹이 맡을까. 앞으로는 이렇게 내가 쓰는 코드에서 다음 질문을 찾아갈 생각이다.

다음 글에서는 범위를 조금 좁혀 보려고 한다. HTML을 받았는데도 화면이 아직 비어 있다면, 브라우저는 무엇을 기다리고 있을까. 이번에는 흐름을 정리했으니, 다음에는 그 사이를 직접 관찰할 차례다.