(브라우저 동작 과정) URL을 입력하면?

후추쌈·2024년 10월 18일
0

바닐라JS

목록 보기
1/3
post-thumbnail
  1. 바닐라JS 프로젝트 소개
  2. 브라우저 동작 방식
    (1) 탐색 - URL을 통해 자원 위치 탐색, DNS 조회 및 서버와 연결
    (2) 응답 - 서버로부터 HTML, CSS, JavaScript 등의 자원 받기
    (3) 구문 분석 - HTML과 CSS를 각 노드 트리로 변환
    (4) 렌더 - 렌더 트리로 합치기, 크기와 위치 계산, 화면에 그리기
    (5) 상호 작용 - 사용자 입력 처리 및 이벤트 대응

React.js만 써와서 JSX 문법에만 익숙하다. 빌드 후 바닐라JS로 컴파일되고, 이 결과물을 브라우저가 실행하는데 어떻게 바뀌고 어떻게 돌아가는지 궁금했다.

Momentum을 클론코딩하며 리액트로는 간단한 기능이 바닐라JS로는 어떨지 비교해보면서 구현해보면 재밌을 거 같다.

꼭 모멘텀을 만들기 위해 사용된 코드들을 정리하는 글은 아닐거다. 한 개념을 알면 꼬리를 물고 물어 넓지만 얕지 않게 공부해보려 한다.

기획한 기능들은 다음과 같다.
- 유저 이름과 투두리스트를 입력받아 저장한다. (저장은 엔터키)
- 유저 이름을 기준으로 로그인과 로그아웃이 가능하다. (새로고침에도 로그인이 유지)
- 드래그앤드랍으로 투두리스트를 카드 밖으로 내보내어 삭제한다.
- 투두리스트를 모두 완료하면, 카드가 돌아간다.
- 타이머로 공부 시간을 측정할 수 있고, 시간이 쌓일수록 배경이 화려해진다. (스톱은 스페이스바)
- 현재 시간을 보여준다.
- 날씨 API로 위치 기반 날씨를 보여준다.
- 랜덤으로 명언을 보여준다.
- 자정이 되면 유저 이름, 투두리스트, 타이머 모두 초기화된다.

이번 프로젝트의 목표는 브라우저에서 동작하는 코드에 대해서 알자이다.
그 전에 브라우저가 어떻게 동작하는지 알아야 하지 않을까?

URL을 입력하여 어떻게 자원을 받을 수 있을까?

  1. 프로토콜 정의
  2. DNS 조회
  3. TCP 연결
  4. HTTP 요청 및 응답

프로토콜 정의


브라우저와 서버가 데이터를 주고받는 방식을 정해준다. HTTP는 데이터를 암호화하지 않고 전송하고, HTTPS는 SSL/TLS를 통해 누군가가 데이터를 가로채더라도 내용을 알아볼 수 없도록 암호화한다.

SSL(Secure Sockets Layer)는 원래 사용되던 암호화 프로토콜이고, 현재는 그 후속 버전인 TLS(Transport Layer Security)가 표준으로 사용된다. HTTPS는 이 TLS 암호화를 적용한 HTTP이다.

동아리 웹사이트를 만들 때 백엔드 서버에 IP 주소를 사용해 접근했는데, CORS(Cross-Origin Resource Sharing) 에러가 발생했다. 이는 특정 도메인에 대해서만 자원을 가져갈 수 있도록 제한하는 브라우저의 보안 정책 때문이다.
IP 주소 대신 도메인을 사용해야 했고, 이를 위해 도메인을 연결했지만, HTTP로 연결했을 때도 CORS 에러가 계속 발생했다. 프론트엔드 서버는 HTTPS, 백엔드 서버는 HTTP였는데 프로토콜이 다르면 서로 다른 출처(origin)로 간주하기 때문이다. 프로토콜, 호스트, 포트가 모두 origin과 같아야 한다.
결국 인증서를 발급받아 HTTPS를 사용해야 했던 기억이 있다.

DNS 조회 (호스트에서 IP 주소 확인)

호스트는 도메인(예: site.com) 또는 IP 주소로 서버의 위치이다. 도메인의 경우 DNS 조회를 통해 공인 IP를 얻는다. 이때 얻게된 IP는 일정 기간 동안 캐시되어 더 빠르게 얻을 수 있다.

어떤 네트워크에 연결되어 있냐에 따라 DNS 요청 경로가 다르다.
PC (고정된 네트워크): PC(유선, 와이파이)는 라우터를 통해 ISP 네트워크로 연결되며, DNS 서버에 요청을 보낸다.
모바일 (셀룰러 네트워크): 모바일 기기(3G, 4G, 5G)는 셀 타워(기지국)를 통해 이동 통신사 네트워크로 접속하여 DNS 서버로 요청을 보낸다.

클라우드 또는 온프레미스에 배포할 경우 도메인과 IP를 어떻게 연결할까?
우선 IP 종류를 살펴보자.

  • 고정 IP: IP 주소가 변하지 않으며, 외부에서 언제나 같은 IP로 서버에 접근할 수 있다.
  • 유동 IP: 시간이 지남에 따라 IP가 변경될 수 있다. 주로 가정용 네트워크나 이동 통신망에서 사용되며, IP가 바뀌면 외부에서 서버에 접근하기가 어려워질 수 있다.
  • 공인 IP: 외부 인터넷에서 접근 가능한 IP이다.
  • 사설 IP: 내부 네트워크에서만 사용되는 IP 주소이다.

교내 동아리 웹사이트를 만들고 배포할 방법은 두 가지였다. 첫 번째는 GitHub Pages와 같은 클라우드 기반, 두 번째는 온프레미스이다.
동아리 이름(quipu)을 서브 도메인으로 메인 도메인은 학교 도메인(uos.ac.kr)을 달려면 어떻게 해야할까?

  • GitHub Pages에 배포할 경우
    GitHub Pages는 이미 공인IP를 갖고 있기 때문에 비교적 간단하다.
    GitHub의 공인 IP를 학교 측에 전달해, 발급받은 quipu.uos.ac.kr에 GitHub 서버의 공인 IP을 연결하면 된다.

  • 온프레미스 서버에 배포할 경우
    온프레미스는 학교 내 네트워크에 연결되어 있다. 학교 외부 네트워크에서 접근하기 위해서 공인 IP가 필요하고, 온프레미스 재부팅 시에도 바뀌지 않아야 하기 때문에 고정 IP를 사용해야 한다. 따라서 학교 측에서 quipu.uos.ac.kr 도메인과 여기에 연결된 고정 공인 IP와 네트워크 포트(80/443)를 제공받아야 한다. 이를 온프레미스 서버와 연결하여 외부 네트워크에서도 접근 할 수 있도록 한다.

TCP 연결 (3-way handshake)

DNS 조회로 얻은 IP 주소로 브라우저와 서버를 연결한다. 이 과정은 3-way handshake로 안정적으로 이루어진다.

  • SYN(Synchronize)
    클라이언트가 서버에 SYN 패킷을 보내어 연결 요청을 시작한다.
    (클라이언트가 손을 내밀어 "안녕하세요"라고 한다.)

  • SYN-ACK(Synchronize-Acknowledge)
    서버는 이 요청을 받고 SYN-ACK 패킷으로 응답하며, 클라이언트의 요청을 확인하고 연결 수립을 준비한다.
    (서버가 손을 내밀며 "안녕하세요, 반갑습니다"라고 답한다.)

  • ACK(Acknowledge)
    클라이언트는 서버의 응답을 확인한 후 ACK 패킷을 전송하여, 연결된다.
    (클라이언트가 손을 잡으며 "네, 이제 대화를 시작합시다"라고 답하면, 대화(데이터 통신)가 시작됩니다.)

HTTP 요청 및 응답

TCP 연결이 완료되면, 다음 정보를 포함한 HTTP 요청이 서버로 전송된다.

  • 포트는 서버 내 어떤 서비스에 연결할지 지정한다. HTTP는 80, HTTPS는 443 포트를 사용하고 생략할 수 있다.
  • 경로로 서버의 특정 리소스에 접근한다. 예를 들어, quipu.uos.ac.kr/home이라면 /home 경로에 있는 리소스에 접근하는 것입니다.
  • 쿼리(매개변수)로 서버에 추가적인 정보를 전달한다. ?p1=v1&p2=v2 형태로 서버에 매개변수를 전달
    서버는 이 요청을 처리하고, 브라우저에 HTTP 응답으로 HTML 문서나 JSON 데이터를 포함하여 보낸다.

서버가 브라우저로 데이터를 보낼 때 데어터를 세그먼트로 나누어 전송한다.
너무 많은 데이터를 한 번에 보내거나 너무 적게 보내는 것을 방지하기 위해 혼합 제어(Congestion Control) 알고리즘을 사용하고 이 중 하나가 TCP 슬로우 스타트(TCP Slow Start)이다.

TCP 슬로우 스타트

  • 처음에는 작은 양의 데이터만 전송하며, 네트워크 최대 처리량을 파악한다.
  • 전송할 수 있는 데이터 양은 혼잡 윈도우(CWND) 값에 의해 결정된다.
  • 브라우저가 ACK(승인) 패킷을 보내면, CWND 값이 두 배로 증가하여 전송량을 늘린다.
  • ACK를 받지 못하거나 네트워크가 혼잡하면, CWND가 감소하여 전송량을 줄이고 네트워크 부하를 줄인다.

이렇게 상황에 맞춰 효율적으로 데이터를 전송할 수 있다.

HTML과 CSS와 JS는 어떻게 변하고 동작할까?

  1. 구문 분석하여 DOM, CSSOM 생성
  2. 렌더 트리 생성
  3. 레이아웃
  4. 페인팅과 합성

DOM과 CSSOM

HTML 구문 분석기는 HTML 파일을 순차적으로 읽으며 각 태그를 부모 또는 자식 노드로 하여 DOM 트리를 만든다.
구문 분석기는 메인 스레드에서 작동되는데 프리로드 스캐너를 하여 렌더링 성능을 높일 수 있다.
프리로드 스캐너는 구문 분석기에 비동기적으로 실행되어 이미지, CSS, JS 파일 등 외부 자원을 미리 스캔하고 다운로드 해놓는다.
이미지, CSS는 논 블록킹 자원이고, JS는 async나 defer으로 비동기 처리할 수 있지만, 프리로드 스캐너가 미리 다운로드하면 이미 준비된 상태가 되도록 최적화를 돕는다.

또 CSS를 읽으며 CSSOM 트리를 만드는데 CSS 규칙들이 DOM 요소에 어떻게 적용될지 결정한다.
프리로드 스캐너에 의해 CSS 파일을 미리 다운로드 하여 CSSOM 트리를 형성한다.
HTML 태그 안 인라인으로 정의된 스타일은 구문 분석기에서 처리되어 CSSOM 트리에 뒤늦게 반영된다.

<div style="color: red;">Hello</div>

렌더

DOM과 CSSOM을 결합하여 렌더 트리를 만드는 데, 이는 브라우저가 화면에 그릴 요소를 결정한다.
display: none와 같이 화면에 나타나지 않은 태그는 포함하지 않고, visibility: hidden와 같이 자리를 차지하는 요소는 렌더 트리에 포함된다. 이렇게 렌더 트리는 화면에 표시될 요소의 구조와 스타일을 모두 반영한다.

이후 레이아웃은 뷰포트 크기에 맞춰 모든 노드의 크기와 위치를 결정한다.
이후 이미지 크기가 나중에 결정되거나 동적인 자원의 추가 또는 크기 변경이 발생하면 리플로우가 발생하여 크기나 위치를 재계산한다. (리플로우를 최소화하여 성능을 최적화 시키는 것이 중요하다.)

페인트와 합성

페인트는 요소가 실제로 화면에 그려지는 단계이다. 레이아웃 단계에서 계산된 위치와 크기를 기반으로, 시각적 송성(텍스트, 색상, 이미지, 경계선 등)을 픽셀로 변환하여 그린다. 이 과정에서 첫 번째 의미 있는 페인트(First Meaningful Paint)가 발생하고 사용자가 처음으로 볼 수 있게 된다.

레이어로 요소를 나누어 그리며 리페인트 시 전체 화면을 다시 그리지 않아서 페인팅 작업을 최적화할 수 있다. <video><canvas>, opacity, transform와 같은 요소와 속성은 자신만의 레이어를 생성하여 독립적으로 그려지고 GPU(빠른 그래픽 연산)가 이 작업을 처리하게 되어 효율적이다. 그 외의 요소들은 자식 노드와 함께 그려진다. 너무 많은 레이어로 메모리 사용이 증가할 수 있어서 필요한 경우에만 레이어를 사용해야 한다.

/* opacity나 transform이 자주 변하는 요소를 GPU로 격상 */
.someElement {
  will-change: opacity, transform;
}

이후 레이어들을 올바른 순서로 그려지도록 합성하여 정확한 화면을 출력한다.

만약 이미지나 동적 콘텐츠처럼 자원이 나중에 로드되면, 리플로우가 발생하여 레이아웃이 변경될 수 있다. 이후, 변경된 요소는 리페인트 되어야 하고, 필요에 따라 재합성이 이루어진다. 이미지 크기를 미리 정해두면 리플로우를 방지할 수 있고, 리페인트와 합성 과정만 발생하여 성능 저하를 최소화할 수 있다.

사용자 입력에 대한 처리는 어떻게 이루어질까?

동기적 또는 비동기적으로 이벤트 처리

페인트와 합성이 끝나도 JS 파일이 늦게 로드되고 있다면 스크롤이나 클릭 같은 사용자 상호작용이 지연될 수 있다. (페이지는 보여도 버벅거리는 스크롤을 경험하게 된다.)
Time to Interactive(TTI)는 페이지가 상호작용할 준비가 될 때까지 걸리는 시간이다. DNS 조회부터 사용자와 상호작용할 수 있을 때까지의 시간인데, 이는 브라우저가 50ms 이내에 사용자 입력에 응답할 수 있을 때를 의미한다.

JS파일이 다운로드되고 실행되면, DOM 요소에 이벤트 리스너를 설정한다. 해당 이벤트가 발생하면 연결된 핸들러 코드가 실행된다.
JS는 단일 메인 스레드에서 실행되기 때문에, 메인 스레드에서 DOM 조작과 이벤트 처리 모두를 동작한다.
비동기 작업(AJAX 요청, 타이머 등)을 백그라운드에서 처리하고, 이벤트 큐에 콜백 함수를 넣어 메인 스레드가 준비되면 처리를 한다.
만약 리플로우와 리페인트 같은 렌더링 작업이 메인 스레드에서 진행 중일 때, 이벤트 리스너가 호출되면 블로킹이 발생 할 수 있다.
렌더링을 방해할 수 있는 무거운 작업(서버 통신, 복잡한 계산)은 메인 스레드가 블로킹될 수 있기 때문에, 비동기 처리로 바로 작업을 처리하지 않도록 설계해야 한다.

웹 성능을 최적화 시킨다는 건?

웹 성능을 최적화 한다는 것은 페이지 로드 속도를 향상시키고 사용자 상호작용을 부드럽게 하는 것이다.

브라우저가 자주 변경되지 않는 리소스를 캐싱하여 다시 다운로드를 하지 않도록 해야한다.
CSS 파일을 위쪽에, JS 파일을 아래쪽에 배치하여 CSSOM이 빠르게 생성되고, 렌더링을 차단하지 않도록 해야한다.
display: none같은 스타일을 미리 적용하거나, 불필요한 DOM 요소를 최소화하여 렌더 트리 생성 시간을 줄인다. 리플로우는 매우 비용이 많이 드는 작업이므로, DOM 변경을 최소화한다. 특히 이미지 크기를 미리 지정한다.
애니메이션이나 transform, opacity이 자주 발생하는 경우 GPU 가속을 사용해 레이어를 분리하여 페인팅 성능을 높인다.
이벤트 리스너가 호출되면 즉시 무거운 작업을 하지 않고, 비동기적으로 백그라운드에서 처리하여 메인 스레드의 부하를 줄인다. setTimeout 또는 requestAnimationFrame 등이 있다.
또 페이지의 반응성을 저하시킬 스크롤 또는 터치 이벤트에서 Passive 이벤트 리스너를 사용하여 이 이벤트 리스너가 DOM 조작을 하지 않겠다는 신호를 보내어 렌더링을 위한 준비를 하지 않도록 한다.

document.addEventListener('scroll', handleScroll, { passive: true });

레퍼런스
https://developer.mozilla.org/ko/docs/Web/Performance/How_browsers_work

profile
LEE YENA

0개의 댓글