Skip to content
This page has been auto-translated and may contain errors.View in English

웹이 동작하는 방식

docs.scrimba.com

여러분이 여는 모든 페이지는 같은 짧은 여정을 거칩니다. 여러분은 주소로 무언가를 요청하고, 어딘가에 있는 컴퓨터가 텍스트 문서를 돌려보내며, 브라우저는 그 텍스트를 눈에 보이는 페이지로 바꿉니다. HTML은 그 텍스트가 쓰여지는 언어이므로, 이 여정을 이해하는 것이 이 핸드북의 다른 모든 내용을 이해하는 배경이 됩니다.

클라이언트와 서버

웹은 양쪽이 주고받는 대화입니다. 여러분의 브라우저는 페이지를 요청하는 프로그램인 **클라이언트**입니다. 웹사이트를 가지고 있다가 건네주는 컴퓨터는 서버입니다. 여러분이 물으면 서버가 답합니다.

식당에서 주문하는 상황을 떠올려 보세요. 여러분은 손님이고, 주방은 보이지 않는 곳에 있으며, 웨이터가 여러분의 주문을 주방으로 전달했다가 다시 가져다줍니다. 여러분이 직접 주방에 들어가서 요리하지는 않습니다. 음식을 주문하면 주방에서 준비해서 내보내 줍니다. 여러분의 브라우저는 손님이고, 서버는 주방이며, 웹은 그 사이를 오가는 웨이터입니다.

페이지를 불러올 때마다 **클라이언트**가 서버와 대화를 나눕니다. 클라이언트는 요청을 보내는 프로그램으로, 거의 항상 브라우저이지만 모바일 앱이나 커맨드라인 도구일 수도 있습니다. 서버는 어딘가의 컴퓨터에서 실행되며 요청을 기다렸다가 응답을 돌려보내는 프로그램입니다.

이 관계는 처음에는 한쪽 방향으로만 흐릅니다. 클라이언트가 항상 먼저 말을 건넵니다. 서버가 아무 이유 없이 갑자기 페이지를 밀어 보내는 일은 없습니다. 서버는 클라이언트가 무언가를 요청할 때까지 기다렸다가 그때 응답합니다. 서버 하나가 수천 개의 클라이언트에 동시에 응답할 수 있는데, 이것이 하나의 웹사이트가 같은 주소로 수많은 방문자에게 서비스를 제공할 수 있는 이유입니다.

클라이언트와 서버는 고정된 기기가 아니라 역할입니다. 클라이언트는 요청을 시작하는 쪽이고, 서버는 요청을 기다렸다가 응답을 돌려보내는 쪽입니다. 같은 물리적 컴퓨터가 한 연결에서는 서버이면서 다른 연결에서는 클라이언트일 수 있습니다. 예를 들어 웹 서버가 스스로 데이터베이스를 호출하는 경우가 그렇습니다.

이 모델은 근본적으로 요청 중심이며 상태를 갖지 않습니다(stateless). stateless란 서버가 각 요청을 독립적으로 완결된 것으로 처리한다는 뜻입니다. 즉 기본적으로 이전 요청에 대한 어떤 것도 기억하지 않습니다. 그래서 로그인 상태는 이후 요청에서 다시 확인되어야 하며, 보통 클라이언트가 매번 함께 보내는 쿠키나 토큰을 통해 이루어집니다. 연결 자체는 아무런 기억도 담고 있지 않기 때문입니다. 프로토콜을 stateless하게 유지하는 것이 서버 한 대가 엄청난 트래픽까지 확장될 수 있게 해주는데, 요청과 요청 사이에 방문자마다 열린 컨텍스트 스레드를 유지할 필요가 없기 때문입니다.

Juno클라이언트와 서버 양쪽이 나누는 하나의 대화: 여러분의 브라우저는 묻는 쪽인 클라이언트이고, 서버는 답하는 쪽인 컴퓨터입니다. 손님과 주방, 그 사이의 웨이터처럼, 여러분은 절대 주방에 들어가지 않고 주문만 하면 음식이 나옵니다. 이 그림을 기억해 두세요, 이 페이지의 나머지 내용도 결국 이 그림의 다른 버전일 뿐입니다.
Juno클라이언트와 서버 클라이언트가 묻고 서버가 답하며, 항상 클라이언트가 먼저 말을 건넵니다. 서버 하나가 동시에 여러 클라이언트를 처리하는데, 바로 이 덕분에 하나의 주소가 수많은 방문자에게 서비스를 제공할 수 있습니다. 무언가가 로딩될 때, 여러분은 이런 주고받기의 한 사이클을 보고 있는 것입니다.
Juno클라이언트와 서버 클라이언트와 서버는 정해진 상자가 아니라 역할이며, 같은 기기가 두 역할을 모두 맡을 수 있습니다. 웹은 기본적으로 stateless이므로, 클라이언트가 매번 증거를 다시 보내지 않는 한 서버는 요청 사이에 아무것도 기억하지 못합니다. 쿠키나 토큰이 바로 그 증거 역할을 합니다. 이 stateless함은 극복해야 할 한계가 아니라, 서버 한 대가 실제 트래픽을 버텨낼 수 있게 해주는 조건입니다.

URL과 도메인

여러분이 입력하는 주소는 URL, 즉 Uniform Resource Locator입니다. 우편 주소가 집 한 채를 가리키는 것처럼, 웹에서 한 가지를 가리키는 완전한 주소입니다.

https://scrimba.com/learn을 보세요. https 부분은 브라우저가 서버와 어떻게 대화할지를 나타냅니다. scrimba.com 부분은 도메인으로, 거리와 동네 이름처럼 웹사이트의 이름입니다. /learn 부분은 집 번지수처럼 특정 페이지로 가는 경로입니다. 이 세 부분이 합쳐져 정확히 하나의 페이지를 가리키며, 이를 입력하면 브라우저가 어디로 가야 할지 알게 됩니다.

**URL**은 여러 부분으로 이루어져 있고, 각 부분에 이름을 붙이면 전체를 읽기가 훨씬 쉬워집니다. https://scrimba.com/learn/html을 예로 들어 보겠습니다.

  • https는 **스킴(scheme)**으로, 브라우저가 페이지를 가져올 때 사용하는 프로토콜입니다.
  • scrimba.com도메인으로, 사람이 읽기 쉬운 사이트 이름입니다.
  • /learn/html은 **경로(path)**로, 그 사이트에서 어떤 페이지인지를 나타냅니다.

도메인은 숫자를 대신하는 친절한 이름입니다. 모든 서버는 192.0.2.10처럼 생긴 문자열인 IP 주소로 접근할 수 있고, 도메인 이름 덕분에 이 숫자를 외울 필요가 없습니다. 도메인을 입력하면 브라우저는 DNS, 즉 Domain Name System을 통해 IP 주소를 찾아보는데, 이는 이름을 번호로 바꿔주는 전화번호부처럼 동작합니다.

**URL**은 브라우저가 리소스를 찾는 데 필요한 모든 정보를 담고 있으며, 각 구간에는 각자의 역할이 있습니다. https://shop.example.com:443/products?id=42#reviews에서: 스킴은 https, shop은 서브도메인, example.com은 등록된 도메인, 443은 포트, /products는 경로, ?id=42는 쿼리 스트링, #reviews는 프래그먼트입니다.

포트는 서버에 있는 번호가 붙은 문(門)입니다(443은 HTTPS의 기본 포트라서 보통 생략됩니다). 쿼리 스트링은 서버에 매개변수를 전달하며, 이에 따라 서버가 돌려주는 결과가 달라질 수 있습니다. # 뒤의 프래그먼트는 오직 브라우저만 처리하며 서버로는 절대 전송되지 않습니다. 페이지 내부의 한 위치를 가리킬 뿐입니다. 도메인을 IP 주소로 바꾸는 과정은 DNS(Domain Name System, 이름을 번호로 바꿔주는 웹의 주소록)를 거치며, 이 조회 자체가 네트워크 왕복이기 때문에 콜드 DNS 조회는 페이지의 첫 바이트가 도착하기도 전에 사용자가 감당해야 하는 지연 시간입니다. 이 구조를 알아두면 실용적입니다. 서버가 볼 수 있는 것(경로와 쿼리)과 볼 수 없는 것(프래그먼트), 그리고 느린 첫 로딩이 어디에 숨어 있을 수 있는지를 알려주기 때문입니다.

JunoURL과 도메인 URL은 한 페이지의 완전한 주소입니다. https는 서버와 어떻게 대화할지를 나타내고, scrimba.com 같은 도메인은 사이트의 이름이며, /learn 같은 경로는 그 사이트의 한 페이지를 가리킵니다. 왼쪽에서 오른쪽으로 읽으면 브라우저가 정확히 어디로 향하는지 알 수 있습니다.
JunoURL과 도메인 URL은 스킴, 도메인, 경로로 나뉘고, 이 구성을 한번 파악하면 어떤 주소든 한눈에 읽을 수 있습니다. 도메인은 숫자를 대신하는 친절한 이름입니다. DNS는 scrimba.com을 브라우저가 실제로 접속할 수 있는 IP 주소로 바꿔주는 전화번호부입니다. 이 조회는 페이지가 로딩되기 전에 조용히 이루어집니다.
JunoURL과 도메인 URL의 모든 부분에는 존재 이유가 있습니다: 스킴, 서브도메인, 도메인, 포트, 경로, 쿼리, 프래그먼트. 서버는 경로와 쿼리는 볼 수 있지만 프래그먼트는 절대 볼 수 없습니다. # 뒤의 부분은 브라우저에만 남기 때문입니다. 그리고 DNS 조회는 실제 네트워크 왕복이므로, 콜드 조회는 첫 바이트가 오기 전에 사용자가 감당하는 지연 시간입니다. 첫 로딩이 느리게 느껴질 때 기억해 둘 만한 사실입니다.

요청과 응답 사이클

엔터를 누르면 브라우저는 서버에 **요청(request)**을 보냅니다. "이 페이지를 보내주세요"라고 말하는 짧은 메시지입니다. 서버는 페이지 자체인 **응답(response)**을 돌려보냅니다. 이 왕복 한 번이 완전한 한 번의 트립이며, 여러분이 페이지를 열거나 링크를 클릭할 때마다 매번 일어나는 일입니다.

사서에게 책을 요청하는 상황을 떠올려 보세요. 여러분이 제목을 말하면(요청), 사서가 그 책을 찾아 건네줍니다(응답). 이제 여러분 손에 책이 있습니다. 책이 없다면 사서는 그 사실을 알려주는데, 이것도 하나의 응답입니다. 웹도 똑같이 동작하지만, 훨씬 더 빠르고 1초에 여러 번 일어납니다.

페이지 로딩은 하나의 **요청(request)**과 하나의 **응답(response)**으로 이루어집니다. 브라우저는 원하는 페이지가 무엇인지, 그리고 어떻게 받고 싶은지를 명시하는 요청을 보냅니다. 서버는 페이지와 함께 요청이 어떻게 처리되었는지를 알려주는 **상태 코드(status code)**를 담은 응답을 돌려보냅니다.

다음 세 가지 상태 코드는 항상 마주치게 되므로 먼저 익혀두세요.

  • 200 OK: 성공했습니다, 요청하신 내용입니다.
  • 404 Not Found: 서버는 정상이지만, 그 주소에는 아무것도 없습니다.
  • 301 Moved Permanently: 이 페이지는 이제 다른 곳에 있으니, 브라우저는 새 주소로 이동해야 합니다.

일반적인 페이지에 대해 서버가 돌려주는 것은 페이지의 이미지가 아닙니다. 여러분이 직접 손으로 작성하는 것과 같은 태그로 이루어진, HTML로 작성된 순수한 텍스트 문서입니다. 브라우저는 그 텍스트를 받아 눈에 보이는 페이지를 만들어냅니다.

**요청(request)**과 응답 사이클은 HTTP, 즉 HyperText Transfer Protocol에 의해 정의됩니다. 클라이언트와 서버가 메시지를 어떻게 주고받을지에 대해 합의된 규칙 집합입니다. 요청은 메서드(동사: 데이터를 가져오는 GET, 데이터를 보내는 POST), 경로, 그리고 헤더(이름과 값으로 이루어진 메타데이터, 예를 들어 클라이언트가 어떤 형식을 받아들이는지)를 담고 있습니다. 응답은 상태 코드, 자체 헤더, 그리고 대개 실제 콘텐츠인 **본문(body)**을 담고 있습니다.

상태 코드는 범위별로 구분되며, 그 범위를 보면 무엇을 봐야 할지 알 수 있습니다. 2xx는 성공입니다(200 OK). 3xx는 리다이렉션입니다(301 Moved Permanently는 브라우저와 검색 엔진에게 리소스가 새로운 정식 주소로 옮겨갔음을 알리므로, SEO를 위해 정확히 처리할 가치가 있습니다). 4xx는 클라이언트 오류로, 요청 자체가 잘못되었다는 뜻입니다(404 Not Found, 403 Forbidden). 5xx는 서버 오류로, 요청은 정상이었지만 서버 쪽에서 문제가 생겼다는 뜻입니다(500 Internal Server Error). 이 구분은 실용적입니다. 4xx는 요청을 살펴봐야 함을 알려주고, 5xx는 서버를 살펴봐야 함을 알려줍니다. 문서에 대한 응답 본문은 HTML 텍스트이며, Content-Type 헤더가 브라우저에게 이를 일반 텍스트나 이미지가 아니라 HTML로 처리하라고 알려줍니다. 하나 더 알아둘 점: HTTP는 모든 내용을 그대로 읽을 수 있는 형태로 전송하지만, HTTPS는 같은 프로토콜을 암호화로 감싸서 중간에 있는 누구도 내용을 읽거나 조작할 수 없게 만듭니다. 그래서 오늘날 실제 서비스되는 모든 사이트는 HTTPS를 사용합니다.

Juno요청과 응답 사이클 모든 페이지는 왕복입니다: 브라우저가 "이걸 보내주세요"라는 요청을 보내면, 서버는 그 페이지가 담긴 응답을 보냅니다. 사서에게 책을 부탁해서 건네받거나, 없다는 말을 듣는 것과 같습니다. 이 왕복은 여러분이 페이지를 열 때마다, 조용하고 빠르게 일어납니다.
Juno요청과 응답 사이클 요청 하나가 나가고 응답 하나가 돌아오며, 여기에 상태 코드가 붙습니다. 200(성공), 404(그 주소에 아무것도 없음), 301(이동됨, 따라오세요)을 먼저 익히세요. 이 셋이 여러분이 보게 될 대부분의 상황을 다룹니다. 그리고 페이지에 대해 실제로 돌아오는 것은 사진이 아니라 순수한 HTML 텍스트라는 점을 기억하세요. 브라우저가 그것으로 화면을 만들어냅니다.
Juno요청과 응답 사이클 HTTP는 이 여정의 문법입니다: 요청에는 메서드, 경로, 헤더가 있고, 응답에는 상태 코드, 헤더, 본문이 있습니다. 상태 코드는 범위로 읽으세요. 4xx는 요청을 고치라는 뜻이고, 5xx는 서버가 문제를 일으켰다는 뜻이며, 이 구분이 어디를 봐야 할지 알려줍니다. 본문은 Content-Type으로 표시된 HTML 텍스트이고, HTTPS는 암호화로 감싸진 HTTP입니다. 그래서 실제 서비스되는 모든 사이트가 HTTPS를 씁니다.

브라우저는 전달받은 HTML로 무엇을 하는가

응답은 한 페이지 분량의 HTML 텍스트입니다. 브라우저는 그 텍스트를 위에서부터 아래로 읽으면서 화면에 그려냅니다. 제목 태그를 보면 크고 굵은 텍스트로 보여주고, 문단 태그를 보면 본문 줄로 보여주며, 이미지 태그를 보면 그림을 가져옵니다.

레시피를 읽고 요리하는 것과 비슷하다고 생각하면 됩니다. 텍스트 자체는 요리가 아니라 요리에 대한 설명이고, 브라우저는 그 설명을 실제로 보고 사용할 수 있는 무언가로 바꾸는 요리사입니다. 같은 HTML은 항상 같은 페이지를 만들어냅니다. 브라우저가 매번 같은 지시를 그대로 따르기 때문입니다.

브라우저는 HTML 텍스트를 그대로 보여주는 것이 아니라, 그것을 바탕으로 페이지를 만들어냅니다. 문서를 위에서 아래로 읽으면서 태그를 DOM, 즉 Document Object Model로 바꾸는데, 이는 메모리에 유지되는 페이지 요소들의 살아있는 트리 구조입니다. 실제로 화면에 표시되는 것도 이 DOM이고, 이후 CSS와 JavaScript가 읽고 바꾸는 대상도 바로 이 DOM입니다.

여기서 순서가 중요합니다. 브라우저는 여러분이 작성한 순서대로 문서를 읽으므로, 상단에 있는 <script>나 스타일시트는 그 아래에 있는 콘텐츠보다 먼저 처리됩니다. 이것이 페이지에 대한 정보<head>에, 눈에 보이는 콘텐츠를 <body>에 두는 실질적인 이유이며, 태그를 어디에 두는지가 그 효과가 언제 나타나는지에 영향을 주는 이유입니다. 여러분은 단순히 요소를 나열하는 것이 아니라, 브라우저에게 읽는 순서를 지시하고 있는 것입니다.

HTML을 픽셀로 바꾸는 과정은 하나의 파이프라인이며, 그 구조를 알면 왜 어떤 페이지는 다른 페이지보다 느리게 그려지는지 알 수 있습니다. 브라우저는 HTML 텍스트를 파싱(읽어서 구조를 만드는 것)하여 DOM(Document Object Model: 모든 요소를 담은 메모리 상의 트리)으로 만듭니다. CSS도 마찬가지로 파싱하여 병렬 트리를 만들고, 이 둘을 결합한 뒤, 기하학적 배치를 계산합니다(레이아웃: 각 박스가 놓이는 위치). 마지막으로 화면에 픽셀을 페인트합니다.

파서가 위에서 아래로 읽기 때문에, 배치 위치가 타이밍을 바꿉니다. 브라우저가 어떤 리소스의 처리를 끝낼 때까지 콘텐츠를 그리지 않는다면, 그 리소스는 **렌더 블로킹(render blocking)**입니다. <head>에 있는 스타일시트는 스타일이 적용되지 않은 화면이 잠깐 번쩍이지 않도록 하기 위해 의도적으로 렌더 블로킹이지만, 스타일시트가 느리면 첫 페인트 전체가 지연됩니다. <head>에 있는 일반 <script>는 더 나쁩니다. 파서가 멈추고, 스크립트를 내려받아 실행한 뒤에야 다시 진행하기 때문에, 문서 상단에 있는 스크립트는 그 아래에 있는 모든 요소를 지연시킵니다(이 블로킹을 막기 위해 deferasync 속성이 존재합니다). 이것이 문서 순서와 <head> 배치가 무엇이 먼저 그려질지를 결정하는 이유입니다. 블로킹 리소스 위에 있는 모든 것은 그 리소스를 기다립니다.

이는 레이턴시(latency), 즉 바이트가 사용자에게 전달되기까지 걸리는 시간과 연결되는데, 로컬 성능을 아무리 높여도 이 시간은 없어지지 않습니다. 모든 요청에는 왕복 비용이 들기 때문에, 요청 수가 적고 중요한 바이트가 더 빨리 도착할수록 페이지가 더 빨리 그려지기 시작합니다. HTML에서 여러분이 조절할 수 있는 지렛대는 순서입니다. 사용자가 먼저 봐야 할 것을 문서 상단에 두고, 블로킹 리소스를 그 앞에서 치워두면, 의미 있는 첫 페인트가 더 빨리 나타납니다. 이 층위에서는 더 적고 더 빠른 바이트가 성능 게임의 전부입니다.

Juno브라우저는 전달받은 HTML로 무엇을 하는가 돌아온 HTML은 페이지에 대한 설명이고, 브라우저는 그것을 실제로 만들어내는 요리사입니다. 위에서 아래로 읽으면서: 제목 태그는 크고 굵은 텍스트로, 문단 태그는 본문 텍스트로, 이미지 태그는 그림을 가져오는 것으로 처리합니다. 같은 HTML을 넣으면 매번 같은 페이지가 나옵니다.
Juno브라우저는 전달받은 HTML로 무엇을 하는가 브라우저는 HTML을 위에서 아래로 읽으며 DOM을 만드는데, 이 살아있는 요소 트리가 실제로 화면에 표시되는 것입니다. 순서대로 읽기 때문에, 태그를 어디에 두는지가 언제 효과가 나타나는지를 결정합니다. 이것이 head와 body를 나누는 진짜 이유입니다. 여러분은 브라우저에게 단순히 요소 더미를 주는 것이 아니라 읽는 순서를 알려주는 것입니다.
Juno브라우저는 전달받은 HTML로 무엇을 하는가 파싱해서 DOM을 만들고, CSS와 결합하고, 배치하고, 그리는 이 파이프라인 때문에 위치가 타이밍을 바꿉니다. 렌더 블로킹 리소스 위에 있는 모든 것은 그것을 기다리므로, head에 있는 느린 스타일시트나 일반 <script>는 첫 페인트를 지연시킵니다. 그래서 deferasync가 존재하는 것입니다. 여기에 없앨 수 없는 레이턴시까지 더하면, 방법은 항상 같습니다. 더 적고 더 빠른 바이트, 그리고 가장 중요한 것을 문서 상단에 두는 것입니다.