Frontend 14
DOM 조작의 기초: HTML에서 동적인 웹 페이지까지
HTML 파일은 텍스트인데 JavaScript가 querySelector로 어떻게 화면 요소를 찾아 바꾸는지 감이 없다.
근거 · 교안 p61-p66
1장: 텍스트 HTML, 어떻게 살아있는 ‘객체’가 되는가?
1장: 텍스트 HTML, 어떻게 살아있는 ‘객체’가 되는가?
솔라의 손가락이 노트북 화면 위를 불안하게 떠다녔다. 왼쪽에는 방금 작성한 간단한 index.html 파일이, 오른쪽에는 그 결과물인 웹페이지가 띄워져 있었다. ‘안녕하세요!’라는 제목과 짧은 문단 하나. 그게 전부였다. 하지만 솔라의 미간은 풀릴 줄 몰랐다. 코드 편집기에 보이는 것은 분명 알파벳과 꺾쇠괄호로 이루어진 텍스트 덩어리였다. 돌도, 나무도 아닌 그냥 글자.
“이상해.”
혼잣말을 들었는지, 옆에서 조용히 책을 읽던 루나가 고개를 들었다.
“뭐가?”
“이거. ‘DOM은 HTML을 브라우저가 다루는 객체 구조로 만든 것이다.’라는 문장.” 솔라는 모니터를 가리키며 말했다. “말은 알겠는데, 말이 안 돼. 이 HTML 파일은 그냥 텍스트 파일이잖아. 내가 메모장에 글 쓰는 거랑 다를 게 없다고. 그런데 어떻게 자바스크립트가 이 텍스트 파일을 조종해서 화면에 보이는 글자 색을 바꾸거나 내용을 지울 수 있다는 거야? 연결이 안 돼.”
솔라의 주장은 명확했다. 텍스트 파일은 그저 기록된 정보 덩어리일 뿐, 스스로 움직이거나 외부의 조종을 받을 수 있는 대상이 아니라는 생각이었다. 자바스크립트가 HTML 파일을 직접 열어서 특정 줄의 텍스트를 찾아 바꾼 다음 저장하는, 그런 복잡하고 비효율적인 일을 할 리는 없었다.
루나는 잠시 솔라의 화면을 들여다보더니, 오른쪽 브라우저 창을 턱짓으로 가리켰다.
“그 텍스트 파일이 브라우저 안으로 들어가면, 더 이상 그냥 텍스트 파일이 아니게 되니까.”
“그게 무슨 말이야? h1 태그는 여기서도 h1이고, 저기서도 h1이잖아.”
“정말 그럴까?” 루나는 자리에서 일어나 솔라의 옆으로 다가왔다. “브라우저한테 직접 물어보자. 걔가 지금 보고 있는 ‘안녕하세요!’가 정말 그냥 글자인지.”
루나는 솔라에게 브라우저의 개발자 도구를 열어보라고 했다. 익숙하게 F12 키를 누르자 화면 한쪽에 복잡한 창이 나타났다. 수많은 탭 중에서 루나는 ‘Elements’ 탭을 가리켰다.
“여길 봐. 네가 짠 코드랑 똑같이 생겼지?”
과연 그랬다. <html>, <body>, <h1>… 솔라가 코드 편집기에 입력했던 구조가 그대로 보였다. 하지만 미묘하게 달랐다. 마우스를 각 줄 위로 가져가자, 웹페이지의 해당 부분이 파랗게 강조되었다. 그냥 텍스트를 읽는 것과는 다른, 무언가 상호작용이 가능한 상태처럼 보였다.
“자, 이제 거기 <h1> 태그에 마우스 오른쪽 버튼을 눌러봐.”
솔라는 루나가 시키는 대로 했다. 여러 메뉴 항목이 나타났다. 루나는 그중 ‘Store as global variable’이라는 항목을 고르라고 했다. 솔라가 클릭하자, 바로 아래 ‘Console’ 탭에 == $0이라는 글자와 함께 temp1이라는 파란색 변수명이 나타났다.
“temp1이라는 임시 변수에 방금 네가 선택한 h1 요소를 저장한 거야. 이제 콘솔에 직접 물어볼 시간이야. temp1의 정체가 뭔지.”
루나는 솔라에게 콘솔 입력창에 특정 명령어를 쳐보라고 알려주었다.
typeof temp1
솔라는 조심스럽게 타이핑하고 엔터 키를 눌렀다. 잠시 후, 콘솔 창에 한 단어가 출력되었다.
'object'
솔라는 그 단어를 가만히 응시했다. ‘object’. 객체. 텍스트도, 숫자도, 단순한 문자열도 아니었다. 살아있는 프로그램의 부품처럼 다룰 수 있는 ‘객체’.
순간 머릿속에서 무언가 ‘탁’ 하고 연결되는 느낌이 들었다.
“아…!”
솔라는 자기도 모르게 탄성을 내뱉었다.
“알겠다. 브라우저는 index.html 파일을 그냥 보여주기만 하는 게 아니었어. 파일을 읽어서, 이 태그 하나하나를 전부 자바스크립트가 알아먹을 수 있는 ‘객체’로 바꿔서 가지고 있었던 거구나. 레고 설명서를 보고 진짜 레고 블록을 조립해놓은 것처럼!”
이제 모든 것이 이해되기 시작했다. 코드 편집기에 있는 것은 설계도인 텍스트 파일이지만, 브라우저가 화면에 보여주는 것은 그 설계도를 바탕으로 조립된 ‘객체 모델’이었던 것이다. 자바스크립트는 더 이상 손댈 수 없는 텍스트 파일을 상대하는 게 아니었다. 이미 조립이 끝나 언제든 만지고 조작할 수 있는 이 ‘객체’들을 상대하는 것이었다.
‘DOM은 HTML을 브라우저가 다루는 객체 구조로 만든 것이다.’
아까는 그저 뜬구름 잡는 소리 같았던 문장이, 이제는 너무나 명확하고 당연한 사실로 다가왔다. 솔라는 다시 Elements 탭을 바라보았다. 이제 <h1> 태그는 그냥 글자가 아니라, textContent나 style 같은 다양한 속성을 가진 입체적인 존재로 보였다.
“그래. 바로 그거야.” 루나가 작게 미소 지었다.
한 고비를 넘었다는 후련함에 솔라는 의자 등받이에 기댔다. 하지만 시선은 여전히 Elements 탭의 복잡한 구조에 머물러 있었다. html 태그 안에 body가 있고, 그 안에 또 h1과 p가 있고… 실제 웹사이트는 이것보다 수백, 수천 배는 더 복잡할 터였다. 문득 새로운 질문이 고개를 들었다.
“언니, 잠깐만. 그럼 이 수많은 객체 블록들 중에서 내가 원하는 딱 하나는 어떻게 찾아? 자바스크립트한테 ‘저기 저 세 번째 div 안에 있는 두 번째 p 태그 좀 가져와봐!’ 하고 어떻게 말해줘야 하지?”
2장: 방대한 DOM 트리에서 원하는 요소를 정확히 찾아내기
루나는 솔라의 질문에 말없이 고개를 끄덕이더니, 키보드 위로 손을 가져갔다. index.html 파일의 <h1> 태그 아래에 있던 단출한 <p> 태그 한 줄이 세 줄로 늘어나는 모습이 편집기 화면에 나타났다. 새로 생긴 태그들에는 각각 다른 표식이 붙었다. 하나는 class="highlight"라는 꼬리표를, 다른 하나는 id="main-text"라는 이름표를 달았다. 마지막으로 루나는 이 문단들을 통째로 <div> 태그로 감쌌다.
파일이 저장되자마자 브라우저 화면이 번쩍이며 새로고침되었다. 방금 전까지 단 하나의 문단만 덩그러니 있던 페이지는, 이제 제법 구조를 갖춘 문서처럼 보였다. 하지만 솔라의 눈에는 그저 더 복잡해진 미로일 뿐이었다. 이전 질문, ‘수많은 객체들 중에서 내가 원하는 걸 어떻게 찾지?’가 이제는 피할 수 없는 현실이 되어 눈앞에 펼쳐진 것이다.
“어… 이렇게 되면 자바스크립트한테 뭐라고 말해야 해?” 솔라의 목소리에는 조바심이 섞여 있었다. 마치 거대한 도서관에서 책 제목도, 저자도 모른 채 책을 찾아야 하는 막막한 기분이었다. “‘저기 저 highlight 클래스가 붙은 p 태그 좀 가져와봐!’ 하고 어떻게 말해줘야 하지? 어쩌면 document.findElementsByClassName('highlight') 같은 아주 긴 이름의 명령어가 있는 걸까? 아니면 document.paragraphs[1]처럼 배열 번호로 접근해야 하나?” 머릿속에 온갖 복잡한 추측이 떠올랐다.
루나는 콘솔 창을 가리키는 대신, 코드 편집기의 다른 부분을 짚었다. <head> 태그 안에 들어있는 <style> 블록이었다. 거기에는 방금 추가한 클래스를 위한 간단한 CSS 규칙이 적혀 있었다.
.highlight {
background-color: yellow;
}
“이 코드는 무슨 뜻이지?” 루나가 물었다.
“응? 그건… ‘highlight’라는 클래스를 가진 모든 요소의 배경색을 노란색으로 만들라는 거잖아.” 솔라는 너무 당연한 걸 묻는다는 듯이 대답했다. 실제로 브라우저 화면에서 해당 문단은 노란색 배경을 가지고 있었다.
“맞아. 넌 이미 브라우저에게 수많은 HTML 태그 중에서 ‘highlight 클래스를 가진 것’만 정확히 골라내는 방법을 알려준 거야. CSS를 통해서.”
루나의 말에 솔라는 잠시 숨을 멈췄다. ‘CSS를 통해서…’ 전혀 생각지도 못한 연결고리였다. 스타일을 꾸미기 위해 무심코 사용했던 문법이, 사실은 광대한 HTML 구조 속에서 특정 요소를 정확히 ‘지목’하는 주소 체계였던 것이다.
“자바스크립트도 똑같은 주소 체계를 사용해.” 루나는 말을 이었다. “네가 이미 알고 있는 그 방법을 그대로 쓰면 돼. 이제 콘솔에 직접 명령을 내려보자. ‘이 문서(document)에서, 이 선택자(.highlight)로, 요소를 찾아줘(query).’”
루나의 안내에 따라 솔라는 콘솔 창에 조심스럽게 타이핑했다.
document.querySelector('.highlight')
엔터 키를 누르자, 콘솔 창에 <p class="highlight"> 객체가 즉시 나타났다. Elements 탭에서 보았던 바로 그 객체 블록이었다. 복잡하고 긴 명령어를 상상했던 솔라는 허탈할 정도였다. 너무나 간단했다. 자바스크립트에게 무언가를 찾는 새로운 방법을 배울 필요가 없었다. 이미 알고 있던 CSS 선택자 문법, 그걸 그대로 쓰기만 하면 되는 것이었다.
“대박… 그럼 아이디도 돼? #main-text?”
솔라가 신나서 다음 명령어를 입력했다.
document.querySelector('#main-text')
결과는 마찬가지였다. id="main-text"를 가진 <p> 태그 객체가 정확히 반환되었다.
“그럼 만약에, 똑같은 태그가 여러 개 있으면 어떻게 돼? p 태그는 지금 세 개 있잖아.”
솔라는 곧장 document.querySelector('p')를 입력했다. 그러자 첫 번째 <p> 태그 객체 하나만 덩그러니 나타났다.
“아, querySelector는 조건에 맞는 것 중에 제일 첫 번째 하나만 찾아주는구나.” 솔라가 스스로 결론을 내렸다.
“그럼 전부 다 찾고 싶을 때는?” 루나가 되물었다.
솔라는 잠시 고민했다. ‘하나를 선택하는 게 querySelector라면, 전부 선택하는 건… querySelectors? 아니면…All?” 솔라는 마지막 추측에 힘을 실어 입력했다.
document.querySelectorAll('p')
엔터 키를 누르자, 이전과는 다른 결과가 나타났다. 객체 하나가 아니라, 괄호 안에 세 개의 <p> 객체가 담겨있는 목록(NodeList)이 출력되었다. 마치 ‘네가 찾던 p 태그 여기 세 개 다 있어’라고 말하는 것 같았다.
솔라는 다시 모니터의 Elements 탭과 콘솔 창을 번갈아 보았다. 아까는 그저 복잡하게 얽힌 나무 구조처럼 보였던 DOM이, 이제는 잘 정리된 데이터베이스처럼 느껴졌다. 태그 이름으로, 클래스로, 아이디로, 심지어는 div > p 같은 복잡한 구조로도 원하는 것을 무엇이든 ‘선택’할 수 있다는 자신감이 생겼다. ‘querySelector와 querySelectorAll로 요소를 선택한다’는 문장은 더 이상 암기해야 할 지식이 아니었다. 그것은 강력한 탐색 도구를 손에 쥐는 주문이었다.
흥분한 솔라는 콘솔에 출력된 <p class="highlight"> 객체를 마우스로 가리켰다.
“좋았어! 이제 이 녀석을 내 손에 넣었어. 그럼… 이제 이걸로 뭘 할 수 있지? 이 객체를 이용해서 화면에 보이는 글자를 ‘안녕!’에서 ‘잘 가!’로 바꿀 수 있어? 아니면 노란색 배경을 파란색으로 바꿀 수도 있나?”
객체를 선택하는 방법을 알아내자, 솔라의 호기심은 곧바로 다음 단계로 향하고 있었다. 선택한 이 블록을 어떻게 원하는 대로 주무를 수 있을까?
3장: 선택된 DOM 객체의 내용, 속성, 스타일 자유자재로 변경하기
솔라의 질문이 채 끝나기도 전에, 루나의 손가락이 다시 키보드 위에서 움직였다. index.html 파일에 몇 줄의 코드가 빠르게 추가되었다. 방금 전까지 없었던 <img> 태그 하나와, 아무 내용 없는 <div> 태그 하나였다. 특히 <img> 태그는 src="" 속성이 텅 빈 채로 덩그러니 놓여 있었다.
파일이 저장되자 브라우저 화면이 다시 한번 번쩍이며 바뀌었다. 깔끔하던 페이지 한구석에 깨진 이미지 아이콘이 흉하게 나타났고, 그 아래에는 빈 공간만 덩그러니 자리 잡고 있었다. 솔라가 방금 손에 넣었다고 기뻐했던 <p class="highlight"> 객체는 이제 새로운 이웃들 사이에서 어색하게 보였다. 이것이 솔라의 다음 질문에 대한 루나의 대답이었다. ‘이제 이걸로 뭘 할 수 있냐고? 이 새로운 요소들로 직접 보여줄게.’
솔라는 미간을 찌푸렸다. “이건 또 뭐야? 이제 저 깨진 이미지랑 빈 박스를 가지고 뭘 하라는 거야?”
루나는 말없이 솔라의 노트북 화면 한쪽에 참고 자료처럼 보이는 짧은 목록을 띄웠다.
element.textContent = "..."element.innerHTML = "..."element.setAttribute('속성이름', '값')element.style.color = "blue"element.classList.add('클래스이름')
목록을 본 솔라의 표정이 순식간에 굳었다. “잠깐만. 그럼 글자를 바꿀 땐 textContent를 쓰고, 이미지 주소를 바꿀 땐 setAttribute를 쓰고, 색깔을 바꿀 땐 style을 써야 한다고? 또 새로운 클래스를 추가할 땐 classList? 뭐야, 그럼 내가 하고 싶은 조작마다 전부 다른 명령어를 외워야 하는 거야? 이건 너무 복잡하잖아!”
솔라의 머릿속은 뒤죽박죽이 되었다. 내용을 바꾸는 방법, 속성을 바꾸는 방법, 스타일을 바꾸는 방법이 전부 제각각인 별개의 기술처럼 느껴졌다. 간신히 ‘선택’이라는 강력한 도구를 얻었는데, 정작 선택한 이후에 할 수 있는 일들이 수십 개의 제멋대로인 규칙 덩어리로 보였다.
“전부 다른 명령어처럼 보여?” 루나가 조용히 물었다. “하나씩 직접 해보면서 다시 생각해 봐. 먼저, 아까 우리가 추가한 빈 <div>부터.”
루나는 솔라에게 콘솔 창에 먼저 그 div 요소를 선택해서 변수에 저장하라고 했다.
const myDiv = document.querySelector('div');
“좋아. 이제 myDiv라는 객체를 손에 넣었어. 그 객체 안에 들어있는 글자를 바꿔보자. 아까 그 목록에 있던 textContent를 써서.”
솔라는 반신반의하며 키보드를 두드렸다. 마치 객체의 속성에 새로운 값을 할당하는 것처럼.
myDiv.textContent = '여기에 글자가 나타났습니다!';
엔터 키를 누르는 순간, 브라우저의 텅 비어 있던 공간에 ‘여기에 글자가 나타났습니다!’라는 문장이 마법처럼 나타났다.
“어?” 솔라는 짧은 탄성을 내뱉었다. “이건… 그냥 객체의 속성값을 바꾸는 거랑 똑같잖아? myDiv라는 객체의 textContent라는 속성에 새로운 문자열을 넣어준 것뿐이네.”
다음은 깨진 이미지 차례였다. 루나는 이번에도 먼저 img 요소를 선택하게 했다.
const myImage = document.querySelector('img');
“이번엔 setAttribute를 써볼까? ‘src’라는 속성(attribute)에 이미지 주소를 ‘설정(set)’해주는 거야.”
myImage.setAttribute('src', 'https://via.placeholder.com/150');
명령을 실행하자마자, 흉하게 깨져 있던 아이콘이 사라지고 그 자리에 깔끔한 회색 사각형 이미지가 나타났다. 솔라는 잠시 생각에 잠겼다. ‘setAttribute는 함수 형태이긴 하지만, 결국 myImage라는 객체를 상대로 ‘src’라는 속성을 바꾸라고 명령한 거네.’
마지막으로, 이전 장에서 선택했던 노란색 배경의 문단이 눈에 들어왔다.
const highlightP = document.querySelector('.highlight');
“이번엔 색을 바꿔보자. style 속성을 이용할 거야.”
highlightP.style.color = 'blue';
즉시 문단의 검은색 글씨가 파란색으로 변했다.
“잠깐, 알 것 같아!” 솔라가 외쳤다. “highlightP라는 객체 안에는 style이라는 속성이 또 들어있는 거구나! 그리고 그 style 객체 안에 color라는 속성이 있어서 그걸 바꾼 거고. 객체.속성.속성… 결국 이것도 객체의 속성을 바꾼 거네!”
솔라의 눈이 빛나기 시작했다. 별개의 명령어 목록처럼 보였던 것들이 하나의 원리로 꿰뚫리고 있었다.
루나는 마지막 퍼즐 조각을 던졌다. “CSS에 .dark-mode라는 클래스가 미리 정의되어 있다고 상상해봐. 배경은 검은색, 글자는 흰색으로 만드는. 그 스타일을 한 번에 적용하려면?”
솔라는 목록의 마지막 항목인 classList를 떠올렸다.
highlightP.classList.add('dark-mode');
그 순간, 노란색 배경에 파란 글씨였던 문단이 검은 배경의 흰 글씨로 완전히 탈바꿈했다.
솔라는 키보드에서 손을 떼고 화면을 가만히 응시했다. 글자를 바꾸고, 이미지 주소를 설정하고, 색깔을 바꾸고, 클래스를 통째로 갈아 끼우는 모든 과정. 시작점은 달랐지만 종착점은 같았다.
“알았어… 완전히 알았어.” 솔라가 조용히 말했다. “나는 바보같이 ‘글자 바꾸기’, ‘이미지 바꾸기’, ‘색 바꾸기’가 다 다른 마법 주문인 줄 알았어. 아니었어. 그냥 딱 두 단계였던 거야.”
솔라는 손가락을 펴 보였다.
“첫째, querySelector로 원하는 객체를 선택한다. 둘째, 그 객체가 가진 수많은 속성(textContent, style, classList 등)들 중 내가 원하는 것의 값을 조작한다. 끝. 이게 전부였어.”
아까는 그저 암기해야 할 목록으로 보였던 textContent, setAttribute, style, classList가, 이제는 ‘DOM 객체’라는 상자 안에 들어있는 여러 가지 도구의 이름표처럼 보였다. 무엇을 바꾸든 근본 원리는 ‘객체의 속성을 변경한다’는 것으로 통일되었다.
스스로 깨달음을 얻은 솔라는 갑자기 새로운 장난을 꾸미는 아이처럼 눈을 반짝였다. 그녀는 재빨리 index.html 파일에 <button>다크 모드</button>라는 코드를 추가했다. 그리고 콘솔 창에 망설임 없이 명령어를 입력하기 시작했다.
const bodyElement = document.querySelector('body');
const modeButton = document.querySelector('button');
그리고는 마치 완성된 기능을 시험하듯 한 줄씩 실행했다.
bodyElement.style.backgroundColor = 'black';
modeButton.textContent = '라이트 모드';
순식간에 웹 페이지 전체 배경이 검게 변하고, 버튼의 글씨가 바뀌었다. 솔라는 만족스러운 미소를 지었다.
“좋아. 이제 이 버튼을 클릭할 때마다 이 코드들이 실행되게 만들면, 진짜 다크 모드 기능이 되는 거네. 어떻게 조작해야 하는지 알았으니까, 이제 못할 게 없겠어.”
더 이상 HTML은 단순한 텍스트가 아니었다. 자바스크립트는 미지의 마법이 아니었다. 솔라의 눈앞에는 이제 얼마든지 모양을 바꾸고, 내용을 채우고, 색을 입힐 수 있는 살아있는 객체들의 세계가 펼쳐져 있었다.