[HTML5] 꼼꼼히 살펴보는 SCRIPT 엘리먼트

프론트엔드 개발자라면 꼭 살펴보세요.

아마도 <script>는 자바스크립트를 공부할 때 가장 먼저 배우는 HTML 엘리먼트일 것이다. 속성이 많지도 않고 심지어 아무런 속성을 입력하지 않아도 유효하고 잘 동작하는 엘리먼트라서 지나치기 쉽다. 하지만 자바스크립트 실행에 있어서는 어쩌면 가장 중요한 엘리먼트이기에 한 번쯤은 제대로 꼼꼼히 살펴보는 게 좋을 것이다.

이 글은 지속적으로 업데이트되는 Living Standard 문서에 기초하고 있으므로 2014년에 발표된 HTML5 권고안에 포함되지 않은 내용도 있다. 이런 경우에는 별도로 관련 표기를 해두었으니 참고하기 바란다.

스크립트 엘리먼트

스크립트 엘리먼트는 <script>로 시작하고 </script>로 끝난다. 다른 HTML 엘리먼트와 마찬가지로 엘리먼트 이름의 대소문자를 구분하지 않고, 고유한 기능이 있는 다양한 속성을 사용할 수 있다. 필수 속성이 없기 때문에 아무런 속성을 설정하지 않고도 사용할 수 있다.

아무런 속성을 사용하지 않을 때는 보통 엘리먼트 내부에 스크립트 컨텐츠를 작성한다.  명세에서는 스크립트 언어가 자바스크립트라고 가정하고 있으며 다른 언어의 지원은 (적어도 현재까지는) 명세와 충돌할 것이라 선언하고 있다.

...  implementing other languages is in conflict with this standard, given the processing model defined for the script element.

스크립트 엘리먼트에 정의된 처리 모델을 고려해 볼 때, (자바스크립트 이외의) 다른 언어의 구현은 이 표준과 충돌할 것이다.

HTML5 명세 중

IE10까지만 해도 현재는 폐지된 language 속성을 사용해서 VBScript를 스크립트 언어로 사용할 수 있었는데 이 방법이 IE11부터는 공식적으로 지원이 중단되었기 때문에 주요 브라우저의 최신 버전을 기준으로 했을 때 자바스크립트 외의 스크립트 언어를 네이티브로 지원하는 브라우저는 없다. 하지만 호환성을 위해 IE11에서도 다음과 같은 헤더를 통해 IE10 모드를 켜두면 VBScript를 사용할 수 있다고 한다.

<meta http-equiv="x-ua-compatible" content="IE=10">

스크립트 컨텐츠

스크립트 콘텐츠에는 유니코드 문자를 모두 사용할 수 있다. 단, 다음 세 가지 항목의 텍스트는 포함할 수 없다(관련 명세).

  • 여는 주석 <!--
  • 닫는 주석 -->
  • 여는 스크립트 태그 <script

따라서 다음과 같은 스크립트는 유효하지 않은 것으로 인식한다.

<script>
var example = '문제 있는 문자열: <!-- <script>';
console.log(example);
</script>

하위 호환성 문제때문에 스크립트 엘리먼트 내부에 있는 <!--<script는 항상 짝이 맞아야 파서가 정확하게 닫는 태그를 인식할 수 있다. 위 예제에서는 <script는 두 번 나타난 반면 </script>는 한 번 밖에 나타나지 않았기 때문에 </script>는 자바스크립트 코드의 일부인 것처럼 취급된다. 물론 올바르지 않은 코드라서 자바스크립트도 실행되지 않는다. 이 문제를 해결하는 가장 쉬운 방법은 <!--<script> 텍스트를 각각 <\!--<\script>로 이스케이프 처리를 하는 것이다.

마찬가지로 다음과 같이 조건 식에 나타나는 것도 주의해야 한다.

// 문제가 발생함
if (x<!--y) { ... }
if ( player<script ) { ... }

// 문제가 발생하지 않음
if (x < !--y) { ... }
if (!--y > x ) { ... }
if (player < script){ ... }
if (script > player){ ... }

역시 하위호환성 때문에 스크립트 컨텐츠에 있는 <!-- 기호는 한 줄 주석 //과 같이 취급된다. 그래서 다음과 같은 올바르지 않은 자바스크립트 코드도 실행할 수 있다(demo).

var a = 10, b = 20;
<!-- a = 30; 실행되지 않는다.
console.log( a + b );

여기서 설명한 스크립트 컨텐츠의 괴상한 특성은 HTML 문서에 삽입된 스크립트 엘리먼트에 텍스트 노드로서 추가한 컨텐츠일때만 동작한다. src 속성을 사용해 외부에서 호출하는 자바스크립트 코드에는 적용되지 않으므로 주의하자.

src 속성

스크립트 파일을 외부에서 불러올 때 사용한다. 이 속성에 유효한 URL을 입력하면 외부의 스크립트를 불러들여서 실행한다. 불러와서 처리하는 방식은 type 속성의 값에 따라 달라질 수 있는데 이 부분은 잠시 뒤에 다루겠다.

src 속성이 설정되어 있을 때 스크립트의 컨텐츠, 다시 말해 <script></script> 사이에 나타나는 텍스트 데이터에는 src가 가리키는 스크립트에 관한 설명만 추가할 수 있다. 입력 가능한 데이터는 공백(탭, 스페이스, 줄바꿈)을 포함하여 자바스크립트의 블럭 주석(/* ... */) 또는 한 줄 주석(//) 뿐이다. 명세 문서에서 예제를 가져오면 다음과 같다.

<script src="cool-effects.js">
 // 새로운 인스턴스 생성:
 //    var e = new Effect();
 // .play를 사용해서 효과를 시작하고 .stop을 사용해서 중단한다.
 //    e.play();
 //    e.stop();
</script>

다만 명세에서 정한 대로 사용하지 않고 주석 외의 문자열, 예를 들어 자바스크립트 코드를 입력한다고 해서 문제가 발생하지는 않는다. 그저 무시될 뿐이다. 웹 브라우저는 src 속성에 유효한 URL이 입력되어 있을 경우 그를 우선시한다.

type 속성

스크립트의 종류를 설정할 수 있다. 사실상 지원하는 언어는 자바스크립트 뿐이라 생략할 수 있고, 이 경우 text/javascript가 사용된다. 그 외에도 application/javascript, application/ecmascript 등 사용할 수 있는 값은 많지만 브라우저에 따라 지원하지 않는 경우도 있으므로 표준인 text/javascript를 사용하는 편이 좋다. 또한 웹 서버에서 스크립트 파일을 보내줄 때는 반드시 text/javascript를 MIME 타입을 사용해야 한다고 정하고 있다.

Servers should use text/javascript for JavaScript resources. Servers should not use other JavaScript MIME types for JavaScript resources, and must not use non-JavaScript MIME types.

자바스크립트가 아닌 다른 타입을 일부러 설정하는 경우도 있다. 이 경우엔 당연하게도 해당 코드를 자바스크립트로 인식하지 않으며 따라서 실행하지도 않고 스크립트 엘리먼트 안의 컨텐츠는 단순한 문자열 데이터 블럭이 된다. 예를 들어 Backbone 프레임워크에서는 다음과 같이 스크립트 엘리먼트에 템플릿을 설정하기도 했다(출처: Sitepoint).

<script type="text/template" id="surfboard-template">
  <td><%= manufacturer %></td>
  <td><%= model %></td>
  <td><%= stock %></td>
</script>

참고로 HTML5에는 이와 같은 용도를 위해 <template> 엘리먼트가 마련되어 있다.

HTML5 명세에는 없지만 Living Standard 문서에 기술되어 있고, 최근 브라우저에서 새롭게 지원하기 시작한 타입으로는 module이 있다(대소문자 구분하지 않음).

type 속성이 module로 설정되면 웹 브라우저는 스크립트를 module script로서 다루게 된다. 이 모드와 구분하기 위해 전통적인 방식으로 다루는 스크립트는 classic script라고 부른다.

모듈 스크립트(module script)가 되면 Node.js 혹은 Babel을 통해서나 가능하던 import 문법을 웹 브라우저에서 네이티브로 사용할 수 있다!  다음 예제를 보자(출처: Jake Archibald의 블로그)

<script type="module">
  import {addTextToBody} from './utils.js';

  addTextToBody('Modules are pretty cool.');
</script>
// utils.js
export function addTextToBody(text) {
  const div = document.createElement('div');
  div.textContent = text;
  document.body.appendChild(div);
}

데모 링크를 클릭하면 실제로 어떻게 동작하는지도 볼 수 있다. ES6의 모듈이 동작하는 방식에 대해서는 스크립트 엘리먼트를 다루는 이 글의 주제를 벗어나므로 다루지 않겠지만, 아직 잘 모르고 있다면 모질라 기술 블로그의 "ES6 In Depth: 모듈(한국어)"을 읽어보기 바란다.

모듈 타입의 스크립트는 다른 모듈(위 예제에서는 utils.js)을 불러올 때 비동기 방식으로 가져오는데, 스크립트를 다운로드 하는 방식에 대해서는 잠시 뒤에 다시 다루도록 하겠다.

module 타입은 Edge, Chrome, Safari, iOS, Safari, Chrome for Android 등 주요 브라우저에서 지원하고 있다. 이 글을 쓰는 시점의 최신 버전인 Firefox 59에서도 이 타입을 지원하기는 하지만 about:config 설정에서 dom.moduleScripts.enabled항목을 설정해주어야 사용할 수 있다.  자세한 내용은 아래 이미지를 참고하고, 최신 상태를 확인하고 싶다면 caniuse.com에서 조회해보도록 하자.

nomodule 속성

type="module"을 지원하지 않는 브라우저에서 사용할 스크립트 파일을 따로 설정하는 불리언 속성이다.

<script type="module" src="main.js"></script>
<script nomodule src="main.fallback.js"></script>

모듈 스크립트를 지원하지 않는 구식 브라우저에서는 type="module"이 설정된 스크립트 엘리먼트의 src 속성을 무시하지만 두 번째 줄에 있는 src 속성은 자바스크립트 코드로서 다루게 될 것이다.

반면 모듈 스크립트를 지원하는 최신 브라우저에서는 type="module"이 설정된 자바스크립트를 모듈로서 다루고 nomodule이 설정된 스크립트 엘리먼트를 무시한다.

async / defer 속성

스크립트를 비동기로 읽어들이고 싶을 때 사용하는 불리언 속성들이다. 기본적으로 스크립트 엘리먼트는 HTML 파서(parser)의 동작을 중단하고 스크립트를 다운로드 후 실행하는 이른바 블러킹 모드(blocking mode)로 동작한다. 여러 스크립트 엘리먼트가 있을 경우에도 실행 순서를 보장할 수 있다는 장점은 있지만, 스크립트 파일을 다운로드하는 동안에 파싱(parsing)이 중단되므로 웹 페이지의 성능을 저하시킨다.

그런데 async 또는 defer 속성을 사용하면 다운로드는 백그라운드에서 진행되고 스크립트를 실행하는 동안만 HTML 파싱을 중단하는 소위 non-blocking 모드로 동작한다. 따라서 웹 페이지는 두 속성을 사용하지 않았을 때보다 더 빠르게 표시될 것이다.

두 속성에는 두 가지 차이가 있다. 하나는 defer는 다운로드가 먼저 끝나더라도 파싱이 완료될 때까지 기다렸다가 스크립트를 실행하지만, async는 다운로드가 끝나면 바로 스크립트를 실행한다는 것이다. 나머지 하나는 defer는 두 속성 중 아무 것도 사용하지 않았을 때와 마찬가지로 실행 순서를 보장해주지만, async는 다운로드 받은 순서대로 실행되기 때문에 실행 순서가 스크립트 엘리먼트의 출현 순서와 다를 수 있다는 것이다.

구식 브라우저에서는 defer만 지원하기도 해서 다음과 같이 두 속성을 모두 적어두기도 한다.

<script defer async src="async.js"></script>

위 예제와 같이 작성하면 async를 지원하는 최신 브라우저에서는 async가 defer에 우선하기 때문에 async의 특성을 따르고 구식 브라우저에서는 defer의 특성을 따른다.

모듈 스크립트에는 async만 사용할 수 있는데, async가 설정되어 있으면 관련 모듈의 다운로드가 끝난 후 바로 스크립트를 실행하지만 설정되어 있지 않으면 마치 defer처럼 웹 페이지 파싱이 끝날 때까지 기다렸다가 실행한다(위 이미지 참조).

crossorigin 속성

현재 문서와 다른 호스트에서 스크립트를 불러올 때 해당 스크립트를 어떻게 다룰 것인지 설정하는 속성이며 사용할 수 있는 값으로는 "anonymous""use-credentials"가 있다.

window.onerror로 에러를 다루는 경우 보안상의 이유때문에 출처가 같지 않은 곳에서 불러오는 스크립트에서 발생하는 에러는 그다지 도움이 되지 않는 정보만 전달된다. 다음의 예제를 통해 차이를 확인해보자.

두 예제 모두 에러가 발생하는 파일을 불러오고,  다음과 같은 코드를 사용해 window.error를 통해 에러를 #log 엘리먼트에 출력하도록 했다.

<script>
  window.onerror = function(message, url, lineNum, columnNum, error) {
    var log = document.getElementById('log');
    log.innerText = '' +
      'Error: ' + message + 
      '\nURL: ' + url +
      '\nline: ' + lineNum +
      '\ncolumn: ' + columnNum;
}
</script>
<pre id="log"></pre>

crossorigin 속성을 사용하지 않은 첫 번째 예제는 다음과 같이 화면에 나타난다. 보다시피 에러 내용도 명확하지 않고 에러가 발생한 위치도 알 수 없다.

crossorigin 속성을 사용하지 않은 경우

반면,  crossorogin 속성을 사용해서 스크립트를 불러온 두 번째 예제에서는 다음과 같이 에러 메시지 내용이 보다 세밀하게 나타난다.

crossorigin="anonymous" 속성을 사용한 경우

앞서 말했듯이 이 같은 동작은 잠재적인 보안 문제때문이다. 악성 웹 사이트에서 onerror를 사용해 다른 사이트에 로그인이 되었는지 확인할 수 있었기 때문이다. 예를 들어 아무 사이트에서나 Gmail 스크립트를 불러오면 다음과 같이 사용자가 Gmail에 로그인했는지 여부를 알 수 있었다(출처: Jeremiah Grossman 블로그).

따라서 이 동작은 window.onerror에 전달하는 정보에만 적용될 뿐 개발자 도구에는 적용되지 않는다. crossorigin 속성의 사용 여부와 상관없이 개발자 도구에서는 상세한 에러 정보가 표시된다.

모듈 모드에서는 crossorigin 속성의 값이 조금 다른 의미를 지닌다.  모듈 모드는 출처가 다른 스크립트를 항상 CORS 방식으로 불러온다. 즉, 다른 호스트에서 스크립트 파일을 불러오고 있는데 스크립트 파일을 보내주는 서버에서 CORS 헤더를 설정하지 않으면 스크립트가 실행되지 않는다.

기본적으로 일반 모드에서 스크립트를 불러올 때는 출처와 상관없이 쿠키 등의 인증 정보(credentials)가 서버에 전달되는데, 모듈 모드에서는 출처가 같지 않은 스크립트 파일을 불러올 때 쿠키 등의 민감한 정보를 보내지 않는다. 출처가 다른 경우에도 crossorigin 속성에 명시적으로 "use-credentials"를 설정하면 인증 정보를 서버로 전송한다. 코드로 표현해보면 다음과 같다(출처: Jake Archibald의 블로그).

<!-- 인증 정보(쿠키 등) 보냄 --> 
<script src="1.js"></script>

<!-- 인증 정보(쿠키 등) 보내지 않음 - 동일 출처 --> 
<script type="module" src="1.js?"></script>

<!-- 인증 정보(쿠키 등) 보냄 - 동일 출처 --> 
<script type="module" crossorigin src="1.js"></script>

<!-- 인증 정보(쿠키 등) 보내지 않음 - 동일 출처 아님 --> 
<script type="module" crossorigin src="http://다른_호스트/1.js"></script>

<!-- 인증 정보(쿠키 등) 보냄 - 동일 출처 아님 --> 
<script type="module" crossorigin="use-credentials" src="http://다른_호스트/1.js"></script>

주의할 점은 동일 출처가 아닌 상태에서 "use-credentials"를 사용할 때는 서버의 응답 헤더에 Access-Control-Allow-Credentials: true가 포함되어 있어야 한다는 것이다. 위 코드의 데모는 여기서 확인해 볼 수 있다.

사라진 속성

몇 해 전까지만 해도 스크립트 엘리먼트에 charset을 사용해서 스크립트 외부 파일의 문자셋을 정해주곤 했었다. 지금과는 달리 euc-kr로 인코딩된 웹 페이지가 많았는데 스크립트 파일은 utf-8로 작성된 경우 문자가 잘못 표기되는 것을 방지하기 위해서였다. 이 속성은 HTML5에서 더 이상 지원되지 않기 때문에 사용해서는 안되는 속성이지만, 사용하는 경우에도 utf-8만 지원한다(대소문자 구분 안함).

여담이지만 아직도 euc-kr, cp949, ksc5601(모두 사실상 같은 코드)과 같은 인코딩을 사용하는 웹 페이지가 있기는 하다(네이버 카페라거나 Naver Cafe 라거나...).

language 속성도 사용하면 안되는 속성이지만 사용할 경우에는 반드시 값이 "JavaScript"여야 한다(대소문자 구분안함).

  1. 주석 비교문에서 1띠용, 모듈 타입에서 2띠용 받았습니다.
    좋은글 공유해주셔서 감사합니다!

댓글을 남겨주세요

This site uses Akismet to reduce spam. Learn how your comment data is processed.