
브라우저가 script 태그를 만날때는 어떻게 동작할까? 🤔
표준 스크립트, 비동기 로딩을 하는 defer, async 스크립트에는 어떤 특징이 있는지 알아보자!

일반적으로 브라우저는 HTML 을 순서대로 파싱하면서 script 태그를 만나면, HTML 의 파싱을 중단하고 해당 스크립트를 가져오는 작업으로 전환한다. 이후 스크립트를 다 불러오면 HTML을 다시 파싱한다. 스크립트 내의 javascript 코드를 통해 DOM 트리에 접근해서 돔을 수정할 수 있는데 이 때 발생하는 에러를 방지하기 위해서이다.
즉, 표준 스크립트는 다음과 같은 특징이 있다.
장점
단점
위와 같이 표준 스크립트는 HTML 파싱 , script를 하나씩만 실행해야 해서 발생하는 문제점이 있다. 이는 스크립트를 비동기 속성으로 만드는 async, defer 로 보완이 가능하다.

스크립트에 async 속성이 존재하면, HTML 파싱중에도 스크립트를 불러올 수 있게 된다. 스크립트를 다 불러와서 사용 가능해지는 즉시, HTML 파싱을 중단하고 스크립트를 실행을 한다. (HTML 파싱과 스크립트 실행은 같이 할 수 없다! ) 실행이 완료되면 HTML 파싱을 다시 시작한다.
(중요한 스크립트일때 유용할 듯..??)
async과 유사하게 브라우저가 HTML 파싱 도중 스크립트를 만나도 파싱과 스크립트를 fetch를 병렬적으로 수행한다. async와의 차이점은, defer는 스크립트를 HTML 모두 파싱한 이후에 실행한다는 것 이다. 또한, defer는 여러개의 스크립트를 순서대로 실행한다는 특징이 있는데, onDOMContentLoaded 이벤트가 발생하기 전에 스크립트를 문서상의 순서대로 실행한다.
브라우저가 HTML 을 전부 읽고 파싱하여 DOM 트리를 완성하는 즉시 발생하는 이벤트이다. DOM 트리를 만든 즉시 이 onDOMContentLoaded 이벤트가 트리거 된다. 이 이벤트는 Document 객체에서 발생하면 DOM 노드에 이벤트 핸들러를 추가하는 작업 등을 할 수 있다. defer를 사용하면 script 가 DOM 을 변경하더라도, 이 변경이 다 끝난 뒤에 onDOMContentLoaded 이벤트를 실행하는 것을 (순서를) 보장할 수 있다. 왜냐하면 defer는 onDOMContentLoaded 가 실행되기 바로 전에 스크립트를 실행하기 때문이다!
async는 fetch 이후에 바로 스크립트 실행, defer는 fetch 이후에는 HTML 파싱으로 돌아가고, HTML 파싱이 다 끝난 이후에 스크립트를 실행한다!
스크립트를 모듈로 설정하고 싶은 경우 다음과 같이 속성을 추가하여 브라우저가 모듈임을 인식할 수 있게 해주어야 한다.
<script type="module">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('rin');
</script>
모듈은 일반 스크립트와 어떤 다른 특징이 있을까?
: 모듈은 항상 엄격 모드(use strict) 로 실행된다. 이로 인해 안전하게 코드를 쓸 수 있다. 또한 모듈은 자신만의 레벨 스코프를 갖기 때문에, 해당 파일에서만 변수와 함수명을 사용할 수 있다. 외부 파일에서 사용하고 싶다면 import / export 를 해야 한다.
: 모듈의 경우 같은 파일을 여러 번 불러와도 한번만 평가된다. 한 모듈의 내용은 어느 외부 파일에서 불러오든 하나의 모듈이다. 외부 파일에서 그 모듈을 변경한다면, 다른 외부 파일에서도 그 변경 사항을 확인할 수 있다.
: 모듈을 만든다는 것은 관심사에 따라 여러 개의 파일로 분리한다는 것이다. 모듈끼리는 독립적인 스코프 등을 갖기 때문에 이를 잘 활용한다면 의존성 관리가 용이하다. 코드를 재사용하고 유지보수가 쉬워지는 건 덤이다!
: type=module 은 비동기 로딩을 구현하는 한 방식이다. 원래는 HTML 문서를 해석하는 동안 스크립트 파일을 만나면, HTML 해석을 멈추고 스크립트 파일을 다운로드 하는 동기적 방식입니다. 하지만 비동기적 방식은 HTML 파싱과 스크립트 다운로드를 동시에 함으로써, 페이지의 초기 로딩 시간을 단축하고 UX를 좋게 만든다. 다른 비동기 방식으로는 async, defer가 있다.
음음 그렇군 위는 모듈의 특성이고, 그렇다면 브라우저 내에서 스크립트를 모듈로 실행하면 어떤 특징이 있을까?
type=”module”type=”module” 은 defer를 내재하고 있다. 즉, 스크립트의 다운로드는 HTML의 파싱과 함께 병렬적으로 실행되지만, 실행은 HTML 의 파싱이 끝난 이후에 실행된다는 것이다.
<script type="module">
alert(typeof button)
</script>
//defer는 무조건 HTML 파싱 후, 즉 스크립트 문서의 실행이 가장 하단으로 내려간다고 보면 된다.
//따라서 위 alert는 정상적으로 실행된다. 돔 요소를 정상적으로 불러올 것이다.
<script>
alert(typeof button)
</script>
//일반 스크립트의 경우 HTML 파싱을 멈추고 다운로드, 실행을 하기 떄문에 위 스크립트가
//실행되는 순간에는 button 요소가 돔 트리에 없는 상태이다.
//따라서 undefined 의 창이 뜰 것이다.
<button id="button">Button</button>
인라인 스크립트에서는 defer가 의미가 없다. 아무리 defer를 걸어도 즉시 로딩 후 실행된다.
하지만 type=module 로 모듈 스크립트로 바꿔준다면, 자동으로 defer 선언이 되어진다. 인라인 스크립트 더라도 defer가 내포된다. 즉, 스크립트가 인라인이든 외부 스크립트든 상관없이 실행은 HTML 파싱 이후에 실행된다.
인라인이든 외부 스크립트이든 defer를 쓰고 싶다면 type=module 을 써줘도 된다. (이렇게 쓰는게 더 낫다!)
type=”module” 은 모듈의 장점과 defer의 장점을 통합한 문법이다. (이는 문법적 설탕이라고도 표현한다)
async는 원래 외부 스크립트에서만 사용 가능한 기능이다. defer와 마찬가지로 인라인 스크립트에서는 유효하지 않다.
하지만, type=”module”로 선언시에는 인라인 스크립트에도 async를 적용할 수 있다! (type=”module” 은 사기 문법…)
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
위는 인라인 스크립트를 사용한 예시이다. ./analytics.js 를 불러오는 작업이 끝나면, HTML 파싱 같은 다른 작업은 중단되고 해당 모듈이 바로 실행된다.
이런 특징은 광고나 이벤트 리스너, 카운터 같이 어디에도 종속되지 않는 기능을 사용할 때 유용하다.
🔗참고링크
https://gist.github.com/seia-soto/6a1fb7b0b5ae04bf4471763265ab7786
https://www.moonkorea.dev/Javascript-%EB%AA%A8%EB%93%88
https://yeoulcoding.me/269