Frontend 15

DOM 요소 생성, 이벤트, Form 입력 처리: 뒤섞인 화면 갱신 흐름 이해하기

버튼 클릭, input 값 읽기, submit 막기, 새 요소 추가가 한꺼번에 나오면 화면과 데이터 흐름이 뒤섞인다.

근거 · 교안 p67-p72

DOM 요소 생성, 이벤트, Form 입력 처리: 뒤섞인 화면 갱신 흐름 이해하기 대표 이미지

1장: 화면에 요소 ‘만들고 붙이기’: DOM 구조 변경의 시작

솔라의 노트북 화면에는 알록달록한 코드 편집기가 열려 있었다. ‘투두 리스트 만들기’ 예제 코드였다. form, input, button 태그가 보였고, 그 아래에는 addEventListener, createElement, appendChild, value 같은 자바스크립트 명령어들이 복잡하게 얽혀 있었다. 솔라는 미간을 찌푸린 채 화면과 노트를 번갈아 보았다.

“분명 아는 단어들인데….”

입력창에 할 일을 쓰고 버튼을 누르면 목록에 착 추가되는 간단한 기능. 하지만 코드는 간단해 보이지 않았다. 사용자가 버튼을 ‘클릭’하는 순간, 입력창의 글자를 ‘읽고’, 새로운 목록 요소를 ‘만들어서’, 화면에 ‘붙이는’ 이 모든 일이 한꺼번에 일어나는 것처럼 보였다. 마치 마술 같았다. 하지만 그 마술의 비밀을 파헤치려 할수록, 각각의 동작들이 뒤섞여 머릿속만 더 혼란스러워졌다.

그때 방에 들어온 루나 언니가 솔라의 굳은 표정을 보고는 조용히 옆에 섰다.

“뭐가 그렇게 복잡해?”

“언니, 이거 봐. 버튼 클릭, 글자 가져오기, 새 요소 만들기, 화면에 붙이기… 이 모든 게 뒤죽박죽이야. 대체 어떤 순서로 일이 처리되는 건지 모르겠어. 그냥 한 번에 ‘짠’ 하고 나타나는 것 같아.”

솔라의 손가락이 예제 코드의 자바스크립트 부분을 불안하게 맴돌았다. 루나는 잠시 코드를 들여다보더니, 고개를 끄덕였다.

“그걸 한 번에 다 보려고 하니까 엉키는 거야. 우리, 딱 하나만 먼저 해보자.”

루나는 솔라의 코드 편집기에서 새 파일을 열었다.

“텅 빈 하얀 화면에, 버튼 딱 하나만 나타나게 하는 것부터. 마술 말고, 조립을 해보는 거지.”

솔라는 반신반의하며 루나가 시키는 대로 텅 빈 HTML 파일을 하나 만들어 브라우저에서 열었다. 예상대로 화면은 아무것도 없는 새하얀 도화지였다.

“자, 이제 개발자 도구를 열고 콘솔 창에 이렇게 한번 써봐.”

루나가 나지막이 말했다.

const myButton = document.createElement('button');

솔라는 조심스럽게 코드를 따라 입력하고 엔터 키를 눌렀다. 콘솔 창에는 undefined라는 글자만 뜰 뿐, 새하얀 화면에는 아무런 변화도 없었다.

“어? 아무것도 안 변하는데?”

솔라가 고개를 갸웃거리자 루나가 옅은 미소를 지었다.

“그치? 그럼 이제 콘솔에 myButton이라고만 다시 입력해볼래?”

솔라는 myButton이라고 입력하고 다시 엔터를 쳤다. 그러자 콘솔 창에 <button></button>이라는 결과가 출력되었다.

“어? 여기엔 있네. 컴퓨터 안에는 만들어졌는데, 왜 화면에는 없지?”

바로 그 지점이었다. 솔라가 혼란스러워했던 첫 번째 매듭. 루나는 화면과 콘솔 창을 차례로 가리켰다.

“응. 지금 솔라 네가 한 건, 버튼이라는 ‘부품’을 허공에 만들기만 한 거야. 아직 이 하얀 도화지에 ‘붙이지’는 않았어. 우리 눈에 보이지 않는 곳에 그냥 둥둥 떠 있는 거지.”

부품. 조립. 그제야 솔라의 머릿속에서 무언가 ‘분리’되기 시작했다.

“그럼 이제 조립해볼까? 이렇게.”

루나가 다음 코드를 불러주었다.

document.body.appendChild(myButton);

솔라가 코드를 입력하고 엔터를 치는 순간, 마법처럼 하얀 화면 한구석에 회색 버튼이 툭 튀어나왔다. 정말이었다. 허공에 떠다니던 부품이 제자리를 찾아 딱 붙는 느낌이었다.

솔라의 눈이 동그래졌다.

“아! createElement는 그냥 부품을 만드는 명령어였구나. 이걸 화면에 보이게 하려면 appendChild로 ‘여기에 붙여라’ 하고 명령을 한 번 더 해야 하는 거였어. 만드는 거랑 붙이는 게 완전히 다른 단계였네!”

뒤죽박죽 섞여 있던 실타래에서 첫 번째 가닥이 선명하게 풀려 나왔다. 화면을 바꾸는 것은 한 번의 ‘짠’하는 마술이 아니었다. 보이지 않는 곳에서 요소를 ‘만들고’, 그것을 눈에 보이는 문서(DOM) 구조에 ‘붙이는’ 두 단계로 나뉜 명백한 절차였다.

기분이 좋아진 솔라는 방금 나타난 버튼을 의기양양하게 클릭했다. 딸깍, 딸깍. 하지만 마우스 커서만 바쁘게 움직일 뿐, 버튼은 아무런 미동도, 반응도 없었다. 방금 전까지의 흥분과 감탄이 무색해졌다.

“언니. 근데… 버튼이 생기긴 했는데, 왜 아무 일도 안 일어나?”

솔라가 멋쩍게 물었다. 루나는 아무 말 없이 그저 묵묵히 ‘죽어 있는’ 버튼을 바라볼 뿐이었다. 이제 막 조립된 부품에는 아직 영혼이 깃들지 않은 것 같았다.

2장: 사용자 행동에 ‘반응하기’: 이벤트 리스너 연결

솔라의 손가락 끝에서 마우스가 딸깍거렸지만, 화면 속 회색 버튼은 그저 묵묵부답이었다. 방금 전 appendChild로 화면에 나타났을 때의 감동은 온데간데없고, 이제는 그저 움직이지 않는 그림처럼 느껴졌다. 솔라의 미간에 다시 희미한 주름이 잡혔다.

옆에서 지켜보던 루나는 솔라의 질문에 바로 대답하는 대신, 조용히 키보드를 가져가 콘솔 창에 새로운 코드를 한 줄 입력했다.

function handleClick() {
  console.log('버튼이 눌렸어요!');
}

엔터 키를 누르자 undefined가 출력되었다. 화면에는 여전히 아무 변화가 없었다. 솔라는 이게 무슨 의미인지 알 수 없었다. 함수를 하나 만들었지만, 그게 저 ‘죽어있는’ 버튼과 무슨 상관이란 말인가. 첫 번째 실마리를 풀었다고 생각했는데, 또 다른 벽에 부딪힌 기분이었다.

“언니, 함수를 하나 만들긴 했는데… 이걸로 뭘 어떡하라고?”

솔라의 목소리에는 조급함이 섞여 있었다. 버튼을 누르면 뭔가 일어나야 한다는 생각, 그 ‘클릭’이라는 행동이 여전히 머릿속에서 다른 개념들과 뒤엉켜 소용돌이치고 있었다.

루나는 말없이 다음 코드를 입력했다. 마치 분리된 두 개의 부품을 조심스럽게 이어 붙이는 기술자처럼.

myButton.addEventListener('click', handleClick);

코드가 나타나자 솔라는 눈을 가늘게 떴다. myButton. 아까 만들었던 그 버튼이었다. 그리고 handleClick. 방금 루나가 만든 함수였다. 가운데에는 따옴표에 싸인 click이라는 낯선 단어가 있었다.

“자, 이제 다시 버튼을 눌러봐.”

루나가 말했다. 솔라는 반신반의하며 마우스를 움직여 화면 속 회색 버튼을 클릭했다.

딸깍.

그 순간, 개발자 도구의 콘솔 창에 파란색 글씨로 메시지가 나타났다.

> 버튼이 눌렸어요!

솔라는 자기도 모르게 “어!” 하고 소리를 냈다. 다시 한번 버튼을 클릭했다.

> 버튼이 눌렸어요!

메시지가 한 줄 더 쌓였다. 누를 때마다 정확하게 반응했다. 죽어있던 버튼에 마치 영혼이 깃든 것 같았다. 버튼을 만드는 것, 화면에 붙이는 것, 그리고 이제… 버튼에 생명을 불어넣는 것. 세 번째 단계가 눈앞에서 펼쳐졌다.

addEventListener… 이게 ‘듣는 귀’를 붙여주는 거구나!”

솔라가 흥분하며 말했다.

myButton한테 ‘click’이라는 소리가 들리면, handleClick 함수를 실행하라고 약속을 걸어두는 거네! 버튼 자체는 원래 아무것도 못 하는 게 맞았어. 우리가 뭘 할지 알려줘야만 움직이는 거였구나.”

솔라의 머릿속에서 뒤엉켜 있던 ‘버튼 클릭’이라는 개념이 제자리를 찾아 분리되었다. 그것은 마술이 아니었다.

  1. 사용자의 행동을 감지할 대상 (myButton)
  2. 감지할 행동의 종류 ('click')
  3. 행동이 감지되었을 때 실행할 임무 (handleClick 함수)

이 세 가지를 명확하게 ‘연결’해주는 절차였다. addEventListener는 바로 그 연결 고리의 이름이었다. 이제 솔라는 이것을 ‘이벤트 관찰자’라고 부르기로 마음먹었다. 특정 행동이 일어나는지 아닌지 계속 지켜보는 역할이니까.

“정확해. 만드는 것, 붙이는 것, 그리고 이제 ‘듣게’ 하는 것까지 분리한 거야.”

루나의 차분한 목소리에 솔라는 고개를 끄덕였다. 드디어 뒤죽박죽 엉켜 있던 코드 뭉치에서 또 하나의 가닥을 명확하게 풀어낸 것이다.

하지만 기쁨도 잠시, 솔라의 시선은 콘솔 창에 찍힌 메시지에서 화면의 버튼으로, 다시 콘솔 창으로 옮겨갔다.

“그런데 언니. 콘솔에 글씨만 찍히잖아. 내가 원래 하려던 건 이게 아닌데. 버튼을 누르면 화면에 새로운 목록이 추가되거나, 숫자가 바뀌거나… 그런 걸 하려면 어떻게 해야 해?”

솔라는 새로운 의문에 부딪혔다. 관찰자를 붙여 신호를 받는 데까지는 성공했지만, 그 신호를 받아서 화면을 바꾸는 진짜 ‘마술’은 아직 시작도 하지 못한 것 같았다.

“그리고 ‘click’ 말고 또 뭘 들을 수 있는데? ‘submit’ 같은 것도 아까 코드에 있었는데, 그런 건 어떻게 쓰는 거야?”

3장: 이벤트에서 ‘정보 읽기’: 입력값과 이벤트 객체 활용

솔라는 지난번에 만든 회색 버튼과 콘솔 창을 번갈아 보았다. 버튼을 누르면 콘솔에 ‘버튼이 눌렸어요!’라고 외치는, 충실하지만 어딘가 부족한 녀석이었다. 화면을 바꾸는 진짜 마술을 부리고 싶었지만, 콘솔에 찍히는 글자로는 어림도 없었다. ‘클릭’이라는 신호를 받는 데는 성공했지만, 그 신호에 어떤 의미를 담거나, 다른 곳의 정보를 가져와 사용하는 방법은 여전히 안갯속이었다.

솔라가 골똘히 생각에 잠긴 사이, 루나가 조용히 콘솔 창에 몇 글자 더 입력했다.

const myInput = document.createElement('input');
document.body.appendChild(myInput);

엔터 키가 눌리자마자, 얌전히 있던 회색 버튼 옆에 하얀 입력창이 툭 생겨났다. 솔라는 화면에 나타난 새로운 ‘부품’을 멍하니 바라보았다. 방금 전까지 버튼 하나만 덩그러니 있었는데, 이제는 무언가 입력할 수 있는 공간이 생겼다. 이 새로운 조합이 무엇을 의미하는지, 솔라는 직감적으로 깨달아야 했다.

“언니, 이건 또 뭐야?”

“원래 하려던 게 뭐였지? 투두 리스트. 목록에 추가할 할 일은 어디서 오는 걸까?”

루나의 말에 솔라는 아, 하고는 새로 생긴 입력창에 마우스 커서를 가져갔다. ‘여기서 오는 거지.’ 솔라는 키보드로 ‘자바스크립트 공부’라고 입력했다. 하얀 입력창에 검은 글씨가 선명하게 새겨졌다.

“그래. 글자는 여기에 있어. 그리고 행동은 저 버튼을 누르는 거고. 그럼… 버튼을 눌렀을 때, 코드가 이 상자 안에 내가 쓴 글자가 뭔지 어떻게 알아?”

바로 그 지점이었다. 데이터는 입력창에 있고, 행동의 신호는 버튼에서 발생한다. 둘은 명백히 분리되어 있었다. 솔라의 머릿속에서 ‘입력값 읽기’와 ‘버튼 클릭’이라는 두 개의 퍼즐 조각이 서로 맞물리지 않고 겉돌았다.

루나는 지난번에 만들었던 handleClick 함수를 조용히 수정하기 시작했다.

// function handleClick() {
//   console.log('버튼이 눌렸어요!');
// }
// myButton.addEventListener('click', handleClick);

function handleInputChange(event) {
  console.log(event);
}

myInput.addEventListener('change', handleInputChange);

코드가 바뀌자 솔라의 눈이 가늘어졌다. myButton에 연결했던 ‘관찰자’를 떼어내고, 새로 만든 myInputchange라는 이름의 관찰자를 붙였다. 그리고 가장 이상한 부분. 텅 비어 있던 함수의 소괄호 안에 event라는 처음 보는 단어가 둥지를 틀었다.

event? 저건 어디서 온 거야? 우리가 저 함수를 부를 때 아무것도 안 넣어줬잖아.”

마치 약속하지 않은 손님이 불쑥 나타난 것 같았다. 솔라에게 이벤트는 그저 함수를 실행시키는 ‘신호탄’ 같은 것이었지, 그 자체가 무언가를 들고 오는 존재라고는 생각하지 못했다.

“일단 한번 직접 확인해봐. 입력창에 아무거나 쓰고 엔터 키를 눌러봐.”

솔라는 루나의 말에 따라 입력창에 ‘안녕하세요’라고 입력하고 엔터 키를 쳤다. 그 순간, 콘솔 창이 거대한 객체 하나를 토해냈다.

> ChangeEvent {isTrusted: true, type: 'change', target: input, … }

수십 줄에 달하는 속성들이 빼곡히 들어찬 정보의 산이었다. 솔라는 압도당했다. “우와, 이게 다 뭐야? 그냥 신호만 오는 게 아니었네.”

“전부 읽을 필요는 없어. 딱 하나만 찾아봐. target.”

루나가 차분하게 말했다. 솔라는 console.log(event)console.log(event.target)으로 바꾸고 다시 코드를 실행했다. 이번에는 콘솔에 익숙한 모습이 나타났다.

> <input>

“어! 이건… 아까 우리가 만든 입력창이잖아!”

“맞아. event.target은 이벤트가 발생한 바로 그 주인공이야. 이제 그 주인공한테 물어보기만 하면 돼. ‘네가 지금 가지고 있는 값이 뭐니?’ 하고.”

그 말에 솔라의 머릿속에서 번개가 쳤다. 입력창(input element)의 값을 가져오는 속성. 여기저기서 본 기억이 났다. 솔라는 떨리는 손으로 코드를 다시 수정했다.

console.log(event.target.value);

입력창에 ‘유레카!’라고 외치듯 입력하고 엔터를 치자, 콘솔 창에 선명하게 글자가 찍혔다.

> 유레카!

“됐어! 됐어, 언니! event는 그냥 신호가 아니라… 사건 현장 보고서 같은 거였어! 누가(target), 무슨 짓을 했는지 다 적혀있는 거야. 우리는 그 보고서를 보고 value 같은 필요한 정보만 쏙 빼내면 되는 거였어!”

솔라의 목소리는 흥분으로 가득 찼다. 뒤섞여 있던 ‘이벤트’와 ‘입력값 읽기’가 ‘event.target.value’라는 명확한 경로로 이어지는 순간이었다. 이 작은 event 객체야말로, 사용자의 세상과 코드의 세상을 연결하는 가장 중요한 정보원이었다. 솔라는 이 녀석을 ‘이벤트 정보 해석기’라고 불러주고 싶었다.

자신감이 붙은 솔라는 의욕적으로 키보드를 두드렸다. “좋았어. 이제 이 값으로 createElement 해서 <li> 만들고 appendChild로 화면에 진짜 붙여볼 거야!”

솔라는 재빨리 HTML에 <form> 태그를 추가하고, 그 안에 입력창과 제출 버튼을 넣었다. 그리고 submit 이벤트가 발생했을 때 입력창의 값을 가져와 새 목록을 만드는 코드를 작성했다. 모든 조각이 제자리를 찾은 완벽한 계획이었다.

“자, 간다!”

솔라는 입력창에 ‘첫 번째 할 일’이라고 쓰고 제출 버튼을 힘차게 클릭했다.

찰나의 순간, 화면에 ‘첫 번째 할 일’이라는 목록이 나타나는가 싶더니, 눈을 한번 깜빡이자 페이지 전체가 하얗게 번쩍이며 새로고침되었다. 방금 입력했던 글자도, 나타날 뻔했던 목록도 모두 사라지고 초기 상태로 돌아와 버렸다.

“어? 어어? 방금 분명히 보였는데! 왜 다 사라졌지?”

솔라의 당황한 외침이 방을 울렸다. 루나는 아무 말 없이, 마치 ‘원래 그런 거야’라고 말하는 듯한 미소를 지으며 깜빡거리는 커서만 바라보고 있었다. 브라우저에게는 자바스크립트 코드 말고도, 지켜야 할 자기만의 규칙이 있는 듯했다.

4장: 브라우저 ‘방해 막기’: 기본 동작 제어

솔라의 눈앞에서 화면이 하얗게 번쩍였다. 방금 전까지만 해도 ‘첫 번째 할 일’이라는 글자가 목록에 추가되는 것이 분명히 보였는데, 그 찰나의 환영은 온데간데없이 사라졌다. 마치 아무 일도 없었다는 듯, 텅 빈 입력창과 초기화된 화면만이 솔라를 맞이했다.

솔라는 허탈함에 마우스만 쥔 채 굳어버렸다. createElement로 부품을 만들고, appendChild로 조립하고, addEventListener로 귀를 달아주고, event.target.value로 정보까지 빼내 왔다. 모든 조각을 완벽하게 맞췄다고 생각했는데, 마지막 순간에 정체 모를 힘이 모든 것을 수포로 돌린 기분이었다.

“방금… 보였는데,” 솔라가 멍하니 중얼거렸다. “왜 내 말을 안 듣고 마음대로 화면을 바꿔버리는 거야?”

솔라의 당황한 목소리에, 옆에서 지켜보던 루나가 조용히 화면의 <form> 태그를 손가락으로 가리켰다.

“솔라. 넌 지금 브라우저랑 동시에 말을 하고 있어. 그리고 브라우저가 너보다 목소리가 더 크네.”

“브라우저랑 말을 한다고? 난 내 코드랑만 얘기하고 있었는데?”

솔라는 이해할 수 없다는 표정을 지었다. 자바스크립트 코드는 자신이 짠 것이고, 브라우저는 그 코드를 실행하는 도구일 뿐이라고 생각했다. ‘제출(submit)’ 버튼을 눌렀을 때 실행될 동작은 자신이 쓴 함수가 전부여야 했다.

루나는 솔라의 코드에서 <form>이라고 쓰인 부분을 다시 한번 톡톡 두드렸다.

“이 <form> 태그의 오랜 임무가 뭔지 생각해 봐. 자바스크립트가 없던 아주 먼 옛날부터, 이 태그는 무슨 일을 하려고 만들어졌을까?”

오랜 임무. 그 말에 솔라는 잠시 생각에 잠겼다. 투두 리스트 예제뿐만 아니라, 회원가입 페이지, 로그인 페이지… 폼 태그는 늘 어딘가로 정보를 ‘제출’하는 곳에 있었다.

“음… 사용자가 입력한 정보를 어딘가 다른 곳, 그러니까 서버 같은 곳으로 보내는 거?”

“맞아. 그리고 정보를 보낸 후에는, 보통 새로운 페이지로 이동하거나 현재 페이지를 새로고침해서 그 처리 결과를 보여주지. 그게 바로 <form> 태그가 태어날 때부터 갖고 있던 약속이자, 브라우저의 ‘기본 동작’이야.”

기본 동작. 솔라의 머릿속에 그 단어가 박혔다. 솔라가 submit 버튼을 누르는 순간, 두 가지 명령이 동시에 울려 퍼진 것이었다. 하나는 솔라가 직접 작성한 ‘새로운 목록을 추가하라’는 자바스크립트의 명령. 그리고 또 하나는 브라우저가 <form> 태그와의 오랜 약속에 따라 수행하는 ‘페이지를 새로고침하라’는 기본 동작. 결과는 명백했다. 브라우저는 자바스크립트가 화면에 무언가 그리기도 전에, 자신의 기본 임무인 새로고침을 더 충실하게 수행해 버린 것이다.

“그럼… 브라우저더러 잠깐 조용히 하라고 말해야 하는 거야?”

“바로 그거야. 우리가 대화를 주도해야지.”

루나는 지난번에 수정한 handleInputChange 함수를 handleSubmit이라는 이름으로 바꾸고, event 매개변수를 다시 소환했다. 솔라는 이제 event가 단순한 신호가 아니라, 사건의 모든 정보가 담긴 보고서라는 사실을 알고 있었다.

function handleSubmit(event) {
  // 1. 브라우저의 기본 동작을 막는다.
  
  // 2. 입력값을 가져온다.
  const inputValue = event.target.querySelector('input').value;
  
  // 3. 새 요소를 만들어 화면에 추가한다.
  // ... (이하 생략)
}

myForm.addEventListener('submit', handleSubmit);

루나는 코드의 맨 윗줄, 주석 바로 아래에 커서를 옮겼다.

“그 보고서에는 사건을 중단시킬 수 있는 특별한 명령어도 들어있어. 이렇게.”

루나가 키보드를 두드리자 한 줄의 코드가 나타났다.

event.preventDefault();

‘기본(default)을 막는다(prevent)’라는, 너무나도 직설적인 이름이었다. 솔라는 그 한 줄이 만들어낼 차이가 궁금해졌다.

“자, 이제 다시 해봐.”

솔라는 침을 꿀꺽 삼키고, 다시 입력창에 ‘두 번째 시도’라고 적었다. 그리고 이전보다 훨씬 조심스럽게 제출 버튼을 클릭했다.

… 딸깍.

아무 일도 일어나지 않았다. 아니, 정확히는 ‘아무 일도 일어나지 않는 일’이 일어났다. 화면이 번쩍이지 않았다. 페이지는 미동도 없이 제자리에 있었고, 솔라가 방금 입력한 ‘두 번째 시도’라는 글자가 목록에 얌전히 추가되어 있었다. 사라지지 않고, 굳건히 그 자리를 지켰다.

“우와…! 진짜다! preventDefault… 이 한 줄이 브라우저의 입을 막아버렸네!”

솔라는 방금 추가된 목록을 보며 감탄했다. 이제야 모든 조각이 통제 아래 들어온 느낌이었다. 브라우저가 자기 마음대로 화면을 새로고침하는 ‘방해’를 막고, 오롯이 자바스크립트가 화면을 제어할 권한을 되찾아온 것이다. “아, submit은 무조건 새로고침되는 게 아니었구나. 그냥 브라우저의 ‘기본 동작’일 뿐이었고, 우리가 그걸 막을 수 있었던 거네.” 이것은 단순한 기술이 아니었다. 브라우저와의 대화에서 주도권을 잡는 방법이었다. 솔라는 이 명령어를 ‘기본 동작 억제기’라고 부르기로 했다.

“맞아. 만드는 것, 붙이는 것, 듣는 것, 정보를 읽는 것, 그리고 이제 ‘방해를 막는 것’까지. 필요한 부품은 전부 모였네.”

루나의 말에 솔라는 고개를 끄덕였다. 눈앞에는 그동안 하나씩 모아온 도구들이 흩어져 있었다. createElement, appendChild, addEventListener, event.target.value, 그리고 방금 얻은 preventDefault.

이제 이 모든 부품을 손에 쥐고 하나의 잘 짜인 기계를 조립할 차례였다. 사용자 입력을 받아, 새로고침 없이, 화면에 새로운 요소를 착착 추가하고, 다음 입력을 위해 입력창까지 깨끗하게 비워주는 완벽한 흐름. 솔라의 머릿속에 마지막 퍼즐의 그림이 그려지기 시작했다.

5장: 흐름 ‘통합하기’: 동적인 화면 갱신 완성

솔라의 책상 위는 마치 정밀 기계를 조립하기 전의 작업대 같았다. 모니터 한편에 띄워놓은 메모장에는 그동안 솔라가 하나씩 모아온 도구들의 이름이 적혀 있었다.

  • createElement: 부품 만들기
  • appendChild: 화면에 붙이기
  • addEventListener: 행동 듣기
  • event.target.value: 정보 읽기
  • preventDefault: 방해 막기

흩어져 있는 부품들. 각각의 역할은 이제 명확했다. 하지만 솔라는 선뜻 키보드에 손을 올리지 못했다. 머릿속에서는 각 부품의 쓰임새가 선명했지만, 이것들을 어떤 순서로 조립해야 하나의 매끄러운 기계, 즉 사용자가 입력한 할 일을 목록에 착착 추가하는 그 완벽한 흐름을 만들 수 있을지 막막했다. 모든 것을 한 번에 적용하려니, 다시 처음처럼 모든 것이 뒤섞이는 기분이었다.

그때 옆에서 지켜보던 루나가 아무 말 없이 솔라의 코드 편집기로 손을 뻗었다. 그리고 이전에 만들었던 handleSubmit 함수를 가져와 내용을 모두 지우고, 그 안에 마치 설계도처럼 주석으로 된 뼈대만 남겨두었다.

function handleSubmit(event) {
  // 1. 브라우저의 기본 동작 막기
  // 2. 입력창에서 사용자가 입력한 값 가져오기
  // 3. 목록(li) 요소 새로 만들기
  // 4. 새로 만든 목록 요소에 입력값 채워 넣기
  // 5. 완성된 목록 요소를 화면의 리스트(ul)에 추가하기
  // 6. 다음 입력을 위해 입력창 비우기
}

복잡한 코드 대신 나타난 것은 너무나도 명료한 여섯 단계의 한글 설명이었다. 솔라는 자신도 모르게 숨을 잠시 멈췄다. 뒤죽박죽 얽혀 있던 머릿속이 이 여섯 줄의 문장으로 인해 가지런히 정돈되는 느낌이었다. 이건 코드를 짜는 것이 아니라, 빈칸을 채우는 과제처럼 보였다.

“아…”

짧은 탄성과 함께 솔라의 손가락이 움직이기 시작했다.

“1번, 브라우저의 기본 동작 막기. 이건 이제 내 거지.”

솔라는 망설임 없이 event.preventDefault();를 첫 번째 주석 아래에 적어 넣었다. 브라우저와의 대화에서 주도권을 잡는, 이제는 익숙해진 명령어였다.

“2번, 입력값 가져오기. 이것도 이벤트 보고서에서 찾으면 돼.”

const inputValue = event.target.querySelector('input').value;

솔라는 form 자체에서 이벤트를 잡았으므로, event.target이 form이고 그 안의 input을 찾아야 한다는 것까지 정확히 기억해냈다.

다음은 3, 4, 5번. 이것은 가장 처음 배웠던, 모든 것의 시작이었다.

// 3. 목록(li) 요소 새로 만들기
const newLi = document.createElement('li');

// 4. 새로 만든 목록 요소에 입력값 채워 넣기
newLi.textContent = inputValue;

// 5. 완성된 목록 요소를 화면의 리스트(ul)에 추가하기
document.querySelector('ul').appendChild(newLi);

하나의 흐름처럼 코드를 작성하고 나자, 솔라는 잠시 멈칫했다. 마지막 6번, ‘입력창 비우기’. 이것은 생각지 못했던 부분이었다. 하지만 왜 필요한지는 바로 알 수 있었다. 할 일을 추가하고 나서도 입력창에 글자가 그대로 남아있으면 불편할 테니까. 사용자를 위한 마지막 배려였다.

“값을 가져왔던 바로 그곳을 청소해주면 되겠네.”

솔라는 입력창을 가리키는 변수를 하나 만들고, 그 변수의 value를 빈 문자열로 만드는 코드를 추가했다.

event.target.querySelector('input').value = '';

모든 빈칸이 채워졌다. 여섯 단계의 설계도는 이제 완벽한 하나의 자바스크립트 함수가 되어 있었다. 솔라는 떨리는 마음으로 브라우저를 열고 입력창에 ‘첫 투두 리스트 완성!’이라고 입력한 뒤 엔터 키를 힘차게 눌렀다.

화면은 번쩍이지 않았다. 대신, 부드럽고 조용하게 ‘첫 투두 리스트 완성!’이라는 글자가 담긴 새로운 목록이 화면에 나타났다. 그리고 동시에, 입력창은 깨끗하게 비워져 다음 입력을 기다리고 있었다. 마치 잘 훈련된 로봇처럼, 솔라가 설계한 흐름 그대로 완벽하게 움직였다.

솔라는 자신이 작성한 코드를 처음부터 끝까지 천천히 다시 훑어보았다.

이벤트가 발생하면(submit), 가장 먼저 브라우저의 기본 동작을 막고(preventDefault), 이벤트가 발생한 곳에서 필요한 정보(value)를 꺼낸다. 그 정보로 새로운 화면 조각(createElement)을 만들고 내용을 채운 뒤(textContent), 화면의 제자리(ul)에 붙인다(appendChild). 마지막으로 다음 작업을 위해 주변을 정리한다(value = '').

“이게… 흐름이구나.”

솔라가 나지막이 중얼거렸다. 더 이상 뒤죽박죽 엉킨 실타래가 아니었다. 사용자의 행동에서 시작해 화면의 변화로 끝나는, 명확한 데이터와 명령의 흐름. 그제야 솔라는 처음 자신을 혼란에 빠뜨렸던 그 복잡한 예제 코드를 다시 보고 싶어졌다.

솔라는 처음 이 모든 것을 시작하게 했던 ‘투두 리스트 만들기’ 예제 파일을 열었다. 전에는 외계어처럼 보였던 코드 덩어리가 이제는 너무나 명확하게 읽혔다.

“여기서 addEventListener로 듣기 시작하고… 여기서 preventDefault로 새로고침을 막았네. 아, 이 부분에서 createElementli를 만들고, appendChild로 붙이는구나. 전부 내가 방금 짰던 코드랑 똑같은 순서야!”

솔라의 손가락이 화면의 코드를 하나하나 짚어갈 때마다, 혼란의 안개는 걷히고 명확한 길만이 드러났다. 루나는 아무 말 없이 동생의 모습을 지켜보며 조용히 미소 지었다. 마술의 비밀을 파헤친 것이 아니었다. 솔라는 이제 자기 손으로 직접, 화면에 생명을 불어넣는 그 ‘흐름’을 설계하는 방법을 깨우친 것이다.