HyunJun 기술 블로그

module과 script & link 본문

JavaScript

module과 script & link

공부 좋아 2023. 8. 9. 09:08
728x90
반응형

JavaScript의 script와 module

자바스크립트에서 많은 양의 코드 작성 시, script를 많이 작성하다 보면 필연적으로 중복되는 코드가 많이 생기게 되고 이 중복 코드를 줄이고 코드의 가독성을 좋게 하기 위해서 우리는 module을 활용해야만 한다.

 

1. script 태그와 link 태그.

디렉터리 구조는 다음과 같다.

module
├─ index.html
└─ public
   ├─ css
   │  └─ index.css
   └─ js
      ├─ index.js
      └─ index2.js

 

index.html은 아래와 같이 간단하게 작성한다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Script & Module</title>

    <!-- text/javascript는 default 값으로 type을 작성하지 않을 시 자동으로 들어간다. -->
    <script type="text/javascript" src="./public/js/index.js"></script>
    <link rel="stylesheet" href="./public/css/index.css" />
  </head>
  <body>
  </body>
</html>

 

chrome의 개발자 도구 -> 네트워크 탭을 활용하면 HTML, CSS, JS 파일 등의 요청과 응답에 따른 타임라인을 확인할 수 있다. 즉, Live Server의 html 파일을 요청하고 응답받아서 해당 html에 해당하는 텍스트(html 코드)를 받아오며, 해당 html 코드를 읽으면서 html 파일 내에 있는 script(js)와 link(css) 조차도 통신을 하여 js, css 코드(텍스트)를 요청 및 응답으로 다운로드 해오는 것을 알 수 있다.

이때 네트워크 탭은, 해당 파일들을 요청 및 응답에 따라 다운로드해오는 시간을 알려주는 탭이지 해당 파일이 실행되는 시간과는 다른 것임을 인지해야 한다. 이 말은 글을 읽다 보면 이해가 갈 것이다.

2. DOMContentLoaded

그렇다면 body에 div를 추가하고

  <body>
    <div></div>
  </body>

 

index.js 파일을 작성해 보자.

const div = document.querySelector("div");
console.log(div);

이때 과연 div를 가지고 올 수 있을까? 정답은 null이 나오며, 이 뜻은 즉, 값이 없다는 뜻이다. (undefined는 초기화가 안됐다는 것이고, null은 값이 없다는 뜻이다.)

 

단순히 생각하기에는 DOM 요소인 div 엘리먼트가 script 태그보다 나중에 있어서 script가 다운로드되고 실행될 때 해당 DOM 요소를 찾지 못하는 것으로 보인다. 이로써 알 수 있는 것은 일반적인 script 태그의 경우 HTML 파싱 중 script 태그를 만나면 해당 script 태그를 평가 및 실행까지 한 번에 다 한다는 것이다.

 

하지만 눈으로 볼 수 있도록, 조금 더 자세히 확인해 보자. 이번에는 개발자 도구 성능 탭으로 와서, 기록을 누르고 페이지를 새로고침 한 후 페이지가 로드가 다 되면 중지를 눌러보자. 이 성능 탭은 단순히 파일의 요청 및 응답 시간인 네트워크와, js 파일의 평가, 실행 등의 시간도 측정할 수 있다.

 

스크립트 평가와 컴파일이 일어났고, querySelector까지 동작한 것을 확인할 수 있다. 하지만 HTML의 파싱이 그 이후에 일어나서 해당 querySelector은 null을 반환한 것이다.

이때 모든 DOM 컨텐츠가 로드된 시점을 DOMContentLoaded라고 한다.

 

1) 타겟 요소의 이후에 script 태그를 넣는다.

이때 예상한 것처럼 간단하게는 아래처럼 해당 타겟 요소의 다음에 script를 삽입하여 해결할 수 있다.

  <body>
    <div></div>
    <script type="text/javascript" src="./public/js/index.js"></script>
  </body>

 

다시 한번 성능 탭에서 디버깅해보면, div가 파싱된 부분은 2252 밀리초이고,

 

querySelector가 동작한 부분은 4102 밀리초로 정상 동작하는 것을 확인할 수 있다.

 

2) DOMContentLoaded 이벤트를 활용한다.

하지만 이러한 상황이 많이 있는 경우, 여러 개의 script 태그가 body 태그 안을 침범한다면 가독성이 좋지 않아질 수 있으며, 코드가 깔끔하지 않다. 또한 전체적인 페이지의 렌더링 속도조차 느려질 수 있다. 해서 스크립트는 그대로 두고,

  <head>
    <script type="text/javascript" src="./public/js/index.js"></script>
  </head>

 

index.js에서 document에 DOMContentLoaded 이벤트 리스너를 걸면, 해당 스크립트가 실행될 때 해당 이벤트 리스너가 백그라운드에 들어가게 되고, DOM 컨텐츠가 로드가 다 되면 브라우저가 DOMContentLoaded를 알려주고 콜백 함수가 실행되어 같은 결과를 낼 수 있다.

document.addEventListener("DOMContentLoaded", () => {
  const div = document.querySelector("div");
  console.log(div);
});

 

위처럼 DOMContentLoaded 이벤트를 활용하게 되면,

확실히 DOMContentLoaded가 되고 해당 콜백 함수를 호출하게 된다.

3) defer

  • script 태그의 속성 중 하나이다.
  • 일반적으로 브라우저는 <script> 태그를 만나면 해당 스크립트를 다운로드하고 실행하며, 이 과정에서 HTML 파싱을 일시 중단한다.
  • 이로 인해 스크립트 다운로드와 실행 시간이 길어지면 페이지 로딩이 느려질 수 있다.
  • 하지만 defer 속성을 사용하면 스크립트의 다운로드는 백그라운드에서 진행되며, HTML 파싱과 별개로 스크립트가 실행되도록 보장한다.
  • defer 속성을 사용한 스크립트들은 DOMContentLoaded 이벤트가 발생하기 바로 직전에 실행된다.

 

즉 defer 하나로 아래와 같이 간단하게 작성할 수도 있다는 것이다.

  <head>
    <script defer type="text/javascript" src="./public/js/index.js"></script>
  </head>
const div = document.querySelector("div");
console.log(div);

 

div(13번째 줄)가 포함된 HTML 파싱은 2242 밀리초

 

스크립트가 동작한 시간은 4098 밀리초, 또한 DOMContentLaded가 동작하기 전에 실행됐다.

 

defer는 src로 파일을 연결할 때에만 동작한다, HTML의 script 태그 내에 코드를 작성하면 defer를 사용해도 적용되지 않는다.

3. Module

1) Module의 필요성

Module을 시작하기 전 모듈이 왜 필요한가에 대해서 정리해 보려고 한다.

 

만약 index.html에서 index.js, index2.js 2개의 script를 불러오고

  <head>
    <script src="./public/js/index.js"></script>
    <script src="./public/js/index2.js"></script>
  </head>

 

각각의 index.js, index2.js에 아래와 같은 코드가 똑같이 있으면 어떻게 될까? 일단 다른 스크립트이므로 에디터 상에서 에러를 뿜진 않는다.

const str = "hello world";

 

하지만 이는, 같은 HTML 파일 내 즉 같은 스코프에서 const str;를 사용하는 것과 마찬가지이므로 아래와 같은 에러가 발생한다.

즉 스크립트 간의 데이터 공유가 된다는 뜻이다. 자세히 따져보면 index.js가 상단에 있으므로 해당 스크립트는 이미 다운로드가 되고 자바스크립트의 실행이 끝나고 const str이 초기화가 돼있는 상태인데, index2.js를 다운로드하고 실행시키면서 에러가 발생하는 것이다. 즉 이는 하나의 HTML 파일 내에서 기본적으로 하나의 자바스크립트 런타임, 하나의 콜 스택을 같이 사용한다는 뜻이다.

 

이를 의도할 수도 있겠지만, 모듈화를 해서 각각의 스코프를 따로 사용하고 싶을 때도 있을 것이다.

2) 아주 옛날 방식

module은 ES6에서 나왔다. 그래서 옛날의 개발자들은 아래와 같은 방식을 썼다.

 

index.js와 index2.js에 똑같은 아래와 같은 코드가 있다.

(function () {
  const str = "hello world";
  console.log(str);
})();

그러면 같은 HTML 내에 선언된 script 파일들 2개에서 const str이라는 변수명이 겹쳐도 즉시 실행 함수로, 스코프를 분리했기에 사용할 수 있다.

 

하지만 이렇게 되면 각 스코프 간의 변수 공유가 힘들었으므로 옛날의 개발자들은 window 객체를 활용했다.

 

index.js

(function () {
  const str = "hello world";
  window.sharedData = "Hi";
})();

index2.js

(function () {
  const str = "hello world";
  console.log(window.sharedData);
})();

하지만 window 객체는 최상위 객체이므로 특정 파일에서만 공유 받을 수 있게 할 수 없었고 모든 파일에서 공유가 되었다. 그래서 웹 팩 및 Module이 나왔다.

 

3) Module 사용법

- export

export는 모든 선언식 앞에 export를 사용하여 해당 모듈을 export 할 수 있다.

 

index.js

export function hello() {
  console.log("Hello world!");
}
export const hi = "Hi world!";

 

- import

module을 사용하는 js 파일은 html에서 선언할 때 type="Module"를 붙여야 동작하며, Module은 defer를 기본값으로 가지고 있다.

    <script src="./public/js/index.js"></script>
    <script src="./public/js/index2.js"></script>

이렇게 되면 index2.js 자체에서 module을 불러오므로 위의 index.js 스크립트를 불러오는 코드는 필요 없게 된다. 즉 아래의 코드만 있으면 된다.

    <script type="Module" src="./public/js/index2.js"></script>

이렇게 Module을 사용하게 되면 index.js와 index2.js는 같은 스코프 내에 있지 않으므로 일단 아래와 같이 index2.js에서 단순 호출이 불가능하다.

hello();

 

아래처럼 import를 해서 사용해야 한다.

import { hello, hi } from "./index.js";

hello();
console.log(hi);

 

 

 

이때 만약 index.js에서 export 한 선언문이 아닌, 아래 코드의 alert처럼 실행문이 들어가 있다면

alert("test");

export function hello() {
  console.log("Hello world!");
}
export const hi = "Hi world!";

 

index2.js에서 import를 할 때 해당 js 파일을 평가 및 실행을 해서 가지고 오므로 해당 실행문이 실행된다.

 

이는 모듈조차도 통신을 통해 가져오기 때문에 해당 코드를 평가하고 실행하기 때문이다.

 

 

4) export default

export default는 import 시 아무 이름으로 받을 수 있다. 또한 "{}"를 사용하지 않아도 된다. 즉 1개만 import 할 수 있다.

 

index.js

const a = "123";
export default a;

 

 

index2.js

import a from "./index.js";

console.log(a);

위의 코드를 보면 "{}"도 사용하지 않았고 1개의 데이터만 export, import 하는 것을 볼 수 있다.

 

이 말인즉슨? 아래처럼 객체를 1개로 내보내어 활용할 수도 있다.

export default {
  hello: () => {
    console.log("Hello world!");
  },
  hi: "Hi world!",
};
import myImport from "./index.js";

myImport.hello();
console.log(myImport.hi);

 

728x90
반응형
Comments