기본 콘텐츠로 건너뛰기

브라우저 주소창에 URL을 입력하면 화면이 뜨기까지: DNS부터 렌더링까지

브라우저 주소창에 URL을 입력하면 화면이 뜨기까지: DNS부터 렌더링까지

빠른 답

  • 주소 입력 뒤 바로 화면이 뜨는 것이 아니라 DNS 조회, 연결 수립, TLS 협상, HTTP 요청, 렌더링이 순서대로 진행됩니다.
  • 첫 화면 속도는 서버 응답 시간만이 아니라 CSS 다운로드, JavaScript 실행, 레이아웃·페인트 비용에도 크게 영향을 받습니다.
  • HTTPS 사이트는 요청 전에 인증서 검증과 키 협상이 들어가므로 TLS 단계가 별도 병목이 될 수 있습니다.
  • 병목은 Chrome DevTools의 Network와 Performance 탭으로 네트워크 지연과 렌더링 비용을 나눠 확인하는 것이 가장 빠릅니다.

흐름으로 보기

1
DNS 조회
2
TCP 연결
3
TLS 협상
4
HTTP 요청/응답
5
DOM·CSSOM 생성
6
레이아웃·페인트·컴포지팅

이 여섯 단계는 웹 페이지 로딩의 가장 큰 뼈대입니다. 앞쪽은 리소스를 받아오는 네트워크 흐름이고, 뒤쪽은 받은 데이터를 실제 화면으로 바꾸는 브라우저 렌더링 흐름입니다. 페이지가 느릴 때 원인을 정확히 찾으려면 이 둘을 섞지 말고 끊어서 봐야 합니다.

왜 이 과정을 같이 봐야 하나

브라우저 주소창에 https://www.google.com을 입력하는 행동은 단순히 “서버에 요청한다”로 끝나지 않습니다. 브라우저는 먼저 어디로 연결할지 찾아야 하고, 안전한 연결을 만들어야 하며, HTML과 정적 자원을 받아온 뒤 그것을 다시 화면용 구조로 바꿔야 합니다. 사용자가 보는 첫 화면은 서버 응답 그 자체가 아니라, 브라우저가 수행한 렌더링 파이프라인의 결과물입니다.

이 관점을 놓치면 성능 문제를 잘못 짚기 쉽습니다. 예를 들어 서버는 HTML을 빨리 내려줬는데도 화면이 늦게 뜰 수 있습니다. CSS가 늦게 와서 스타일 계산이 밀렸거나, 동기 JavaScript가 HTML 파싱을 막았거나, 레이아웃과 페인트 비용이 커졌을 수 있기 때문입니다. 그래서 이 주제는 네트워크와 렌더링을 따로 외우기보다 하나의 흐름으로 이해하는 편이 훨씬 실용적입니다.

연결이 열리기 전: DNS, TCP, TLS, HTTP

브라우저는 먼저 입력값이 검색어인지 URL인지 판단합니다. URL이라고 판단하면 도메인 이름을 IP 주소로 바꾸기 위해 DNS 조회를 시작합니다. 이때 브라우저 캐시, 운영체제 캐시, 로컬 네트워크 캐시, DNS 서버 캐시를 순서대로 활용할 수 있습니다. 캐시에 있으면 빠르게 끝나고, 없으면 추가 질의가 발생합니다.

IP를 얻은 뒤에는 서버와 연결을 만듭니다. HTTP/1.1과 HTTP/2는 기본적으로 TCP 위에서 동작하므로 먼저 3-way handshake가 필요합니다. HTTPS라면 여기에 TLS 핸드셰이크가 추가됩니다. 이 단계에서는 서버 인증서를 검증하고, 통신에 사용할 암호화 키를 협상합니다. 사용자 입장에서는 보이지 않지만, 네트워크 RTT가 크거나 인증서 검증 비용이 커지면 여기서 지연이 생길 수 있습니다.

연결이 준비되면 브라우저는 HTTP 요청을 보내고, 보통 가장 먼저 HTML 문서를 받습니다. 중요한 점은 브라우저가 HTML을 전부 받은 뒤 움직이는 것이 아니라, 받는 동안 파싱을 시작한다는 사실입니다. 그래서 HTML 안에서 CSS, JavaScript, 이미지, 폰트 같은 자원을 발견하면 후속 요청이 바로 이어질 수 있습니다.

간단한 분해 측정은 curl만으로도 어느 정도 확인할 수 있습니다.

curl -o /dev/null -s \
  -w "dns:%{time_namelookup}\nconnect:%{time_connect}\ntls:%{time_appconnect}\nttfb:%{time_starttransfer}\ntotal:%{time_total}\n" \
  https://www.google.com

이 출력에서 dns, connect, tls, ttfb, total을 나눠 보면 네트워크 쪽 병목이 어디에 있는지 대략 감이 옵니다. 예를 들어 ttfb는 짧은데 브라우저 첫 화면이 늦다면, 서버보다 렌더링 비용을 먼저 의심하는 편이 맞습니다.

응답이 화면이 되는 핵심: DOM, CSSOM, 렌더 트리

HTML 응답이 도착하면 브라우저는 파서를 돌려 DOM을 만듭니다. DOM은 문서 구조를 메모리 안의 트리로 표현한 것입니다. 제목, 문단, 버튼, 리스트 같은 요소들이 부모-자식 관계로 연결됩니다. 이 단계는 “문서 구조를 이해하는 단계”라고 보면 됩니다.

CSS 파일이 도착하면 브라우저는 CSSOM을 만듭니다. CSSOM은 어떤 선택자에 어떤 스타일 규칙이 적용되는지를 정리한 스타일 구조입니다. DOM만으로는 화면을 그릴 수 없고, CSSOM까지 있어야 각 요소의 실제 모습과 계산 기준이 정해집니다. 그래서 CSS는 초기 렌더링 속도에 직접적인 영향을 줍니다.

이후 브라우저는 DOM과 CSSOM을 결합해 렌더 트리를 만듭니다. 여기에는 실제 화면에 표시할 노드만 들어갑니다. 문서 구조 전체를 담는 DOM과 달리, 렌더 트리는 “그릴 대상”에 더 가깝습니다. 예를 들어 화면에 표시되지 않는 일부 요소는 DOM에는 있어도 렌더 트리에는 빠질 수 있습니다.

이 지점에서 자주 나오는 오해가 있습니다. “HTML만 받으면 거의 다 끝난 것 아닌가?”라는 생각입니다. 실제로는 그렇지 않습니다. CSSOM이 준비되지 않으면 스타일 계산을 완료할 수 없고, 스타일이 확정되지 않으면 레이아웃도 안정적으로 진행되지 않습니다. 첫 화면 속도에서 CSS가 중요한 이유가 바로 여기에 있습니다.

레이아웃, 페인트, 컴포지팅은 각각 무엇을 하나

렌더 트리가 준비되면 브라우저는 각 요소의 위치와 크기를 계산합니다. 이 단계가 레이아웃입니다. 박스 모델, 줄바꿈, 폰트 크기, 부모 폭, flex, grid, 스크롤 영역 같은 정보가 여기서 반영됩니다. 요소 크기나 문서 구조가 바뀌면 레이아웃이 다시 일어날 수 있습니다.

레이아웃 다음은 페인트입니다. 이 단계에서는 텍스트 색상, 배경, 테두리, 그림자, 이미지 같은 시각 정보를 실제 그리기 명령으로 바꿉니다. 요소 수가 많거나 스타일 효과가 복잡하면 페인트 비용이 커질 수 있습니다. 특히 큰 배경 이미지, 블러, 복잡한 그림자는 생각보다 비용이 큽니다.

마지막이 컴포지팅입니다. 브라우저는 요소를 여러 레이어로 나누고, 필요한 레이어만 합성해 최종 화면을 만듭니다. transform이나 opacity 같은 속성은 레이아웃 전체를 다시 계산하지 않고 컴포지팅 단계에서 처리되는 경우가 많아 애니메이션 최적화에 자주 활용됩니다. 다만 레이어를 무작정 늘리는 것이 항상 좋은 것은 아니고, 메모리 사용량과 관리 비용도 함께 봐야 합니다.

실무에서는 “변경이 어느 단계까지 전파되는가”를 보는 습관이 중요합니다.

  • 박스 크기나 텍스트 내용 변경은 레이아웃까지 다시 갈 수 있습니다.
  • 배경색 변경은 보통 페인트만 다시 일으킵니다.
  • transform 기반 이동은 컴포지팅에서 끝날 가능성이 높습니다.

이 차이를 이해하면 어떤 UI 변경은 부드럽고, 어떤 변경은 스크롤 중 끊기는지 설명하기 쉬워집니다.

첫 화면을 늦추는 흔한 비용

첫 화면을 늦추는 대표적인 비용은 렌더링 차단 CSS와 동기 JavaScript입니다. CSS는 스타일 계산에 필요하므로 늦게 도착하면 화면 구성이 밀립니다. 동기 스크립트는 HTML 파싱을 멈출 수 있고, 실행 중 DOM을 바꾸면 이후 레이아웃과 페인트 비용도 커집니다. 그래서 “HTML 응답이 빨랐다”는 사실만으로는 체감 속도를 설명할 수 없습니다.

또 하나 흔한 문제는 강제 동기 레이아웃입니다. 레이아웃 값을 읽은 직후 스타일을 바꾸는 패턴을 반복하면 브라우저는 중간 계산을 계속 강제로 수행해야 할 수 있습니다. 흔히 layout thrashing이라고 부르는 문제입니다.

다음 코드는 같은 루프 안에서 레이아웃 값을 읽고 스타일을 바로 쓰는 예입니다.

const cards = document.querySelectorAll('.card');

for (const card of cards) {
  const width = card.getBoundingClientRect().width;
  card.style.height = `${width * 0.6}px`;
}

목록이 작을 때는 티가 잘 안 나지만, 요소 수가 많아지면 레이아웃 계산이 반복되어 메인 스레드 점유 시간이 길어질 수 있습니다. 읽기와 쓰기를 분리하면 비용을 좀 더 예측 가능하게 만들 수 있습니다.

const cards = [...document.querySelectorAll('.card')];
const widths = cards.map((card) => card.getBoundingClientRect().width);

requestAnimationFrame(() => {
  cards.forEach((card, index) => {
    card.style.height = `${widths[index] * 0.6}px`;
  });
});

이 패턴이 모든 상황의 정답은 아니지만, 브라우저 렌더링 비용을 이해하는 데는 좋은 예시입니다. 핵심은 “DOM을 바꿨다”가 아니라 “그 변경이 레이아웃, 페인트, 컴포지팅 중 어디까지 다시 일으키는가”입니다.

DevTools로 병목을 어떻게 찾나

이 주제를 공부할 때 가장 실용적인 도구는 Chrome DevTools입니다. Network 탭은 요청 단위 병목을 보여 줍니다. DNS, 연결, SSL, 대기, 다운로드 시간을 나눠 볼 수 있고, 어떤 리소스가 늦게 왔는지, 캐시가 적용됐는지, 리다이렉트가 있었는지 확인하기 쉽습니다.

반면 Performance 탭은 브라우저 내부 작업을 보여 줍니다. 긴 JavaScript 실행, 스타일 재계산, 레이아웃, 페인트, 컴포지팅, 메인 스레드 Long Task가 어느 시점에 발생했는지를 볼 수 있습니다. 즉, Network는 “받아오는 비용”, Performance는 “받은 뒤 처리하는 비용”을 보는 도구에 가깝습니다.

실전에서는 아래 순서가 효율적입니다.

  • Network에서 HTML, CSS, JS의 도착 시점과 우선순위를 먼저 본다.
  • Performance에서 긴 Task, 잦은 Layout, 큰 Paint 구간을 확인한다.
  • Coverage로 초기 로딩에 실제로 쓰는 CSS·JS 비율을 점검한다.
  • Disable cache와 CPU/Network throttling을 켜고 다시 재현해 본다.

Lighthouse 점수는 요약 지표로는 유용하지만, 실제 원인을 찾을 때는 타임라인과 요청 waterfall을 직접 읽는 편이 더 정확합니다.

구성과 설정으로 줄일 수 있는 비용

브라우저 렌더링은 프런트 코드만으로 최적화되지 않습니다. 서버와 CDN 설정도 큰 영향을 줍니다. 대표적인 것이 캐시 정책과 압축입니다. CSS, JS, 폰트처럼 자주 바뀌지 않는 파일은 긴 캐시를 걸고, 파일명에 해시를 넣는 구성이 일반적입니다.

다음은 정적 자원 캐시와 압축을 함께 고려한 예시입니다.

location /assets/ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

gzip on;
gzip_types text/css application/javascript application/json text/plain;
gzip_min_length 1024;

이 설정은 재방문 비용을 줄이고 전송량도 줄여 줍니다. 다만 HTML 문서까지 무조건 오래 캐시하면 배포 직후 갱신이 늦어질 수 있으므로, 문서와 정적 자산을 같은 정책으로 다루면 안 됩니다. 보통 HTML은 짧게, 해시가 붙은 정적 파일은 길게 캐시합니다.

스크립트 로딩 전략도 중요합니다. 모든 스크립트를 초기에 동기 실행하면 HTML 파싱과 렌더링이 쉽게 지연됩니다. 초기 상호작용에 꼭 필요한 코드만 앞에 두고, 나머지는 defer, 코드 스플리팅, 지연 로딩을 활용하는 편이 낫습니다. 목표는 “자바스크립트를 무조건 줄이기”가 아니라 “첫 화면과 직접 관련 없는 실행을 뒤로 미루기”입니다.

면접과 실무에서 자주 틀리는 설명

가장 흔한 실수는 네트워크와 렌더링을 하나로 뭉개서 설명하는 것입니다. DNS -> TCP -> HTTP까지만 말하면 절반만 설명한 셈입니다. 사용자가 체감하는 “화면이 떴다”는 순간은 DOM, CSSOM, 렌더 트리, 레이아웃, 페인트, 컴포지팅까지 이어진 뒤에야 만들어지기 때문입니다.

두 번째 실수는 JavaScript를 항상 “렌더링 이후의 후처리”처럼 설명하는 것입니다. 동기 스크립트는 HTML 파싱을 멈출 수 있고, DOM과 스타일 계산에도 직접 영향을 줍니다. 즉, JavaScript는 렌더링 뒤에 따로 붙는 것이 아니라 렌더링 파이프라인 자체에 개입할 수 있습니다.

세 번째 실수는 서버 응답만 빠르면 첫 화면도 자동으로 빠르다고 생각하는 것입니다. 실제로는 CSS가 늦거나, 폰트가 늦거나, 메인 스레드가 바쁘면 사용자는 빈 화면이나 불완전한 화면을 더 오래 보게 됩니다. 그래서 성능 문제는 서버, 네트워크, 브라우저 렌더링을 단계별로 나눠서 봐야 합니다.

면접 답변으로는 다음 흐름이면 충분히 탄탄합니다. 브라우저가 URL을 해석하고 DNS로 IP를 찾은 뒤 TCP와 TLS로 연결을 준비합니다. 그다음 HTTP 요청으로 HTML을 받고, HTML과 CSS를 파싱해 DOM과 CSSOM을 만든 뒤 렌더 트리, 레이아웃, 페인트, 컴포지팅을 거쳐 화면을 그린다고 설명하면 됩니다. 마지막에 “병목은 DevTools의 Network와 Performance 탭으로 나눠 확인한다”까지 붙이면 실무 감각도 드러납니다.

원문 참고

https://www.maeil-mail.kr/question/20

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...