14 KiB
테라리움 프로젝트 Part 3: DOM 조작과 클로저
스케치노트: Tomomi Imura
강의 전 퀴즈
소개
DOM, 즉 "문서 객체 모델(Document Object Model)"을 조작하는 것은 웹 개발의 핵심 요소입니다. MDN에 따르면, "문서 객체 모델(DOM)은 웹 문서의 구조와 내용을 구성하는 객체들의 데이터 표현입니다." DOM 조작의 어려움은 종종 JavaScript 프레임워크를 사용하여 DOM을 관리하게 된 주요 이유였지만, 이번에는 순수 JavaScript로 직접 관리해 보겠습니다!
또한, 이번 강의에서는 JavaScript 클로저의 개념을 소개합니다. 클로저는 다른 함수에 의해 감싸진 함수로, 내부 함수가 외부 함수의 스코프에 접근할 수 있도록 합니다.
JavaScript 클로저는 방대하고 복잡한 주제입니다. 이번 강의에서는 테라리움 코드에서 클로저의 기본 개념만 다룹니다. 클로저는 내부 함수와 외부 함수가 구성되어 내부 함수가 외부 함수의 스코프에 접근할 수 있도록 합니다. 더 자세한 내용은 광범위한 문서를 참고하세요.
우리는 클로저를 사용하여 DOM을 조작할 것입니다.
DOM을 웹 페이지 문서를 조작할 수 있는 모든 방법을 나타내는 트리로 생각해 보세요. 다양한 API(응용 프로그램 인터페이스)가 작성되어 프로그래머가 선택한 프로그래밍 언어를 사용해 DOM에 접근하고, 편집, 변경, 재배열 및 관리할 수 있습니다.
DOM과 이를 참조하는 HTML 마크업의 표현. 출처: Olfa Nasraoui
이번 강의에서는 JavaScript를 작성하여 사용자가 페이지의 식물을 조작할 수 있도록 하는 인터랙티브 테라리움 프로젝트를 완성할 것입니다.
사전 준비
테라리움의 HTML과 CSS가 이미 작성되어 있어야 합니다. 이번 강의가 끝날 때쯤, 사용자는 식물을 드래그하여 테라리움 안팎으로 이동할 수 있게 될 것입니다.
작업
테라리움 폴더에 script.js
라는 새 파일을 만드세요. 이 파일을 <head>
섹션에 가져옵니다:
<script src="./script.js" defer></script>
참고: 외부 JavaScript 파일을 HTML 파일에 가져올 때
defer
를 사용하면 HTML 파일이 완전히 로드된 후에 JavaScript가 실행되도록 할 수 있습니다.async
속성을 사용할 수도 있지만, 우리의 경우 드래그 스크립트를 실행하기 전에 HTML 요소가 드래그 가능 상태로 완전히 준비되어야 하므로defer
를 사용하는 것이 중요합니다.
DOM 요소
먼저, DOM에서 조작하려는 요소에 대한 참조를 생성해야 합니다. 우리의 경우, 현재 사이드바에 대기 중인 14개의 식물입니다.
작업
dragElement(document.getElementById('plant1'));
dragElement(document.getElementById('plant2'));
dragElement(document.getElementById('plant3'));
dragElement(document.getElementById('plant4'));
dragElement(document.getElementById('plant5'));
dragElement(document.getElementById('plant6'));
dragElement(document.getElementById('plant7'));
dragElement(document.getElementById('plant8'));
dragElement(document.getElementById('plant9'));
dragElement(document.getElementById('plant10'));
dragElement(document.getElementById('plant11'));
dragElement(document.getElementById('plant12'));
dragElement(document.getElementById('plant13'));
dragElement(document.getElementById('plant14'));
여기서 무슨 일이 일어나고 있을까요? 문서를 참조하여 DOM을 통해 특정 Id를 가진 요소를 찾고 있습니다. HTML 첫 번째 강의에서 각 식물 이미지에 개별 Id를 부여했던 것을 기억하시나요? (id="plant1"
) 이제 그 작업이 유용하게 쓰입니다. 각 요소를 식별한 후, 이를 곧 작성할 dragElement
라는 함수에 전달합니다. 이렇게 하면 HTML의 요소가 드래그 가능 상태가 되거나 곧 그렇게 될 것입니다.
✅ 왜 요소를 Id로 참조할까요? CSS 클래스가 아닌 이유는 무엇일까요? 이전 CSS 강의를 참고하여 이 질문에 답해 보세요.
클로저
이제 dragElement
클로저를 생성할 준비가 되었습니다. 클로저는 내부 함수 하나 이상을 포함하는 외부 함수입니다.
클로저는 하나 이상의 함수가 외부 함수의 스코프에 접근해야 할 때 유용합니다. 예를 들어:
function displayCandy(){
let candy = ['jellybeans'];
function addCandy(candyType) {
candy.push(candyType)
}
addCandy('gumdrops');
}
displayCandy();
console.log(candy)
이 예제에서 displayCandy
함수는 이미 함수 내에 존재하는 배열에 새로운 사탕 유형을 추가하는 함수를 감쌉니다. 이 코드를 실행하면 candy
배열은 정의되지 않은 상태가 됩니다. 이는 배열이 클로저에 국한된 로컬 변수이기 때문입니다.
✅ candy
배열을 접근 가능하게 만들려면 어떻게 해야 할까요? 배열을 클로저 외부로 이동해 보세요. 이렇게 하면 배열이 전역 변수가 되어 클로저의 로컬 스코프에만 국한되지 않게 됩니다.
작업
script.js
의 요소 선언 아래에 다음과 같은 함수를 만드세요:
function dragElement(terrariumElement) {
//set 4 positions for positioning on the screen
let pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
terrariumElement.onpointerdown = pointerDrag;
}
dragElement
는 스크립트 상단의 선언에서 terrariumElement
객체를 가져옵니다. 그런 다음, 함수에 전달된 객체에 대해 0
으로 설정된 몇 가지 로컬 위치를 설정합니다. 이러한 로컬 변수는 드래그 앤 드롭 기능을 각 요소에 추가하면서 조작됩니다. 테라리움은 이러한 드래그된 요소들로 채워질 것이므로, 애플리케이션은 요소들이 배치된 위치를 추적해야 합니다.
또한, 이 함수에 전달된 terrariumElement
는 pointerdown
이벤트를 할당받습니다. 이는 DOM 관리를 돕기 위해 설계된 웹 API의 일부입니다. onpointerdown
은 버튼이 눌리거나, 우리의 경우 드래그 가능한 요소가 터치될 때 실행됩니다. 이 이벤트 핸들러는 몇 가지 예외를 제외하고 웹 및 모바일 브라우저에서 작동합니다.
✅ 이벤트 핸들러 onclick
은 브라우저 간 지원이 훨씬 더 광범위합니다. 그런데 왜 여기서 사용하지 않을까요? 여기서 만들고자 하는 화면 상호작용의 정확한 유형에 대해 생각해 보세요.
Pointerdrag 함수
terrariumElement
는 드래그할 준비가 되었습니다. onpointerdown
이벤트가 실행되면 pointerDrag
함수가 호출됩니다. 이 줄 바로 아래에 해당 함수를 추가하세요: terrariumElement.onpointerdown = pointerDrag;
작업
function pointerDrag(e) {
e.preventDefault();
console.log(e);
pos3 = e.clientX;
pos4 = e.clientY;
}
여기서 여러 가지 일이 일어납니다. 먼저, e.preventDefault();
를 사용하여 pointerdown 시 기본적으로 발생하는 이벤트를 방지합니다. 이렇게 하면 인터페이스의 동작을 더 잘 제어할 수 있습니다.
스크립트 파일을 완전히 작성한 후 이 줄을 제거하고 실행해 보세요. 어떤 일이 발생하나요?
둘째, 브라우저 창에서 index.html
을 열고 인터페이스를 검사합니다. 식물을 클릭하면 'e' 이벤트가 캡처되는 것을 볼 수 있습니다. 이벤트를 자세히 살펴보면 pointerdown 이벤트 하나로 얼마나 많은 정보가 수집되는지 확인할 수 있습니다!
다음으로, 로컬 변수 pos3
와 pos4
가 e.clientX로 설정되는 것을 확인하세요. 검사 창에서 e
값을 찾을 수 있습니다. 이 값들은 클릭하거나 터치할 때 식물의 x와 y 좌표를 캡처합니다. 식물을 클릭하고 드래그할 때의 동작을 세밀하게 제어하려면 이러한 좌표를 추적해야 합니다.
✅ 왜 이 전체 앱이 하나의 큰 클로저로 작성되었는지 점점 더 명확해지고 있나요? 그렇지 않다면, 14개의 드래그 가능한 식물 각각의 스코프를 어떻게 유지할 수 있을까요?
초기 함수를 완성하려면 pos4 = e.clientY
아래에 두 개의 pointer 이벤트 조작을 추가하세요:
document.onpointermove = elementDrag;
document.onpointerup = stopElementDrag;
이제 포인터를 이동할 때 식물이 포인터와 함께 드래그되도록 하고, 식물을 선택 해제할 때 드래그 동작이 멈추도록 지정합니다. onpointermove
와 onpointerup
은 모두 onpointerdown
과 동일한 API의 일부입니다. 아직 elementDrag
와 stopElementDrag
함수가 정의되지 않았으므로, 이를 다음에 작성하세요.
elementDrag 및 stopElementDrag 함수
이제 두 개의 내부 함수를 추가하여 식물을 드래그하거나 드래그를 멈출 때 발생하는 동작을 처리함으로써 클로저를 완성할 것입니다. 원하는 동작은 언제든지 식물을 드래그하여 화면 어디에나 배치할 수 있는 것입니다. 이 인터페이스는 특정 드롭존이 없는 등 매우 자유롭게 설계되어 사용자가 식물을 추가, 제거 및 재배치하여 테라리움을 원하는 대로 디자인할 수 있습니다.
작업
pointerDrag
의 닫는 중괄호 바로 아래에 elementDrag
함수를 추가하세요:
function elementDrag(e) {
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
console.log(pos1, pos2, pos3, pos4);
terrariumElement.style.top = terrariumElement.offsetTop - pos2 + 'px';
terrariumElement.style.left = terrariumElement.offsetLeft - pos1 + 'px';
}
이 함수에서는 처음에 설정한 위치 1-4를 많이 수정합니다. 여기서 무슨 일이 일어나고 있을까요?
드래그하는 동안, pos1
을 pos3
(이전에 e.clientX
로 설정됨)에서 현재 e.clientX
값을 뺀 값으로 재할당합니다. pos2
도 유사한 작업을 수행합니다. 그런 다음, pos3
과 pos4
를 요소의 새로운 X와 Y 좌표로 재설정합니다. 드래그하면서 이러한 변경 사항을 콘솔에서 확인할 수 있습니다. 그런 다음, 식물의 CSS 스타일을 조작하여 새로운 위치를 설정합니다. 이는 pos1
과 pos2
의 새로운 위치를 기준으로 식물의 상단과 왼쪽 X, Y 좌표를 계산하여 이루어집니다.
offsetTop
과offsetLeft
는 부모 요소를 기준으로 요소의 위치를 설정하는 CSS 속성입니다. 부모 요소는static
으로 설정되지 않은 모든 요소가 될 수 있습니다.
이러한 위치 재계산은 테라리움과 그 안의 식물의 동작을 세밀하게 조정할 수 있도록 합니다.
작업
마지막 작업은 elementDrag
의 닫는 중괄호 바로 아래에 stopElementDrag
함수를 추가하는 것입니다:
function stopElementDrag() {
document.onpointerup = null;
document.onpointermove = null;
}
이 작은 함수는 onpointerup
과 onpointermove
이벤트를 재설정하여 식물의 진행을 다시 시작하거나 새 식물을 드래그할 수 있도록 합니다.
✅ 이 이벤트를 null로 설정하지 않으면 어떤 일이 발생하나요?
이제 프로젝트가 완성되었습니다!
🚀도전 과제
클로저에 새로운 이벤트 핸들러를 추가하여 식물에 더 많은 동작을 추가해 보세요. 예를 들어, 식물을 더블 클릭하면 맨 앞으로 가져오는 기능을 추가할 수 있습니다. 창의력을 발휘해 보세요!
강의 후 퀴즈
복습 및 자습
화면에서 요소를 드래그하는 것은 사소해 보일 수 있지만, 이를 구현하는 방법은 다양하며 원하는 효과에 따라 많은 함정이 있을 수 있습니다. 사실, 드래그 앤 드롭 API라는 전체 API가 있습니다. 이번 모듈에서는 우리가 원하는 효과가 약간 달랐기 때문에 사용하지 않았지만, 이 API를 사용하여 자신만의 프로젝트에서 무엇을 할 수 있는지 확인해 보세요.
포인터 이벤트에 대한 더 많은 정보는 W3C 문서와 MDN 웹 문서에서 확인할 수 있습니다.
항상 CanIUse.com을 사용하여 브라우저 호환성을 확인하세요.
과제
면책 조항:
이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다. 정확성을 위해 최선을 다하고 있지만, 자동 번역에는 오류나 부정확성이 포함될 수 있습니다. 원본 문서를 해당 언어로 작성된 상태에서 권위 있는 자료로 간주해야 합니다. 중요한 정보의 경우, 전문적인 인간 번역을 권장합니다. 이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.