웹, 특히 프론트엔드 환경에서 html은 뗄래야 뗄 수 없는 존재입니다.
react / vue 등을 사용했을 때도 html을 항상 같이 사용하고 있으니 더더욱 간과하기 쉬운 부분이라는 생각이 듭니다.
저 또한 그랬던 것 같습니다.
이번에 html을 공부하면서 마음 먹은 게 있습니다.
'알고 있다고 생각하지 말기', '재미없으면 더 깊게 보기'
나름대로 의식은 했는데 그럼에도 찝찝한 부분이 남아있긴 합니다.
그래서 내용 정리도 할 겸 html에서 놓치기 쉬운 부분들에 대해 최대한 쉽게 흘러가듯 작성해보겠습니다.
HTML5 템플릿 예제
// IDE에 html:5를 입력하면 나오는 자동완성 템플릿
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
...
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
우선 첫 줄부터 보겠습니다.
1. DTD (Document Type Definition) 선언부
<!DOCTYPE html>
<!doctype html>
브라우저는 html 파일을 받으면 이 선언부를 통해 document의 형식을 판별합니다. (대소문자 상관없이 사용 가능)
현재 예시는 웹표준인 html5 문서임을 나타내고 있습니다.
과거 브라우저들은 점유율 경쟁에서 이기기 위해 웹 표준 개발 기구 W3C (world wide web consortium)의 명세를 무시한 채 서로 뽐내듯이 각자만의 기능을 넣었습니다.
현대에는 이를 '브라우저 전쟁'이라 부릅니다.
브라우저마다 html 요소를 렌더링하는 방식 등이 상이했기때문에, 개발자는 눈물 흘리며 브라우저마다 핸들링하고, 당연히 개발자 머리가 많이 아팠을 겁니다.
html5는 결국 궁극적으로 웹 접근성, 사용자 경험 증대를 위해 탄생했다고 생각하면 좀 더 편할 것 같습니다.
브라우저마다 html을 처리하는 방식이 달라 발생하는 불편은 유저의 몫일테니까요.
웹 표준 개발 기구 W3C는 HTML4.01 표준 이후 html에 xml문법이 적용된 XHTML1.0을 표준으로 발표했습니다.
하지만 XHTML만으로는 빠른 속도로 성장하는 웹 생태계를 포용하는 데에 한계가 있었습니다.
제한된 환경에서 추가적인 기능을 제공하기 위해 active-X등과 같은 플러그인들이 우후죽순 생겨나기 시작했고, PC가 아닌 환경에서는 플러그인을 사용하지 못하는 등의 웹 접근성 문제 또한 생겨나게 됩니다.
이러한 XHTML의 구조적인 문제에 지친 애플, 모질라, 오페라는 WHATWG (Web Hypertext Application Technology Working Group)를 설립하고, HTML4.0의 후속 버전인 HTML5를 출시하였습니다.
이 당시 웹 표준이던 XHTML은 개발자들에게 외면 받으며, 개발자들은 보다 유연한 문법과 멀티미디어 기능, 하위 버전 호환성까지 갖춘 HTML5를 사용하기 시작했습니다.
사용하는 사람이 없으니 사실상 표준이 무의미하겠죠.
그래서 XHTML2.0을 개발하던 W3C는 개발을 멈추고, 2014년 HTML5를 표준으로 채택했습니다.
이와 같이 웹 문서에도 여러 형식과 버전이 존재하고 있기 때문에, 브라우저는 html에서 DTD 선언부를 통해 문서의 형식과 버전을 판별하여 그에 맞는 렌더링 모드를 설정합니다.
DTD 선언부가 존재하지 않는다면 브라우저는 비표준 렌더링 모드인 quirks mode로 인식하기 때문에, 개발자가 의도한대로 렌더링되지 않고 또 브라우저마다 각기 다른 형식으로 렌더링 될 수 있습니다.
2. html Tag
<html lang='en'>
...
</html>
html태그는 최상위 요소인 root를 뜻하며, 모든 요소는 html 하위에 존재해야 합니다.
(DTD 선언부의 html 어트리뷰트는 root 엘리먼트를 뜻합니다. 그래서 'html'이 설정되어 있습니다)
html 태그에서 하나 더 눈여겨 볼 것은 lang 어트리뷰트입니다.
- 스크린리더 접근성 향상
- 크롬 언어 인터페이스 최적화
스크린리더는 lang 속성을 통해 언어를 설정하기 때문에, lang이 올바르지 않다면 한국어 컨텐츠가 영어 발음으로 읽히는 등의 문제가 발생할 수 있습니다.
또한 크롬에서 제공하는 사이트 번역 인터페이스가 올바르지 않게 작동될 수 있습니다. (한국어 컨텐츠인데 '한국어로 번역' 버튼이 노출되는)
그렇기 때문에 lang 어트리뷰트를 통해 문서의 주 언어를 올바르게 설정해야 합니다.
3. head Tag
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<meta name="description" content="...">
<meta name="keywords" content="...">
<link href="/media/examples/link-element-example.css" rel="stylesheet" />
...
</head>
head 태그에는 렌더링되지 않는 요소들, 메타 데이터가 포함되어 있습니다.
메타 데이터는 데이터를 설명하기 위한 데이터입니다.
쉽게 말하자면, html은 데이터이고 이 html문서를 설명하기 위한 정보 데이터들을 메타 데이터라 합니다.
문서의 제목, 설명, 인코딩 방식, 디바이스에서의 viewport 정의, 웹 어플리케이션 정보, 키워드 등을 정의할 수 있고 검색엔진 봇이 문서의 메타 데이터를 참고하여 사이트에 대한 정보를 이해하고 인덱싱하기 때문에 SEO 최적화에 중요한 요소입니다.
더 많은 메타 태그 속성이 궁금하다면 (메타 태그 속성 정리)
예시를 다시 보면 약간의 의문이 들 수 있습니다.
외부 리소스를 불러오는 link태그가 head 내부에 존재합니다. 아 이것도 메타 데이터로 봐야하나..? 생각할 수 있을 것 같습니다.
4. link Tag
link태그는 head내부에서 사용하길 권장하고 있습니다.
link태그는 주로 아이콘, 파비콘, 폰트, 스타일시트 등 외부 리소스를 불러오는 데에 사용되는 태그입니다.
link 태그에 대해 좀 더 자세히 알아보겠습니다.
브라우저 렌더링 방식과 함께 보기
브라우저는 렌더링을 위해 html문서를 한 줄 한 줄 순서대로 파싱합니다.
이 과정에서 html 요소를 만나면 DOM 트리를 만들어 나가고, link태그를 통해 CSS 파일이나 style 태그를 만나게 되면 CSS를 파싱하며 CSSOM트리를 구축합니다.
CSS : style sheet / 렌더 블로킹 요소 (Render-blocking) / 스크립트 블로킹 요소 (script-blocking)
CSS 파싱이 진행될 때 script 태그와는 달리 DOM의 파싱이 멈추지는 않습니다.
Dom 파싱을 멈추는 요소는 Parser-blocking이라 합니다. Parser-blocking 요소로는 script 태그가 존재합니다.
CSS의 경우 임베디드 스타일이나 인라인 스타일에서 일부 Parser-blocking 요소로 작동할 수 있다고하지만, 일반적으로 Render-blocking 요소로서 렌더 트리 구축만을 멈추게 합니다. (렌더 트리 구축 후 레이아웃 -> 페인트 -> 렌더링 진행)
또한, CSS는 script-blocking 요소로서 CSSOM구축이 완료되기 전이라면 다운로드한 스크립트에 대한 파싱을 블로킹합니다.
JS로 DOM 트리 요소의 스타일을 수집한 후에 CSSOM이 업데이트 되어 DOM 트리가 변경된다면 JS는 잘못된 스타일을 가질 수 있기 때문입니다.
link rel 어트리뷰트 : preload / prefetch / preconnect
link 태그는 rel 어트리뷰트를 통해 파서에게 리소스의 우선 순위를 알려줄 수 있습니다.
웹 최적화를 위해 preload / prefetch / preconnect를 사용할 수 있으며, 아래에서 간단하게 설명해보겠습니다.
preload
<link rel="preload" as="font" crossorigin="crossorigin" type="font/woff2" href="myfont.woff2">
preload는 폰트 등과 같이 필수로 불러와야 하는 리소스에 대해 사용하는 것을 추천합니다.
브라우저는 해당 리소스의 priority를 highest로 처리하며 리소스를 받아옵니다.
prefetch
<link rel="prefetch" href="page-2.html">
prefetch는 미래에 사용될 것이라고 생각되는 리소스를 미리 요청, 캐싱하여 사용할 수 있습니다.
예로 들자면 다음 페이지에 대한 문서를 요청할 수 있습니다.
다만 해당 파일만을 받아오는 것이지, 파일이 참조하고 있는 외부 리소스들을 받아오지는 않습니다.
이미 클라이언트에 캐싱된 데이터가 존재하더라도 prefetch를 통해 받아오게 되기때문에 prefetch를 적용할 때에는 효용 가치가 있는지 파악한 후에 사용하는 게 좋습니다.
preconnect
<link rel="preconnect" href="https://어쩌고저쩌고.cloudfront.net">
preconnect는 현재 도메인에서 외부 도메인의 리소스를 참고할 것이라 브라우저에게 알려주는 것입니다.
브라우저는 DNS lookup을 통해 도메인의 호스트 정보를 받아오고, TCP왕복을 진행한 뒤 https 프로토콜을 위한 SSL/TLS 핸드셰이크 과정을 거치게됩니다.
preconnect를 사용할 경우 DNS, TCP/IP등과 같은 외부 도메인에 대한 연결을 미리 진행할 수 있습니다.
HTTP/1.1 이상부터는 TCP에 대한 연결을 유지하고 있기때문에 preconnect를 잘 사용한다면 최적화에 큰 도움을 줄 수 있습니다.
클라이언트에서 사용 예를 들자면, 도메인과 연결된 CDN링크가 될 수 있을 것 같습니다.
4. script Tag
<body>
...
<script type="module" src="/src/index.js"></script>
</body>
js파일을 불러올 수 있는 script 태그입니다.
script태그는 body태그 하단에 위치해야 한다는 이야기를 들어보셨을 것 같습니다.
크게 한 가지의 이유가 존재한다고 생각합니다.
- Parse-Blocking 요소
브라우저 런타임 환경에서 작동하는 자바스크립트에선 DOM API를 통해 자유롭게 DOM에 접근할 수 있습니다.
브라우저 동작원리를 한 번 더 살펴보겠습니다.
브라우저는 html파일을 상단에서부터 한 줄 한 줄 파싱하며 DOM트리를 생성합니다.
이 과정에서 html 파서가 script 태그를 만날 경우, 엔진의 제어권은 렌더링 엔진에서 자바스크립트 엔진으로 옮겨가게 됩니다.
script는 Parse-blocking 요소로서 script 태그를 만나게 되면 html의 '파싱'을 멈추고, 자바스크립트 파일에 대한 다운로드를 진행한 뒤, 다운로드가 완료되면 파싱을 진행합니다.
자바스크립트 엔진은 추상 구문 트리(abstract syntax tree, AST)를 생성한 후 자바스크립트를 실행한 뒤, html 파싱이 중단되었던 지점으로 돌아와 다시 html 파싱을 진행합니다.
그렇기 때문에 자바스크립트가 실행되는 시점에 js내부에 아직 DOM트리에 존재하지 않는 요소에 대한 로직이 존재한다면, 해당 DOM요소를 감지하지 못할 수 있습니다.
살펴보니, html을 파싱하다 말고 script 파일의 다운로드를 기다린다는 게 비효율적이라는 생각이 들 것입니다.
HTML5 : async / defer 어트리뷰트의 등장
HTML5에서 script 태그의 특성이 가진 문제를 해결하기 위해 어트리뷰트로 async, defer가 추가되었습니다.
두 어트리뷰트가 해결하고자 하는 공통 관심사는 스크립트 파일 처리에 대한 최적화입니다.
async
<script async type="text/javascript" src="/src/index.js" />
<script>
document.getElementById("test").innerHTML = "Hello test!";
</script>
--> import없이 script 파일 작성 시, async와 동일하게 작동
async의 경우 html 파싱 과정에서 미리 리소스를 다운로드합니다. (html 파싱과 스크립트 파일 다운로드를 병렬적으로)
다운로드가 완료되는 시점에 html 파싱을 멈추고 js파일에 대한 파싱을 진행합니다.
미리 js파일을 다운로드하고, 다운로드가 완료되는 시점에 자바스크립트 엔진이 분석을 시작하는 것이기때문에 js파일에 대한 실행 시점은 명확히 할 수 없습니다.
그렇기때문에 광고 스크립트나, DOM 의존성이 낮은 스크립트에 적용할 수 있습니다.
defer
<script defer type="text/javascript" src="/src/index.js" />
<script type="module" src="/src/index.js" />
-> type이 'module'인 경우, defer와 동일하게 작동
defer의 경우 async와 동일하게 script파일에 대한 다운로드를 html 파싱과 병렬로 처리합니다.
다만 자바스크립트 파일의 실행 시점에 차이가 있습니다.
또 한 번 브라우저 동작 원리와 함께 설명해보겠습니다.
DOMContentLoaded
브라우저는 html 파싱을 진행하며 DOM트리를 구축하고, CSSOM 트리를 구축한 뒤 두 트리를 결합하여 렌더 트리를 생성합니다. (레이아웃 이후는 이번에 설명하지 않겠습니다.)
html문서의 생명주기와 관련된 이벤트 중 DOMContentLoaded가 존재합니다.
DOMContentLoaded는 브라우저가 HTML을 전부 읽고, DOM트리를 완성하는 즉시 (CSSOM 트리는 완성되어 있는 상태) 실행됩니다.
위 렌더링 과정 그림과 함께 보면 Render Tree로 결합되기 직전에 실행되는 이벤트라고 볼 수 있습니다.
defer 속성을 갖고 있는 script 태그의 파일의 경우 DOMContentLoaded가 실행되기 직전에 js파일을 실행합니다.
고로 DOM 의존성이 존재하는 스크립트 파일에 사용할 수 있습니다.
이번 포스팅에선 HTML 태그의 역할과 놓칠 수 있는 부분들에 대해 설명하는 글을 생각했었는데, 작성하다 보니 양이 좀 많아진 것 같습니다.
HTML자체가 웹 문서를 보여주기 위해 탄생 했지만, 이후 웹 생태계가 넓어지며 어플리케이션 형태로 진화하다 보니 html에서도 브라우저 위에서 효율적으로 작동하기 위한 여러 방법들이 고안되었습니다. 그래서 브라우저 동작 원리를 빼놓고 설명하지 않을 수가 없었네요.
결국 모든 웹은 브라우저 위에서 동작하기 때문에, 웹 생태계를 이해하려면 브라우저의 동작 원리를 아는 것이 중요하다고 생각합니다.
이번엔 태그 설명에 초점이 맞춰져 있었기때문에 설명하지 않고 넘어간 부분들이 있어 추후 브라우저 동작 원리에 대한 포스팅을 따로 진행해보겠습니다.
감사합니다.
'Code > 그래서' 카테고리의 다른 글
[React - 그래서] key prop은 렌더링에 어떻게 쓰이는가 (Fiber / 재조정 reconciliation) (1) | 2023.12.17 |
---|