브라우저의 작동 방식

번역브라우저

사용자는 콘텐츠가 빠르게 로딩되고 상호 작용이 부드러운 웹 경험을 원합니다. 개발자는 이 두 가지 목표를 달성하기 위해 노력해야 합니다.

성능과 체감 성능을 개선하는 방법을 이해하려면 브라우저의 작동 방식을 아는 것이 좋습니다.

개요

빠른 사이트는 좋은 사용자 경험을 제공합니다. 사용자는 로딩이 빠르고 상호 작용이 부드러운 콘텐츠가 포함된 웹 경험을 원하고 기대합니다.

웹 성능의 두 가지 주요 문제는 지연 시간과, 대부분 브라우저가 단일 스레드라는 것입니다.

지연 시간은 빠른 페이지 로딩의 가장 큰 걸림돌입니다. 네트워크 지연 시간은 무선으로 컴퓨터에 바이트를 전송하는 데 걸리는 시간입니다.

개발자의 목표는 사이트를 최대한 빨리 로드하거나, 최소한 초고속으로 로드되는 것처럼 보이게 하여 사용자가 요청한 정보를 최대한 빨리 얻게 하는 것입니다. 웹 성능은 페이지를 최대한 빨리 로드하는 것입니다.

대부분의 경우 브라우저는 단일 스레드입니다. 즉, 한 작업을 끝내야 다른 작업을 수행합니다.

부드러운 상호 작용을 위한 개발자의 목표는 다음과 같습니다. 부드러운 스크롤에서 터치 반응에 이르기까지 성능이 뛰어난 사이트 상호 작용을 보장해야 합니다. 메인 스레드가 모든 작업을 완료하고 사용자 상호 작용을 항상 처리할 수 있도록 하려면 렌더 타임이 중요합니다. 웹 성능은 위해서는 브라우저의 단일 스레드 특성을 이해하고, 가능하면 메인 스레드의 책임을 최소화하여 렌더링이 부드럽고 상호 작용에 대한 응답이 즉각적이어야 합니다.

내비게이션

내비게이션은 웹 페이지 로딩의 첫 번째 단계입니다. 내비게이션은 사용자가 주소 표시줄에 URL을 입력하고, 링크를 클릭하고, 양식을 제출하는 등의 작업으로 페이지를 요청할 때마다 발생합니다.

웹 성능의 목표 중 하나는 탐색을 완료하는 데 걸리는 시간을 최소화하는 것입니다. 이상적인 조건에서는 내비게이션이 오래 걸리지 않지만 지연 시간과 대역폭은 지연을 유발할 수 있는 적입니다.

DNS 룩업

웹 페이지로 이동하는 첫 번째 단계는 해당 페이지의 자산이 있는 위치를 찾는 것입니다. https://example.com으로 이동하면 HTML 페이지는 IP 주소가 93.184.216.34인 서버에 있습니다. 이 사이트를 방문한 적이 없다면 DNS 룩업이 이루어져야 합니다.

브라우저가 DNS 룩업을 요청하면 최종적으로 이름 서버가 해당 필드를 입력하고 IP 주소로 응답합니다. 이 초기 요청 후 IP가 잠시 캐시될 가능성이 높습니다. 이름 서버에 다시 연결하는 대신 캐시에서 IP 주소를 검색하여 후속 요청 속도가 향상됩니다.

DNS 룩업은 일반적으로 페이지 로드를 위해 호스트 이름당 한 번만 수행하면 됩니다. 그러나 요청된 페이지가 참조하는 각 고유 호스트 이름에 대해 DNS 룩업을 수행해야 합니다. 글꼴, 이미지, 스크립트, 광고, 메트릭의 호스트 이름이 모두 다른 경우 각 호스트에 대해 DNS 룩업을 수행해야 합니다.

모바일 요청은 먼저 기지국으로 전송된 후 인터넷으로 전송되기 전에 중앙 전화 회사 컴퓨터로 전송됨

이는 특히 모바일 네트워크에서 성능에 문제가 될 수 있습니다. 사용자가 모바일 네트워크에 있을 때 각 DNS 룩업은 권한 있는 DNS 서버에 도달하기 위해 전화기에서 기지국으로 이동해야 합니다. 전화기, 기지국, 이름 서버 사이의 거리는 상당한 지연 시간을 추가할 수 있습니다.

TCP 핸드셰이크

IP 주소가 알려지면 브라우저는 TCP 3방향 핸드셰이크(TCP three-way handshake)를 통해 서버 연결을 설정합니다. 이 메커니즘은 통신을 시도하는 두 개체(이 경우 브라우저와 웹 서버)가 주로 HTTPS를 통해 데이터를 전송하기 전에 네트워크 TCP 소켓 연결의 매개변수를 협상할 수 있도록 설계되었습니다.

TCP의 3방향 핸드셰이킹 기술은 종종 ‘SYN-SYN-ACK’, 보다 정확하게는 SYN, SYN-ACK, ACK라고 부릅니다. 두 컴퓨터 사이에서 TCP 세션을 협상하고 시작하기 위해 TCP가 전송하는 세 가지 메시지가 있기 때문입니다. 각 서버 간에 세 개의 메시지가 더 오고갈 것이며 아직 요청은 이루어지지 않았습니다.

TLS 협상

HTTPS를 통해 보안 연결을 설정하려면 또 다른 ‘핸드셰이크’가 필요합니다. 이 핸드셰이크 또는 TLS 협상은 통신을 암호화하는 데 사용할 암호를 결정하고, 서버를 확인하고, 실제 데이터 전송을 시작하기 전에 보안 연결이 있는지 확인합니다. 이렇게 하려면 콘텐츠 요청이 실제로 전송되기 전에 서버로 세 번 더 왕복해야 합니다.

DNS 조회, TCP 핸드셰이크, TLS 핸드셰이크의 5단계(클라이언트 헬로, 서버 헬로 및 인증서, 클라이언트 키 포함) 및 서버와 클라이언트 모두에 대한 완료

보안 연결을 설정하면 페이지 로드에 시간이 더 걸리지만, 브라우저와 웹 서버 간에 전송되는 데이터를 제3자가 해독할 수 없게 되므로 지연 시간 비용을 감수할 가치가 있습니다.

8번의 왕복 후, 브라우저는 마침내 요청을 할 수 있습니다.

응답

웹 서버에 대한 연결이 설정되면 브라우저는 사용자를 대신하여 초기 HTTP GET 요청을 보냅니다. 웹 사이트의 경우 이는 대부분 HTML 파일입니다. 서버가 요청을 받으면 관련 응답 헤더와 HTML 콘텐츠로 응답합니다.

<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<title>My simple page</title>
<link rel="stylesheet" href="styles.css" />
<script src="myscript.js"></script>
</head>
<body>
<h1 class="heading">My Page</h1>
<p>A paragraph with a <a href="https://example.com/about">link</a></p>
<div>
<img src="myimage.jpg" alt="image description" />
</div>
<script src="anotherscript.js"></script>
</body>
</html>

초기 요청에 대한 응답에는 수신된 데이터의 첫 번째 바이트가 포함됩니다. 첫 번째 바이트까지의 시간(Time to First Byte, TTFB)은 사용자가 요청한 시점(예: 링크 클릭)과 HTML의 첫 번째 패킷을 수신한 시점 사이의 시간입니다. 콘텐츠의 첫 번째 청크는 일반적으로 14KB의 데이터입니다.

위의 예시에서 요청은 확실히 14KB 미만이지만 링크된 자원은 브라우저가 파싱 중에 링크를 만날 때까지 요청되지 않습니다. (하단에 자세한 설명)

TCP 느린 시작 / 14KB 규칙

첫 번째 응답 패킷은 14KB입니다. 이는 네트워크 연결 속도의 균형을 맞추는 알고리즘인 TCP 느린 시작(TCP slow start)의 일부입니다. 느린 시작은 네트워크의 최대 대역폭이 결정될 때까지 전송되는 데이터의 양을 점진적으로 증가시킵니다.

TCP 느린 시작에서 서버는 초기 패킷을 수신한 후 다음 패킷의 크기를 두 배로 늘려 약 28KB로 만듭니다. 후속 패킷은 미리 결정된 임계값에 도달하거나 정체가 발생할 때까지 크기가 증가합니다.

초기 페이지 로드에 대한 14KB 규칙을 들어본 적이 있을 것입니다. 초기 응답이 14KB인 이유가 바로 이 TCP 느린 시작 때문입니다. 웹 성능 최적화는 초기 14KB 응답을 염두에 둬야 합니다. TCP 느린 시작은 정체를 피하기 위해 네트워크의 능력에 적합한 전송 속도를 점진적으로 구축합니다.

혼잡 제어

서버가 TCP 패킷으로 데이터를 전송할 때 사용자의 클라이언트는 확인 응답(acknowledgement) 또는 ACK를 반환하여 전송을 확인합니다. 연결 용량은 하드웨어 및 네트워크 상태에 따라 제한됩니다. 서버가 너무 많은 패킷을 너무 빨리 보내면 패킷이 삭제됩니다. 즉, 확인 응답이 없을 것입니다. 서버는 이를 누락된 ACK로 등록합니다. 혼잡 제어(congestion control) 알고리즘은 전송된 패킷과 ACK의 흐름을 사용하여 전송 속도를 결정합니다.

파싱

브라우저가 첫 번째 데이터 청크를 수신하면 수신된 정보의 파싱을 시작할 수 있습니다. 파싱(parsing)은 브라우저가 네트워크를 통해 수신한 데이터를 DOMCSSOM으로 변환하기 위해 수행하는 단계로, 이는 렌더러가 화면에 페이지를 그리는 데 사용합니다.

DOM은 브라우저 마크업의 내부 표현입니다. DOM은 노출되어 있으며 자바스크립트의 다양한 API를 통해 조작할 수 있습니다.

요청된 페이지의 HTML이 초기 14KB 패킷보다 큰 경우에도 브라우저는 파싱을 시작하고 가지고 있는 데이터를 기반으로 경험을 렌더링하려고 시도합니다. 이것이 웹 성능 최적화에서 브라우저가 페이지 렌더링을 시작하는 데 필요한 모든 것을 포함하거나 최소한 페이지 템플릿(첫 번째 렌더링에 필요한 CSS 및 HTML)을 처음 14KB에 포함하는 것이 중요한 이유입니다. 그러나 화면에 무엇이든 렌더링하기 전에 HTML, CSS, 자바스크립트를 파싱해야 합니다.

DOM 트리 구축

이제 주요 렌더링 경로(critical rendering path)의 다섯 단계를 알아보겠습니다.

첫 번째 단계는 HTML 마크업을 처리하고 DOM 트리를 구축하는 것입니다. HTML 파싱에는 토큰화 및 트리 구성이 포함됩니다. HTML 토큰에는 시작 태그와 끝 태그, 속성 이름과 값이 포함됩니다. 문서의 형식이 올바르면 파싱이 간단하고 빠릅니다. 파서(parser)는 토큰화된 입력을 문서로 파싱하여 문서 트리를 구성합니다.

DOM 트리는 문서의 콘텐츠를 설명합니다. <html> 요소는 문서 트리의 첫 번째 태그이자 루트 노드입니다. 트리는 서로 다른 태그 간의 관계 및 계층을 반영합니다. 다른 태그 내에 중첩된 태그는 하위 노드입니다. DOM 노드의 수가 많을수록 DOM 트리를 구성하는 데 시간이 더 오래 걸립니다.

텍스트 노드를 포함한 모든 노드를 보여주는 샘플 코드의 DOM 트리

파서가 이미지와 같은 차단되지 않는 자원을 찾으면 브라우저는 해당 자원을 요청하고 파싱을 계속합니다. 파싱은 CSS 파일을 만나도 계속됩니다. 하지만 <script> 태그(특히 async 또는 defer 속성이 없는 태그)는 렌더링을 차단하고 HTML 파싱을 일시 중지시킵니다. 브라우저의 프리로드 스캐너가 이 프로세스를 가속화하지만 과도한 스크립트는 여전히 심각한 병목 현상이 될 수 있습니다.

프리로드 스캐너

브라우저가 DOM 트리를 구축하는 동안 이 프로세스가 메인 스레드를 차지합니다. 프리로드 스캐너(preload scanner)는 사용 가능한 콘텐츠를 파싱하고 CSS, 자바스크립트, 웹 글꼴과 같은 우선 순위가 높은 자원을 요청합니다. 프리로드 스캐너 덕분에 파서가 외부 자원에 대한 참조를 찾아 요청할 때까지 기다릴 필요가 없습니다. 기본 HTML 파서가 요청된 자산에 도달할 때에는 자원을 이미 가져오는 중이거나 다운로드되었을 수 있도록 뒤에서 자원을 검색합니다. 프리로드 스캐너의 최적화는 막힘을 줄입니다.

<link rel="stylesheet" href="styles.css" />
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="image description" />
<script src="anotherscript.js" async></script>

이 예시에서 메인 스레드가 HTML과 CSS를 파싱하는 동안 프리로드 스캐너는 스크립트와 이미지를 찾아 다운로드를 시작합니다. 스크립트가 프로세스를 차단하지 않도록 하려면 async 속성을 추가하거나, 자바스크립트 파싱 및 실행 순서가 중요한 경우 defer 속성을 추가하세요.

CSS를 얻기 위해 기다리는 것은 HTML 파싱이나 다운로드를 차단하지 않지만 자바스크립트는 차단합니다. 자바스크립트는 요소에 대한 CSS 속성의 영향을 질의하는 데 자주 사용되기 때문입니다.

CSSOM 구축

주요 렌더링 경로의 두 번째 단계는 CSS를 처리하고 CSSOM 트리를 구축하는 것입니다. CSS 객체 모델은 DOM과 유사합니다. DOM과 CSSOM 모두 트리입니다. 둘은 독립적인 데이터 구조입니다.

브라우저는 CSS 규칙을 이해하고 사용할 수 있는 스타일 맵으로 변환합니다. 브라우저는 CSS에 설정된 각 규칙을 살펴보고 CSS 선택기를 기반으로 부모, 자식, 형제 관계가 있는 노드 트리를 만듭니다.

HTML과 마찬가지로 브라우저는 받은 CSS 규칙을 변환해서 사용해야 합니다. 따라서 HTML을 변환했던 프로세스를 CSS에 대해서 반복합니다.

CSSOM 트리에는 사용자 에이전트 스타일 시트의 스타일이 포함됩니다. 브라우저는 노드에 적용할 수 있는 가장 일반적인 규칙으로 시작해서 보다 구체적인 규칙을 적용하여 계산된 스타일을 재귀적으로 정제합니다. 즉, 속성 값을 캐스케이드(cascade)합니다.

CSSOM 구축은 매우 빠르며 현재 개발자 도구에서 고유한 색상으로 표시되지 않습니다. 오히려 개발자 도구의 ‘Recalculate Style’은 CSS를 파싱하고 CSSOM 트리를 구성하며 계산된 스타일을 재귀적으로 계산하는 데 걸리는 총 시간을 보여줍니다. 웹 성능 최적화 측면에서, CSSOM을 생성하는 데 걸리는 총 시간은 일반적으로 하나의 DNS 룩업에 걸리는 시간보다 짧기 때문에 이 부분은 신경을 크게 쓸 필요가 없습니다.

기타 프로세스

자바스크립트 컴파일

CSS가 파싱되고 CSSOM이 생성되는 동안, 프리로드 스캐너 덕분에 자바스크립트 파일을 비롯한 다른 자산이 다운로드됩니다. 자바스크립트는 해석, 컴파일, 파싱, 실행됩니다. 스크립트는 추상 구문 트리로 파싱됩니다. 일부 브라우저 엔진은 추상 구문 트리(abstract syntax tree)를 가져와 인터프리터에 전달하여 메인 스레드에서 실행되는 바이트코드를 출력합니다. 이를 자바스크립트 컴파일이라고 합니다.

접근성 트리 구축

브라우저는 보조 장치가 콘텐츠를 파싱하고 해석하는 데 사용하는 접근성 트리도 구축합니다. 접근성 객체 모델(accessibility object model, AOM)은 DOM의 시맨틱 버전과 같습니다. 브라우저는 DOM이 갱신되면 접근성 트리를 갱신합니다. 보조 기술 자체로는 접근성 트리를 수정할 수 없습니다.

AOM이 구축되기 전까지는 스크린 리더가 콘텐츠에 접근할 수 없습니다.

렌더

렌더링 단계에는 스타일, 레이아웃, 페인트, 그리고 경우에 따라 합성이 포함됩니다. 파싱 단계에서 생성된 CSSOM과 DOM 트리는 렌더 트리에 결합되어 모든 가시적 요소의 레이아웃을 계산하는 데 사용되며, 이는 화면에 그려집니다.

경우에 따라 콘텐츠를 자체 계층으로 승격시켜 합성할 수 있습니다. CPU 대신 GPU에서 화면의 일부를 페인팅하여 성능을 개선하고 메인 스레드를 확보할 수 있습니다.

스타일

주요 렌더링 경로의 세 번째 단계는 DOM과 CSSOM을 렌더 트리로 결합하는 것입니다. 계산된 스타일 트리 또는 렌더 트리 구성은 DOM 트리의 루트에서 시작하여 표시되는 각 노드를 통과합니다.

<head>와 하위 태그, display: none이 있는 노드(사용자 에이전트 스타일시트에서 볼 수 있는 script { display: none; })는 렌더링된 출력에 표시되지 않으므로 렌더 트리에 포함되지 않습니다. visibility: hidden이 적용된 노드는 공간을 차지하므로 렌더 트리에 포함됩니다. 사용자 에이전트 기본값을 재정의하는 명령을 제공하지 않았기 때문에 위의 코드 예시에 있는 script 노드는 렌더 트리에 포함되지 않습니다.

보이는(visible) 각 노드에는 해당 CSSOM 규칙이 적용됩니다. 렌더 트리는 콘텐츠와 계산된 스타일을 가진 모든 보이는 노드를 보유하고 있습니다. 모든 관련된 스타일을 DOM 트리의 모든 보이는 노드에 일치시키고, CSS 캐스케이드에 기반하여 각 노드에 대해 계산된 스타일이 무엇인지 결정합니다.

레이아웃

주요 렌더링 경로의 네 번째 단계는 렌더 트리에서 레이아웃을 실행하여 각 노드의 형상을 계산하는 것입니다.

레이아웃(layout)은 렌더 트리에 있는 모든 노드의 너비, 높이, 위치를 결정하고, 페이지 상의 각 개체의 크기와 위치를 결정하는 프로세스입니다. 리플로(reflow)는 페이지의 모든 부분 또는 전체 문서의 후속 크기 및 위치를 결정하는 것입니다.

렌더 트리가 구축되면 레이아웃이 시작됩니다. 렌더 트리는 계산된 스타일과 함께 표시되는(displayed) 노드(보이지 않는(invisible) 노드 포함)를 식별했지만 각 노드의 크기나 위치는 식별하지 못했습니다. 각 개체의 정확한 크기와 위치를 결정하기 위해 브라우저는 렌더 트리의 루트에서 시작하여 개체를 통과합니다.

웹 페이지에서는 거의 모든 것이 상자입니다. 다양한 장치와 다양한 데스크탑 환경은 뷰포트 크기의 무한한 다양성을 의미합니다.

레이아웃 단계에서는 뷰포트 크기를 고려하여 브라우저가 화면에 표시될 모든 상자의 크기를 결정합니다. 뷰포트 크기를 기준으로, 레이아웃은 일반적으로 본문(body)에서 시작하여, 각 요소의 상자 모델 속성으로 본문의 모든 자손의 크기를 배치합니다. 그리고 이미지와 같이 크기를 알지 못하는 교체된 요소를 위한 플레이스홀더 공간을 제공합니다.

노드의 크기와 위치가 처음 결정되는 것을 레이아웃이라고 합니다. 노드 크기와 위치에 대한 후속 재계산을 리플로라고 합니다. 위의 예시에서는 이미지가 반환되기 전에 초기 레이아웃이 발생한다고 가정합니다. 이미지 크기를 선언하지 않았기 때문에 이미지 크기를 알게 되면 리플로가 발생합니다.

페인트

주요 렌더링 경로의 마지막 단계는 개별 노드를 화면에 페인팅하는 것으로, 이 중 첫 번째를 첫 번째 의미 있는 페인트(First Meaningful Paint, FMP)라고 부릅니다. 페인팅 또는 래스터화 단계에서 브라우저는 레이아웃 단계에서 계산된 각 상자를 화면의 실제 픽셀로 변환합니다. 페인팅에는 텍스트, 색상, 테두리, 그림자, 그리고 버튼과 이미지와 같은 대체 요소를 포함하여 요소의 모든 시각적 부분을 화면에 그리는 작업이 포함됩니다. 브라우저는 이 작업을 매우 빠르게 수행해야 합니다.

부드러운 스크롤과 애니메이션을 보장하려면 리플로, 페인트, 스타일 계산을 포함하여 메인 스레드를 차지하는 모든 작업을 수행하는 데 브라우저에서 16.67ms 미만이 소요되어야 합니다. 2048 X 1536에서 아이패드는 화면에 칠할 픽셀이 3,145,000개가 넘습니다. 매우 빠르게 칠해야 하는 많은 픽셀입니다. 리페인트가 초기 페인트보다 훨씬 빠르게 수행될 수 있도록 화면 그리기는 일반적으로 여러 계층으로 나뉩니다. 여기에는 합성이 필요합니다.

페인팅은 레이아웃 트리의 요소를 계층으로 나눌 수 있습니다. (CPU의 메인 스레드 대신) GPU 계층으로 콘텐츠를 승격시키면 페인트 및 리페인트 성능이 향상됩니다.

계층을 인스턴스화하는 특정한 속성과 요소들이 있습니다. <video><canvas>, 그리고 opacity, 3D transform, will-change 등의 CSS 속성을 가진 모든 요소들이 이에 해당합니다. 이 노드들은 위의 이유 중 하나(또는 그 이상)로 인해 자손이 자신의 계층을 필요로 하지 않는 한 자손과 함께 자신의 계층에 그려집니다.

계층은 성능을 향상시키지만 메모리 관리 비용이 많이 들기 때문에 웹 성능 최적화 전략의 일부로 과도하게 사용해서는 안 됩니다.

합성

문서의 일부가 다른 계층에 그려져 서로 겹치는 경우, 이들이 올바른 순서로 화면에 그려지고 콘텐츠가 올바르게 렌더링되도록 합성(compositing)이 필요합니다.

페이지가 자산을 계속 불러옴에 따라 리플로가 발생할 수 있습니다. (늦게 도착한 예시 이미지가 그 예입니다.) 리플로는 리페인트와 재합성을 유발합니다. 이미지의 크기를 정의했다면 리플로가 필요하지 않았을 것이며, 필요한 계층만 다시 페인팅하고 합성했을 것입니다. 그러나 우리는 이미지 크기를 넣지 않았습니다! 서버에서 이미지를 가져오면 렌더링 프로세스가 레이아웃 단계로 돌아가서 다시 시작합니다.

상호작용성

메인 스레드가 페이지 페인팅을 마치면 ‘모든 것이 준비되었다’고 생각할 것입니다. 하지만 반드시 그런 것은 아닙니다. 로드에 올바르게 지연(defer)되고 onload 이벤트 후에만 실행되는 자바스크립트가 포함된 경우, 메인 스레드가 사용 중일 수 있으며 스크롤, 터치, 기타 상호 작용에 사용할 수 없습니다.

상호 작용까지의 시간(Time to Interactive, TTI)은 DNS 조회 및 SSL 연결로 이어진 첫 번째 요청부터 페이지가 상호 작용이 될 때까지 걸린 시간을 측정한 것입니다. 첫 번째 콘텐츠가 있는 페인트(First Contentful Paint, FCP) 이후 페이지가 50ms 이내에 사용자의 상호 작용에 응답할 수 있어야 합니다. 메인 스레드가 자바스크립트를 분석, 컴파일, 실행하는 데 사용된다면, 메인 스레드를 사용할 수 없으므로 적시(50ms 미만)에 사용자의 상호 작용에 응답할 수 없습니다.

위의 예시에서는 이미지가 빠르게 로드될 수 있지만 anotherscript.js 파일이 2MB이고 사용자의 네트워크 연결 속도가 느릴 수 있습니다. 이 경우 사용자는 페이지를 매우 빠르게 볼 수 있지만, 스크립트를 다운로드하고 파싱하여 실행할 때까지 스크롤이 버벅거릴 것입니다. 이는 좋은 사용자 경험이 아닙니다. 다음 웹 페이지 테스트 예시처럼 메인 스레드를 점유하는 것을 피해야 합니다.

메인 스레드가 빠른 연결을 통해 자바스크립트 파일의 다운로드, 파싱, 실행에 점유됨

이 예시에서 DOM 콘텐츠 로드 프로세스는 1.5초 이상 걸렸고, 메인 스레드는 전체 시간 동안 완전히 점유되어 클릭 이벤트나 화면 탭에 응답하지 않았습니다.