본문 바로가기
개발

HTML에서 JavaScript를 불러오는 방법과 차이점

by 박호정94 2026. 9. 13.

웹 페이지에서 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는 필요한 상황에 사용하는 것이 좋습니다.