Script tag 위치에 따른 차이점 feat. Rendering Engine

kich555·2021년 8월 21일
0

I don't Know JavaScript

목록 보기
2/9
post-thumbnail

script tag는 HTML <head>에 넣을수도, <body>에 넣을수도 있다. script tag 위치에 따른 차이점을 살펴보며, script tag를 어떻게 사용해야 할 지 살펴보자.

브라우저

HTML 문서 내에서 script tag 위치에 따른 차이점에 대해 이해하려면, 먼저 브라우저의 작동 방식에 대해 간략히 이해하여야 한다.

브라우저의 주요 기능

브라우저의 주요 기능은 사용자가 선택한 리소스를 서버에 요청하고 브라우저에 표시하는 것이다.
리소스는 주로 HTML 문서이며 리소스의 주소는 URI 즉 우리가 흔히 아는 URL (ex:http://naver.com)에 의해 정해진다.)

URI : 통합 자원 식별자(Uniform Resource Identifier) : URI는 인터넷의 우편물 주소 같은 것으로, 정보 리소스를 고유하게 식별하고 위치를 지정할 수 있다.

URL : 통합 자원 지시자(uniform resource locator) : URI 의 가장 흔한 형태로써 특정 서버의 한 리소스에 대한 구체적인 위치를 서술한다.

출처: [마이구미의 HelloWorld]

브라우저는 사용자 인터페이스, 통신, 브라우저 엔진, 렌더링 엔진, js 엔진, 웹 데이터 베이스 등 몇가지 요소로 구성되어 있는데,

이 글 주제와 관련하여 우리가 주의깊게 보아야 할 요소는 바로
렌더링 엔진이다.

렌더링 엔진

렌더링 엔진의 역할은 요청받은 내용을 브라우저 화면에 표시하는 일이다. 이 과정을 렌더링이라고 하며 렌더링을 진행하는 프로그램을 렌더링 엔진이라 부른다.

붉은색 박스 부분 구현에 대한 역할은 렌더링 엔진이 담당한다.

렌더링 엔진은 통신으로부터 요청한 문서의 내용을 얻는 것으로 시작하는데 엔진의 구동 과정에 대해 정말 간략히 설명하자면 아래의 그림과 같다.

렌더링 엔진 기본 구동 과정

출처: [NAVER D2]

  1. 서버로부터 받은 HTML 문서를 파싱하여 DOM 트리를 구축

  2. 구축된 DOM트리를 바탕으로 렌더 트리를 구축 (DOM 트리가 구축되는 동안 브라우저는 렌더 트리를 구축함 = 병렬실행)
    (간단하게 말해서 DOM트리를 바탕으로 '화면에 보여지는' 부분에 대한 트리를 구축하는 과정이다. ex) head 요소와 같은 비시각적 DOM 요소는 렌더 트리에 추가되지 않는다.)

  3. 렌더 트리 배치 좌표값
    (구축된 렌더 트리를 바탕으로 각 요소를 화면 특정 위치에 배치)

  4. 렌더 트리 그리기 -> 우리가 보는 화면이 출력됨

HTML 파싱

HTML 파싱 과정은 렌더링 엔진의 메인 쓰레드에서 진행되는데 여기서 주제와 관련된 문제가 발생한다.

💡 HTML 파서는 파싱 중 script tag를 만나면, 파싱을 일시 중지한 다음, JavaScript Code를 로딩하고 파싱해 실행하기를 기대한다. 왜 그럴까?

이는 바로 JavaScript는 DOM 구조 전체를 바꿀 수 있는 document.write() 메서드와 같은 것을 사용해 문서의 모양을 변경할 수 있기 때문이다.
(참고로 브라우저가 js코드를 실행하는 과정은 zero부터 시작하는 'var'를 쓰지 않는 이유 1편 을 참고하도록 하자)

아래 몇가지 예시를 보자.

💡 <head><script>를 넣었을 경우

<!DOCTYPE html>
<html lang="en">
    <head>
        <script src="JS.js"></script> <!-- head 요소에 <script>요소를 넣었을 경우 --> 
        <link rel="stylesheet" href="style.css" />
        <title>Peaches</title>
    </head>
    <body></body>
</html>

  • <head><script>요소를 작성할 경우, HTML 파싱이 완료되기 전 JS 파일을 로드 및 실행하게된다. 이는 만약 JS 코드가 DOM 요소에 높은 의존성을 띈다면, 문제를 야기할 수 있다.

  • 만약 JS파일이 굉장히 크다면, 로드 및 실행에 시간이 걸려 좋은 사용자 환경을 제공하기 힘들어진다.

💡 </body> 뒤에 <script>를 넣었을 경우

<!DOCTYPE html>
<html lang="en">
    <head>     
        <link rel="stylesheet" href="style.css" />
        <title>PaintJS</title>
    </head>
    <body>
      <div></div>
        <script src="JS.js"></script> <!-- </body> 뒤에 <script>요소를 넣었을 경우 --> 
    </body>
</html>

  • HTML 파싱이 모두 끝난 뒤 <script> 다운로드 및 실행이 시작된다. HTML과 CSS 모두 파싱이 끝났기 때문에 JS가 실행되지 않았어도 사용자는 제공된 렌더 트리 배치 및 그리기에 의한 브라우저 화면을 볼 수 있다.

  • BUT 만약 해당 HTML문서가 JS파일에 높은 의존성을 띈다면, 위와 마찬가지로 좋은 사용자 환경을 제공하기 힘든건 마찬가지다.

💡 <script>에 defer 속성을 사용할 경우

<!DOCTYPE html>
<html lang="en">
    <head>
        <script defer src="JS.js"></script> <!-- <script>에 defer 속성값을 사용할 경우 --> 
        <link rel="stylesheet" href="style.css" />
        <title>PaintJS</title>
    </head>
    <body>
      <div></div>
    </body>
</html>

  • defer 속성을 사용할 경우 지연 스크립트 라고도 한다. 지연 스크립트는 DOM이 준비된 후에 실행되긴 하지만 DOMContentLoaded 이벤트 발생 전에 실행된다.

  • </body> 뒤에 <script>를 넣은 경우와 한가지 다른점이 있는데, 바로 JS파일 다운로드는 병렬구조로 백그라운드에서 진행된다는 것이다.

  • 전체 JS파일을 로드하고 실행시키는데에는 가장 괜찮은 방법이다. BUT JS파일에 document.write() 메서드와 같이 DOM 구조를 변경시키는 메소드가 존재한다면 딱히 의미없는 방식이기도 하다. (어차피 JS파일이 실행됨에따라 DOM 트리가 변경되므로 렌더링 엔진은 DOM 트리 구축과정부터 다시 진행하기 때문)

💡 여러 <script>에 defer 속성을 사용할 경우

<!DOCTYPE html>
<html lang="en">
    <head>
        <script defer src="JS1.js"></script> <!-- 여러 <script>에 defer 속성값을 사용할 경우 --> 
        <script defer src="JS2.js"></script> <!-- 여러 <script>에 defer 속성값을 사용할 경우 --> 
        <script defer src="JS3.js"></script> <!-- 여러 <script>에 defer 속성값을 사용할 경우 --> 
        <link rel="stylesheet" href="style.css" />
        <title>PaintJS</title>
    </head>
    <body>
      <div></div>
    </body>
</html>

  • 지연 스크립트는 일반 스크립트와 마찬가지로 HTML에 추가된 순(상대순, 요소순)으로 실행된다.
    따라서 길이가 긴 스크립트가 앞에, 길이가 짧은 스크립트가 뒤에 있어도 짧은 스크립트는 긴 스크립트가 실행된 후 실행된다.

-> 만약 JS파일들이 순서에 따른 의존성을 띄고있다면 defer 속성을 사용하는 것이 좋다.

💡 <script>에 async 속성을 사용할 경우

<!DOCTYPE html>
<html lang="en">
    <head>
        <script async src="JS1.js"></script> <!-- <script>에 async 속성값을 사용할 경우 --> 
        <link rel="stylesheet" href="style.css" />
        <title>PaintJS</title>
    </head>
    <body>
      <div></div>    
    </body>
</html>
  • async 속성을 사용할 경우 비동기 스크립트 라고도 한다. defer 와 마찬가지로 백그라운드에서 다운로드 되지만, defer 즉 연기하진 않는다. (다운로드가 완료되면 바로 실행된다.) -> 당연히 실행 중 HTML파서는 멈춘다.

  • DOMContentLoaded 이벤트와 async 스크립트는 서로를 기다리지 않는다.-> 페이지 구성이 끝난 후에 async 스크립트 다운로딩이 끝난 경우, DOMContentLoadedasync 스크립트 실행 전에 발생할 수 있다.

  • 비동기 스크립트는 방문자 수 카운터, 광고 관련 스크립트와 같이 각각 독립적인 역할을 하는 서드 파티 스크립트에 주로 사용한다. (독립적이기 때문)

💡 여러 <script>에 async 속성을 사용할 경우

<!DOCTYPE html>
<html lang="en">
    <head>
        <script async src="JS1.js"></script> <!-- <script>에 async 속성값을 사용할 경우 --> 
        <script async src="JS2.js"></script> <!-- <script>에 async 속성값을 사용할 경우 --> 
        <script async src="JS3.js"></script> <!-- <script>에 async 속성값을 사용할 경우 --> 
        <link rel="stylesheet" href="style.css" />
        <title>PaintJS</title>
    </head>
    <body>
      <div></div>    
    </body>
</html>

  • async 속성은 매우 독립적이기 때문에 HTML에 추가된 순**(상대순, 요소순)으로 실행되지 않는다. -> 먼저 다운로드 된 js파일 순서대로 실행된다. -> 만약 js파일들이 순서에 의존성을 가진다면 async속성을 사용하면 안된다.

script 위치의 따른 차이점에 대한 부분은 나중에 기술면접의 질문으로 나오기도 한다고 한다.
하지만 면접을 위해 위 주제를 외우고 한다기보단, 이 기회에 웹 브라우저의 동작방식에 대해 이해해 볼 수 있도록 하자.

해결 못한 궁금점

https://d2.naver.com/helloworld/59361 에 따르면 HTML 파싱은 일반적인 하향식 또는 상향식 파서로 파싱이 되지 않는다고 한다. -> 왜 </body> 뒤에 <script>를 사용하는지 명확히 이해 못함

HTML 파싱이 완료되고, JS파일 실행 전에 DOM 트리가 구축될때, DOM 트리가 구축된 후 JS파일이 DOM 요소를 추가 혹은 변경하는 메소드를 가지고 있다면, DOM트리는 완전히 재구축 되는것인가? 아니면 변경 부분만 재구축되나?

HTML 파싱 방식을 간략하게나마 이해했을때, 변경된 부분만 재구축 될 것이라고 추측한다.
(아직 파싱, DOM 트리 구축, 렌더로 이어지는 부분에 이해가 더 필요하다.) <- 도움!! ㅜㅜ

위와 같은 연장선상인데, 파싱이 완료되어야 DOM트리가 구축되는건가? 파싱/DOM구축/렌더 트리 구축/ 랜더링이 비동기로 이루어지는 건지 동기로 이루어지는건지...?

참조 : 브라우저는 어떻게 동작하는가? naverD2
참조 : 최신 브라우저의 내부 살펴보기 3 - 렌더러 프로세스의 내부 동작 naverD2
참조 : 자바스크립트 실행 최적화
참조 : defer, async 스크립트
참조 : 외부 JavaScript 불러오기 (async, defer)
by 쓴웃음

참조 : 웹 페이지의로드 및 실행 순서 By IT-GUNDAN.COM <- 이거 나중에 다시 읽어보자

profile
const isInChallenge = true; const hasStrongWill = true; (() => { while (isInChallenge) { if(hasStrongWill) {return 'Success' } })();

1개의 댓글

comment-user-thumbnail
2022년 9월 16일

안녕하세요. 글 잘 읽었습니다.
body 태그 하위에 script 파일을 넣어도, script가 여전히 렌더링 자체를 블락킹 하는 현상을 발견했는데, 이는 어떤 이유로 납득해야할지 궁금합니다!
아래코드를 실행하면 5초뒤에 웹페이지 로딩이 발생합니다.

<head>
 <style>
 .box{
   background-color:red;
   height:150px;    
   width:150px;
 }
 </style>
</head>
<body>
  <div class="box"></div> 

  <script>
  var start = new Date();
  while(true) { 
   var now = new Date(); 
   if (now-start > 10000) 
     break; 
  }
  console.log('main thread finished');
  </script>
</body>
답글 달기