Frontend 16
바닐라 JS 게시판: 데이터와 화면은 왜 따로 움직일까?
posts 배열과 DOM 화면이 따로 움직이는 것 같고, 글 추가나 삭제 뒤 왜 render 함수를 다시 부르는지 모르겠다.
근거 · 교안 p73-p80
1장: 데이터는 왜 혼자 바뀌나요?
솔라는 노트북 화면에 떠 있는 간단한 게시판 페이지를 보며 미간을 찌푸렸다. 화면에는 ‘첫 번째 글’, ‘두 번째 글’ 같은 제목의 게시물 몇 개가 목록으로 나란히 표시되어 있었다. 그 옆에 열어 둔 코드 편집기에는 posts라는 이름의 자바스크립트 배열이 보였다. 모든 게 분명해 보였다. 이 배열이 화면에 보이는 게시글 목록의 데이터일 것이다.
“이상하다…”
솔라는 혼잣말을 중얼거리며 키보드에 손을 올렸다. 무언가 확인해 보려는 듯, 브라우저의 개발자 도구 콘솔 창을 열었다. 이미 화면에 보이는 게시글 데이터가 담긴 posts 배열의 내용을 확인하고는, 새로운 글을 추가하는 명령어를 직접 입력했다.
posts.push({ id: 4, title: '새로운 글!', content: '테스트 중' });
엔터 키를 누르자 콘솔은 새 데이터가 성공적으로 추가되었음을 알렸다. 솔라는 posts 배열을 다시 출력해 보았다. 분명히 방금 추가한 { id: 4, ... } 객체가 배열의 마지막에 들어 있었다. 데이터는 완벽하게 바뀌었다.
솔라는 기대에 찬 눈으로 게시판 페이지를 다시 쳐다봤다. 하지만 화면은 꿈쩍도 하지 않았다. 여전히 원래 있던 게시글 세 개만 보일 뿐, 방금 추가한 ‘새로운 글!’은 어디에도 없었다.
“왜 안 나오지? 배열에는 분명히 글이 추가됐는데.”
솔라의 목소리에는 실망과 의아함이 뒤섞여 있었다. 마치 재료를 전부 냄비에 넣었는데 저절로 요리가 완성되기를 기다리는 사람 같았다.
그때 조용히 다가와 노트북 화면을 들여다보던 언니 루나가 나지막이 물었다.
“뭘 확인하고 있었어?”
“아, 언니. 이 게시판 코드 말이야. posts라는 배열에 글 데이터를 넣어두고 화면에 보여주는 간단한 구조거든. 그래서 방금 콘솔에서 posts 배열에 새 글을 하나 추가해 봤어. 배열은 바뀌었는데, 화면은 그대로야. 얘네 둘이 따로 노는 것 같아.”
솔라는 마치 고장 난 장난감을 설명하듯 말했다. 배열을 바꾸면 화면도 당연히 따라 바뀌어야 하는 것 아닌가? 마치 한 몸처럼.
루나는 고개를 끄덕이며 솔라의 노트북 화면을 가리켰다.
“그럼 반대로 한번 해볼래? 배열에서 글을 하나 지워봐.”
솔라는 루나의 말에 posts.pop() 명령어를 콘솔에 입력했다. 배열의 마지막 요소, 즉 방금 추가했던 ‘새로운 글!’이 제거되었다. 물론, 화면에는 처음부터 보이지도 않았던 글이라 아무 변화가 없었다. 솔라는 한 번 더 pop()을 실행했다. 이제 배열에는 원래 있던 게시글 중 마지막 글도 사라지고 두 개만 남았다.
posts 배열을 다시 확인하자 정말 글 두 개만 들어 있었다. 하지만 솔라의 눈앞에 있는 웹페이지에는 여전히 세 개의 게시글이 그대로 보였다.
“이것 봐. 데이터는 두 개뿐인데 화면에는 세 개가 있어. 정말 제멋대로네.”
솔라는 허탈한 웃음을 터뜨렸다.
루나는 말없이 화면 속 posts 배열과 웹페이지의 게시글 목록을 손가락으로 번갈아 가리켰다. 하나는 코드와 데이터의 세계에, 다른 하나는 사용자가 보는 시각적인 세계에 있었다. 둘은 연결되어 있지만, 같은 존재는 아니었다.
그 순간, 솔라는 무언가 깨달은 듯 입을 열었다.
“아… 알겠다. 이 posts 배열은 그냥… 설계도 같은 거구나.”
“설계도?”
“응. 이 배열은 ‘게시글 목록은 이렇게 생겨야 한다’는 정보를 가진 종이 뭉치인 거야. 내가 방금 한 일은 그 종이 뭉치에 새로운 줄을 추가하거나 지운 거고. 그렇다고 해서 이미 지어진 건물이 저절로 바뀌진 않잖아. 누군가 이 바뀐 설계도를 보고 건물을 부수고 새로 지어야 하는 것처럼.”
솔라의 눈빛이 달라져 있었다. 처음의 막막한 혼란 대신, 문제의 구조를 파악한 사람의 명료함이 보였다. 데이터와 화면이 ‘따로 움직이는’ 게 아니었다. 원래부터 ‘따로 있는’ 두 개의 다른 존재였던 것이다. 자바스크립트 배열은 메모리 속 데이터 묶음일 뿐이고, 화면에 보이는 네모난 글 목록은 브라우저가 그려낸 그림이었다.
“데이터를 바꾼 건 그냥 데이터만 바꾼 거구나. 화면에 ‘이제 이 데이터대로 다시 그려!’라고 말을 안 해줬으니까, 화면은 옛날 모습 그대로 있는 거고.”
솔라는 자신이 내린 결론을 확인하듯 루나를 보았다. 루나는 그저 작게 고개를 끄덕였다. 정답을 알려주지 않았지만, 솔라는 스스로의 실험과 비유를 통해 문제의 핵심에 도달했다. 데이터의 변경이 화면의 변경을 자동으로 보장하지 않는다는 사실. 둘 사이에는 명시적인 ‘그리기’라는 과정이 반드시 필요하다는 것을.
문득 솔라의 머릿속에 새로운 질문이 떠올랐다. 이제 둘이 별개라는 건 알겠다. 그럼 그 둘을 어떻게 연결해야 할까?
“그렇다면… 바뀐 설계도로 건물을 다시 짓게 하려면 어떻게 해야 해? 그 ‘다시 그려!’라는 말은 어떻게 하는 거지?”
2장: 화면은 왜 다시 그려야 하나요?
솔라는 이전의 실패에 굴하지 않고 다시 노트북 앞으로 돌아왔다. 화면 한쪽에는 여전히 세 개의 게시글이, 다른 한쪽에는 코드 편집기가 열려 있었다. 지난번의 실험으로 데이터(posts 배열)와 화면(DOM)이 별개의 존재라는 ‘설계도와 건물’의 비유를 얻었지만, 여전히 풀리지 않는 질문이 그녀를 붙들고 있었다. ‘어떻게 하면 바뀐 설계도로 건물을 다시 짓게 할까?’
그녀는 코드 파일을 뒤적이다가 renderPosts라는 이름의 함수를 발견했다. 이름부터 ‘게시글들을 그린다’는 뜻을 노골적으로 드러내고 있었다. 이거다 싶었다. 솔라는 지난번처럼 개발자 도구 콘솔에서 posts 배열에 새 글을 추가했다.
posts.push({ id: 4, title: '네 번째 글', content: '이번엔 성공할까?' });
예상대로 화면에는 아무 변화도 없었다. 데이터 설계도만 바뀌었을 뿐, 건물은 옛날 모습 그대로였다. 이제 솔라는 당황하지 않았다. 대신, 그녀는 침착하게 다음 명령어를 콘솔에 입력했다. 발견한 함수의 이름 그대로였다.
renderPosts();
엔터 키를 누르는 순간, 마법 같은 일이 일어났다. 게시판 목록이 번쩍이더니, 마지막 줄에 ‘네 번째 글’이 나타났다. 솔라는 환호성을 질렀다.
“됐다! 이게 바로 ‘다시 그려!’라는 명령어였어!”
솔라는 의기양양하게 옆에서 지켜보던 루나를 향해 말했다.
“이 renderPosts 함수, 되게 똑똑한 것 같아. 내가 posts 배열에 뭘 추가했는지 알아채고, 화면에 딱 그것만 쏙 추가해 주네. 마치 부족한 부분만 콕 집어서 보수 공사하는 것처럼 말이야.”
솔라의 추측은 그럴듯했다. 그게 가장 효율적인 방법처럼 보였다. 이미 지어진 건물 일부만 고치는 게, 통째로 부수고 새로 짓는 것보다 훨씬 빠르고 경제적일 테니까.
루나는 솔라의 말에 대답 대신, 마우스 커서로 renderPosts 함수가 정의된 코드 블록을 가리켰다.
“정말 그런지 한번 안을 들여다볼까? 그 똑똑하다는 일꾼이 어떻게 일하는지.”
솔라는 루나가 가리킨 곳으로 시선을 옮겼다. 함수 안에는 코드가 몇 줄 없었다. 그녀는 코드를 천천히 소리 내어 읽어 내려갔다.
function renderPosts() {
const postsContainer = document.querySelector('.posts-container');
postsContainer.innerHTML = ''; // 여기?
posts.forEach(post => {
const postElement = document.createElement('div');
postElement.textContent = post.title;
postsContainer.appendChild(postElement);
});
}
첫 줄은 게시글 목록을 담을 부모 요소를 찾는 평범한 코드였다. 그런데 바로 다음 줄에서 솔라의 눈이 동그래졌다.
postsContainer.innerHTML = '';
“잠깐만… innerHTML을 빈 문자열로 만든다고? 이건… 컨테이너 안의 모든 내용을 지워버리는 거잖아?”
마치 공사 현장에 불도저가 들어와 기존 건물을 전부 밀어버리는 장면이 머릿속에 그려졌다. 솔라가 생각했던 ‘변경된 부분만 쏙 골라내는 스마트한 방식’과는 거리가 멀었다. 오히려 가장 무식하고 과격한 방법이었다.
그녀는 충격에 휩싸여 다음 코드를 마저 읽었다. forEach 반복문은 posts 배열에 있는 모든 요소를 처음부터 끝까지 하나씩 순회하고 있었다. 그리고 각 요소마다 새로운 div 태그를 만들어 화면에 추가했다. 새로 추가된 ‘네 번째 글’만이 아니라, 원래 있던 ‘첫 번째 글’, ‘두 번째 글’, ‘세 번째 글’까지 전부 다.
“아…”
솔라는 짧은 탄식을 내뱉었다. 자신이 완전히 잘못 짚었다는 것을 깨달았다. renderPosts 함수는 똑똑한 보수 전문가가 아니었다. 그 정체는 기존 건물을 미련 없이 파괴하고, 설계도를 처음부터 끝까지 다시 읽어 새 건물을 올리는 무대뽀 재건축 시공팀에 가까웠다.
“변경점을 찾는 게 아니었어. 그냥… 전부 지우고, 처음부터 다시 그리는 거였구나.”
솔라는 허탈하게 웃으며 말했다. 왠지 속은 기분까지 들었다. 하지만 잠시 생각에 잠기자, 이 ‘무식한’ 방식이 왜 가장 확실한 방법인지 이해되기 시작했다.
데이터와 화면, 이 두 개의 다른 세계를 완벽하게 똑같이 맞추는 방법. 그것은 ‘어디가 달라졌지?’를 비교하는 복잡한 과정이 아니었다. 그냥 한쪽(화면)을 완전히 백지로 만든 뒤, 다른 한쪽(데이터)을 그대로 베껴서 새로 그리는 것이었다. 그러면 실수나 불일치가 끼어들 틈이 없었다.
“데이터가 원본이고 화면은 사본인 거네. 사본이 원본과 조금이라도 달라질 여지가 생기면, 그냥 낡은 사본은 찢어버리고 원본을 보며 새 사본을 만드는 거야. 매번.”
솔라는 ‘DOM 완전 재구성’이라는 개념을 스스로의 언어로 정리했다. 데이터가 바뀔 때마다 화면 전체를 새로 그리는 이 패턴. 이제 게시판 코드가 왜 그렇게 작성되었는지 명확하게 보였다.
그녀는 방금 전의 실험을 다시 머릿속으로 복기했다. 데이터를 바꾸고, renderPosts를 호출했다. 이 두 단계의 조합이 핵심이었다.
“알겠어. 데이터를 바꾸는 것과 화면을 다시 그리는 건 한 세트구나. 그런데… 지금은 내가 콘솔에 직접 renderPosts()를 쳤잖아. 사용자가 글을 쓸 때마다 개발자 도구를 열어서 이걸 치라고 할 순 없는데. 버튼을 누르면 이 모든 게 저절로 일어나게 하려면 어떻게 해야 하지?”
3장: 데이터-렌더링 루프 이해하기
솔라의 시선은 더 이상 개발자 도구의 콘솔 창에 머물러 있지 않았다. 그녀의 질문은 이제 ‘어떻게?’의 영역으로 넘어왔다. 사용자가 직접 코드를 치지 않고도, 버튼 클릭 같은 자연스러운 행동으로 데이터 변경과 화면 그리기를 한 번에 일으키는 방법. 그것이 궁금했다. 그녀는 코드 편집기에서 게시판의 HTML 파일을 열었다. 화면에는 익숙한 입력 창과 ‘글쓰기’ 버튼이 마크업 코드로 존재했다.
<form id="new-post-form">
<input type="text" id="post-title" placeholder="제목">
<button type="submit">글쓰기</button>
</form>
바로 이 부분이었다. 사용자가 새 글을 쓰는 바로 그 지점. 솔라는 이어서 자바스크립트 파일을 열고 new-post-form이라는 ID를 검색했다. 곧바로 양식을 제출(submit)했을 때 어떤 일이 일어나는지를 정의하는 코드 뭉치를 찾아냈다. 이전까지는 막연한 마법처럼 보였던 부분이었다.
“이게… ‘글쓰기’ 버튼을 눌렀을 때 일어나는 일들을 적어둔 거구나.”
솔라는 혼잣말을 하며 코드 블록을 뚫어지라 쳐다봤다. 옆에서 지켜보던 루나가 조용히 물었다.
“그 함수가 하는 일을 순서대로 한번 말해볼래?”
솔라는 루나의 말에 따라 함수의 첫 줄부터 천천히 눈으로 따라가며 소리 내어 읽기 시작했다.
const form = document.querySelector('#new-post-form');
form.addEventListener('submit', (event) => {
event.preventDefault(); // 1. 폼 제출 시 새로고침 방지
const titleInput = document.querySelector('#post-title');
const newTitle = titleInput.value; // 2. 입력된 글 제목 가져오기
// 3. 새 게시글 데이터 만들기
const newPost = { id: Date.now(), title: newTitle, content: '' };
posts.push(newPost); // 4. 'posts' 배열에 데이터 추가하기
renderPosts(); // 5. 화면 다시 그리기
titleInput.value = ''; // 6. 입력 창 비우기
});
코드를 한 줄 한 줄 따라가던 솔라의 눈이 4번과 5번 라인에서 멈췄다. 그리고 순간, 모든 것이 연결되는 듯한 표정을 지었다.
“어? 이거…!”
그녀는 놀라움에 말을 잇지 못했다. 방금 전까지 자신이 콘솔 창에서 수동으로 했던 두 가지 작업이 바로 거기에, 나란히 적혀 있었다. posts.push()로 데이터 배열을 바꾸고, renderPosts()로 화면을 다시 그리는 것. 마법 같은 자동화가 아니었다. 그냥 순서대로 실행되도록 약속된 명령어들의 묶음일 뿐이었다.
“내가 콘솔에서 했던 거랑 똑같잖아! posts 배열에 새 글을 밀어 넣고, 곧바로 renderPosts 함수를 불러서 화면을 새로고침하고. 버튼을 누르는 건 그냥 이 두 가지 명령을 한꺼번에 실행시키는 방아쇠였던 거야.”
솔라는 허탈하면서도 후련한 웃음을 터뜨렸다. ‘데이터 변경’과 ‘화면 렌더링’은 별개의 작업이지만, 항상 붙어 다녀야 하는 한 쌍이었다. 마치 신발의 왼쪽과 오른쪽처럼. 개발자는 사용자의 행동(버튼 클릭)이 있을 때마다 이 한 쌍의 작업을 순서대로 실행되도록 코드를 작성해 둔 것이었다.
“알겠다. 이게 정해진 약속 같은 거구나. 언제나 ‘데이터 먼저, 렌더링 나중’. 이게 한 세트인 거야.”
스스로 내린 결론에 고개를 끄덕이던 솔라의 눈에 게시글마다 붙어 있는 ‘삭제’ 버튼이 들어왔다. 그녀는 루나를 보지 않은 채, 마치 스스로에게 퀴즈를 내듯 말했다.
“그럼 저 삭제 버튼도 마찬가지겠지. 저 버튼을 누르면, 먼저 posts 배열에서 해당 글 데이터를 찾아 지우고, 그 다음에 renderPosts()를 또 불러서 없어진 상태의 목록을 다시 그리겠지?”
그녀의 목소리에는 이제 확신이 담겨 있었다. 루나는 대답 대신, 삭제 기능이 구현된 코드 부분을 화면에 띄워주었다. 솔라의 예측 그대로였다. 함수는 클릭된 게시물의 ID를 이용해 posts 배열에서 해당 요소를 걸러내어 새로운 배열을 만든 뒤, 어김없이 마지막 줄에서 renderPosts()를 호출하고 있었다.
솔라는 잠시 생각에 잠기더니, 완전히 자기 것이 된 개념을 시험해 보고 싶다는 듯 루나에게 새로운 질문을 던졌다.
“언니, 만약에 여기에 ‘전체 삭제’ 버튼을 하나 만든다면 말이야.”
“응.”
“그럼 그 버튼에 연결될 함수 안에는 딱 두 줄만 있으면 되는 거 맞지? 먼저 posts = []; 이렇게 해서 데이터 배열을 통째로 비워버리고, 그 다음 줄에 바로 renderPosts();를 호출하는 거야. 그럼 깨끗한 화면이 나오겠지.”
솔라는 더 이상 루나의 확인을 구하지 않았다. 그것은 질문의 형태를 한 선언이었다. 데이터와 화면이 따로 움직여서 혼란스러웠던 처음의 솔라는 이제 없었다. 대신 데이터라는 원본 설계도를 먼저 수정한 뒤, 화면이라는 사본을 새로 찍어내는 명확한 규칙을 이해하고, 그것을 새로운 기능에 적용할 줄 아는 개발자가 서 있었다. 그녀는 노트북 화면의 ‘글쓰기’ 버튼을 보며 작게 미소 지었다. 이제 저 버튼이 더 이상 마법처럼 보이지 않았다.