웹 페이지에서 JavaScript를 실행하려면 HTML에서 JavaScript 파일을 불러와야 합니다.
가장 기본적인 방법은 <script> 태그를 사용하는 것입니다.
<script src="./app.js"></script>
그런데 <script> 태그에도 defer, async, type="module" 등의 옵션이 존재하며, 각각 JavaScript가 다운로드되고 실행되는 시점에 차이가 있습니다.
이번 글에서는 HTML에서 JavaScript를 불러오는 대표적인 방법과 각각의 차이점을 알아보겠습니다.
1. 기본적인 JavaScript 불러오기
가장 기본적인 방법은 <script> 태그의 src 속성을 사용하는 것입니다.
<script src="./app.js"></script>
src에는 실행할 JavaScript 파일의 경로를 지정합니다.
예를 들어 다음과 같은 파일 구조가 있다고 가정해보겠습니다.
project/
├── index.html
└── app.js
index.html에서 app.js를 불러오려면 다음과 같이 작성할 수 있습니다.
<!DOCTYPE html>
<html>
<head>
<title>JavaScript</title>
</head>
<body>
<h1>Hello JavaScript</h1>
<script src="./app.js"></script>
</body>
</html>
// app.js
console.log("JavaScript 실행");
이렇게 하면 HTML이 로드되면서 app.js가 실행됩니다.
2. <script>의 위치에 따른 차이
script 태그는 어디에 작성하느냐에 따라서도 동작 방식이 달라집니다.
<head>에 작성하는 경우
<head>
<script src="./app.js"></script>
</head>
브라우저가 HTML을 위에서부터 읽다가 <script> 태그를 만나면 JavaScript 파일을 다운로드하고 실행합니다.
즉, JavaScript가 실행되는 동안 HTML 파싱이 일시적으로 중단됩니다.
HTML 파싱
↓
<script> 발견
↓
JS 다운로드
↓
JS 실행
↓
HTML 파싱 계속
JavaScript 파일의 크기가 크거나 네트워크가 느리다면 HTML 렌더링이 늦어질 수 있습니다.
3. <body> 마지막에 작성하는 방법
이러한 문제를 피하기 위해 예전부터 많이 사용했던 방법이 <body> 태그 가장 아래에 JavaScript를 작성하는 것입니다.
<body>
<h1>Hello</h1>
<button>Click</button>
<script src="./app.js"></script>
</body>
이 경우 브라우저가 대부분의 HTML을 먼저 파싱한 다음 JavaScript를 다운로드하고 실행합니다.
따라서 JavaScript에서 DOM 요소를 찾을 때도 비교적 안전합니다.
const button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("클릭!");
});
다만 현대적인 웹 개발에서는 단순히 <body> 마지막에 배치하는 것보다 defer를 사용하는 방법도 많이 사용합니다.
4. defer를 사용하는 방법
<script src="./app.js" defer></script>
defer의 가장 중요한 특징은 JavaScript 파일을 다운로드하는 동안 HTML 파싱을 계속한다는 것입니다.
HTML 파싱 ────────────────────────┐
↓
JS 다운로드 ────────────────┐ │
↓ │
JS 실행 ←─┘
즉, JavaScript 때문에 HTML 파싱이 중단되지 않습니다.
그리고 HTML 파싱이 완료된 후 JavaScript가 실행됩니다.
따라서 DOM을 조작하는 JavaScript를 작성할 때 유용합니다.
<head>
<script src="./app.js" defer></script>
</head>
const button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("클릭!");
});
HTML 파싱이 완료된 이후 JavaScript가 실행되기 때문에 button을 찾을 수 있습니다.
여러 개의 defer
defer를 여러 개 사용하는 경우에는 작성된 순서대로 실행됩니다.
<script src="./a.js" defer></script>
<script src="./b.js" defer></script>
두 파일은 다운로드는 동시에 진행될 수 있지만 실행은 다음 순서를 따릅니다.
a.js 실행
↓
b.js 실행
따라서 서로 의존하는 JavaScript 파일을 사용할 때도 유용합니다.
5. async를 사용하는 방법
async도 JavaScript를 다운로드하는 동안 HTML 파싱을 계속합니다.
<script src="./app.js" async></script>
하지만 defer와 중요한 차이가 있습니다.
async는 JavaScript 다운로드가 끝나는 즉시 실행됩니다.
HTML 파싱 ────────────┐──────────────
│
JS 다운로드 ──────────┘
↓
JS 실행
JavaScript 다운로드가 완료되면 HTML 파싱을 잠시 중단하고 JavaScript를 실행합니다.
따라서 HTML 파싱이 끝날 때까지 기다리는 defer와 다릅니다.
6. defer와 async의 차이
둘의 차이를 간단하게 비교하면 다음과 같습니다.
defer
<script src="a.js" defer></script>
<script src="b.js" defer></script>
HTML 파싱 ─────────────────── 완료
JS 다운로드 ────────────────┐
↓
a.js 실행
↓
b.js 실행
HTML 파싱을 방해하지 않고, HTML 파싱이 끝난 후 순서대로 실행합니다.
async
<script src="a.js" async></script>
<script src="b.js" async></script>
HTML 파싱 ───────────────────────
↓ ↓
a.js 다운로드 b.js 다운로드
↓
다운로드 완료 → 즉시 실행
다운로드가 끝나는 즉시 실행하기 때문에 a.js가 먼저 작성되어 있다고 해서 반드시 먼저 실행되는 것은 아닙니다.
예를 들어 b.js가 더 빨리 다운로드되면 b.js가 먼저 실행될 수 있습니다.
따라서 서로 의존성이 없는 독립적인 JavaScript 파일에 적합합니다.
7. type="module" 사용하기
JavaScript에서는 import와 export를 이용하여 파일을 모듈 단위로 나눌 수 있습니다.
이때 HTML에서는 다음과 같이 작성합니다.
<script type="module" src="./main.js"></script>
예를 들어 다음과 같은 구조를 만들 수 있습니다.
project/
├── index.html
├── main.js
└── math.js
math.js에서 함수를 export합니다.
export function add(a, b) {
return a + b;
}
그리고 main.js에서 import합니다.
import { add } from "./math.js";
console.log(add(10, 20));
HTML에서는 진입점 역할을 하는 main.js만 불러옵니다.
<script type="module" src="./main.js"></script>
이 방식은 JavaScript 파일이 많아졌을 때 코드를 기능별로 나누고 관리하기 좋습니다.
8. type="module"의 특징
type="module"을 사용하면 일반적인 <script>와 몇 가지 차이가 있습니다.
대표적으로 다음과 같습니다.
import, export 사용 가능
import { add } from "./math.js";
export function add(a, b) {
return a + b;
}
모듈마다 독립적인 스코프를 가짐
일반적인 전역 변수처럼 모든 JavaScript 파일에서 자유롭게 접근하는 구조를 피할 수 있습니다.
기본적으로 defer처럼 동작
모듈 스크립트는 HTML 파싱을 방해하지 않고 다운로드하며, HTML 파싱이 완료된 후 실행됩니다.
따라서 다음처럼 작성할 수 있습니다.
<head>
<script type="module" src="./main.js"></script>
</head>
9. 한눈에 비교하기
방식HTML 파싱JS 실행 시점실행 순서특징
| 기본 <script> | 중단 | 다운로드 후 즉시 | 작성 순서 | 가장 기본적인 방식 |
| defer | 계속 | HTML 파싱 완료 후 | 작성 순서 | 일반적인 외부 JS에 적합 |
| async | 계속 | 다운로드 완료 즉시 | 보장되지 않음 | 독립적인 스크립트에 적합 |
| type="module" | 계속 | HTML 파싱 완료 후 | 모듈 의존성에 따름 | import/export 사용 |
10. 그렇다면 어떤 방법을 사용하는 것이 좋을까?
일반적인 웹 페이지에서 외부 JavaScript 파일을 하나 연결한다면 다음과 같이 defer를 사용하는 것을 고려할 수 있습니다.
<script src="./app.js" defer></script>
JavaScript를 여러 파일로 나누어 import/export를 사용한다면 모듈 방식을 사용할 수 있습니다.
<script type="module" src="./main.js"></script>
반면 async는 다른 JavaScript 파일과 실행 순서에 의존하지 않는 독립적인 스크립트에 적합합니다.
정리
HTML에서 JavaScript를 불러오는 방법은 단순히 <script> 태그를 사용하는 것에서 끝나지 않습니다.
특히 중요한 것은 JavaScript를 언제 다운로드하고 언제 실행하느냐입니다.
핵심만 정리하면 다음과 같습니다.
기본 script
→ JS를 만나면 HTML 파싱을 중단하고 실행
defer
→ JS를 다운로드하면서 HTML 파싱
→ HTML 파싱 완료 후 실행
async
→ JS를 다운로드하면서 HTML 파싱
→ JS 다운로드가 끝나면 즉시 실행
type="module"
→ import/export 사용 가능
→ 기본적으로 defer와 유사하게 동작
따라서 현대적인 JavaScript 개발에서는 defer와 type="module"을 중심으로 이해하고, async는 필요한 상황에 사용하는 것이 좋습니다.
'개발' 카테고리의 다른 글
| Promise, async, await는 어떻게 이어질까: Chromium으로 이해하는 비동기 실행 (0) | 2026.09.20 |
|---|---|
| ‘Discord로 로그인’을 누르면 무슨 일이 일어날까? (0) | 2026.09.06 |
| LLDB을 이용한 C++ 디버깅 원리 (1) | 2026.08.30 |
| 부동소수점은 왜 필요할까? (0) | 2026.08.23 |
| vscode에서 파이썬 프로그램을 디버깅 하는 방법 (0) | 2026.08.16 |