웹 개발을 시작하는 초보자들이 자주 부딪히는 문제 중 하나는 “왜 내 JavaScript 코드가 제대로 작동하지 않을까?”라는 의문입니다. 이 문제의 핵심은 대부분 HTML 파싱과 JavaScript 실행 순서에 대한 이해 부족에서 비롯됩니다. 이 글에서는 웹 페이지가 로드될 때 실제로 어떤 일이 일어나는지, 그리고 초보 웹 개발자가 알아야 할 중요한 개념들을 살펴보겠습니다.
DOM이란 무엇인가?
DOM(Document Object Model)은 HTML 문서의 구조를 트리 형태로 표현한 것으로, 웹 페이지의 모든 요소를 객체로 다룰 수 있게 해줍니다. 브라우저가 HTML 문서를 로드하면, 파서(Parser)는 HTML을 파싱하여 DOM 트리를 구성합니다.
중요한 점은 DOM이 리액트(React)와 같은 프레임워크에만 있는 개념이 아니라는 것입니다. DOM은 모든 웹 브라우저의 기본 기능이며, HTML5를 포함한 모든 웹 기술의 근간입니다.
JavaScript 실행 순서와 <script>
태그 위치
HTML 문서에서 <script>
태그의 위치는 JavaScript 코드의 실행 시점에 직접적인 영향을 미칩니다. 초보 개발자들이 자주 마주치는 문제 중 하나는 다음과 같은 코드입니다:
<!DOCTYPE html>
<html>
<head>
<script>
// 아직 DOM이 로드되지 않은 상태
const button = document.getElementById('myButton'); // null 반환
button.addEventListener('click', function() {
alert('버튼이 클릭되었습니다!');
}); // TypeError: Cannot read property 'addEventListener' of null
</script>
</head>
<body>
<button id="myButton">클릭하세요</button>
</body>
</html>
이 코드는 에러를 발생시킵니다. 왜냐하면 JavaScript가 실행될 때 <button>
요소가 아직 DOM에 존재하지 않기 때문입니다. 브라우저는 HTML을 위에서 아래로 순차적으로 파싱하며 실행하기 때문입니다.
문제 해결 방법: 3가지 접근법
1. <script>
태그 위치 변경
가장 간단한 해결책은 <script>
태그를 </body>
닫는 태그 바로 앞에 배치하는 것입니다:
<!DOCTYPE html>
<html>
<head>
<!-- 여기에 CSS와 메타데이터 -->
</head>
<body>
<button id="myButton">클릭하세요</button>
<script>
// 이 시점에서는 DOM이 모두 로드된 상태
const button = document.getElementById('myButton'); // 정상 작동
button.addEventListener('click', function() {
alert('버튼이 클릭되었습니다!');
});
</script>
</body>
</html>
2. DOMContentLoaded 이벤트 리스너 사용
<script>
태그를 <head>
에 유지하고 싶다면, DOMContentLoaded
이벤트를 사용하여 DOM이 완전히 로드된 후 코드를 실행할 수 있습니다:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM이 완전히 로드된 후 실행
const button = document.getElementById('myButton'); // 정상 작동
button.addEventListener('click', function() {
alert('버튼이 클릭되었습니다!');
});
});
</script>
</head>
<body>
<button id="myButton">클릭하세요</button>
</body>
</html>
3. defer 또는 async 속성 사용
외부 JavaScript 파일을 사용할 때는 defer
또는 async
속성을 사용할 수 있습니다:
<!DOCTYPE html>
<html>
<head>
<!-- defer: HTML 파싱이 끝난 후 순서대로 실행 -->
<script src="script.js" defer></script>
<!-- async: HTML 파싱과 병렬로 로드, 로드 완료 즉시 실행 (순서 보장 없음) -->
<script src="analytics.js" async></script>
</head>
<body>
<button id="myButton">클릭하세요</button>
</body>
</html>
defer
: HTML 파싱이 완료된 후, 스크립트가 선언된 순서대로 실행됩니다.async
: HTML 파싱과 병렬로 스크립트를 로드하고, 로드가 완료되면 즉시 실행합니다 (실행 순서 보장 없음).
DOM 이벤트와 생명주기 이해하기
웹 페이지의 로딩 과정에서 발생하는 주요 이벤트들은 다음과 같습니다:
- DOMContentLoaded: HTML이 완전히 로드되고 파싱되었을 때 발생하며, 외부 리소스(이미지, 스타일시트 등)는 기다리지 않습니다.
- load: 페이지의 모든 리소스(이미지, 스타일시트, 스크립트 등)가 로드되었을 때 발생합니다.
// DOM만 로드되었을 때
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM이 로드되었습니다!');
});
// 페이지의 모든 리소스가 로드되었을 때
window.addEventListener('load', function() {
console.log('페이지가 완전히 로드되었습니다!');
});
초보 웹 개발자가 알아야 할 추가 팁
1. 브라우저 콘솔 활용하기
JavaScript 디버깅의 첫 단계는 브라우저의 개발자 도구를 여는 것입니다. F12 키를 누르거나 마우스 우클릭 후 “검사”를 선택하면 콘솔을 볼 수 있습니다. 여기서 에러 메시지를 확인하고 console.log()
를 통해 변수 값을 출력할 수 있습니다.
2. 이벤트 버블링과 캡처링 이해하기
DOM에서 이벤트는 기본적으로 버블링(bubbling)됩니다. 즉, 하위 요소에서 발생한 이벤트는 상위 요소로 전파됩니다. 이를 이해하면 이벤트 위임(Event Delegation)과 같은 고급 기법을 활용할 수 있습니다.
// 부모 요소에 이벤트 리스너를 한 번만 추가하여 여러 자식 요소를 처리
document.getElementById('parent').addEventListener('click', function(e) {
if (e.target.className === 'child') {
console.log('자식 요소가 클릭되었습니다!');
}
});
3. 비동기 프로그래밍 기초 익히기
현대 웹 개발에서는 비동기 프로그래밍이 필수적입니다. Promises, async/await 문법을 이해하면 API 호출이나 사용자 상호작용을 더 효과적으로 처리할 수 있습니다.
// Promise 사용 예제
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('에러 발생:', error));
// async/await 사용 예제
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('에러 발생:', error);
}
}
4. 반응형 디자인 기본 원칙 알기
모바일 기기 사용이 증가함에 따라, 반응형 웹 디자인의 중요성도 커지고 있습니다. 미디어 쿼리를 사용한 기본적인 반응형 레이아웃을 구현할 줄 알아야 합니다.
/* 기본 스타일 (모바일) */
.container {
width: 100%;
}
/* 태블릿 */
@media (min-width: 768px) {
.container {
width: 750px;
}
}
/* 데스크탑 */
@media (min-width: 1024px) {
.container {
width: 970px;
}
}
5. 웹 성능 최적화 기본 이해하기
사용자 경험을 향상시키기 위해 웹 성능 최적화의 기본을 알아두면 좋습니다. 이미지 최적화, 불필요한 리소스 최소화, 브라우저 캐싱 활용 등이 여기에 포함됩니다.
<!-- 이미지 지연 로딩 예제 -->
<img src="placeholder.jpg" data-src="actual-image.jpg" class="lazy-load">
<script>
// 간단한 지연 로딩 구현
document.addEventListener('DOMContentLoaded', function() {
const lazyImages = document.querySelectorAll('.lazy-load');
const lazyLoad = function() {
lazyImages.forEach(img => {
if (isInViewport(img)) {
img.src = img.dataset.src;
}
});
};
// 스크롤 시 이미지 로드
window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);
// 초기 로드
lazyLoad();
});
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
}
</script>
결론
웹 개발을 시작하는 과정에서 HTML 파싱과 JavaScript 실행 순서를 이해하는 것은 매우 중요합니다. DOM이 어떻게 구성되고, JavaScript가 언제 실행되는지 알면 많은 일반적인 버그를 예방할 수 있습니다.
이 글에서 다룬 <script>
태그 위치 조정, DOMContentLoaded
이벤트 사용, defer
/async
속성 활용은 웹 개발자로서 꼭 알아야 할 기본 지식입니다. 이러한 기초를 탄탄히 다진 후, 더 복잡한 프레임워크와 라이브러리로 발전해 나가는 것이 좋습니다.
계속 학습하고, 실험하고, 코드를 작성하며 경험을 쌓아가세요. 웹 개발의 여정은 끝이 없지만, 그만큼 보람차고 흥미로운 여정이 될 것입니다!