Frontend 17
바닐라 게시판의 한계와 React로 넘어가는 이유
바닐라 JavaScript로도 게시판이 되는데 왜 React가 필요한지, 무엇이 불편해서 넘어가는지 아직 설득되지 않는다.
근거 · 교안 p81-p83
1장: 바닐라 JS 게시판, 정말 괜찮을까?
솔라는 의자 등받이에 몸을 깊게 묻으며 만족스러운 한숨을 내쉬었다. 화면에는 며칠간 공들여 만든 게시판이 반듯하게 떠 있었다. 게시글 목록, 글쓰기 버튼, 간단한 제목과 내용. 순수한 자바스크립트, 소위 ‘바닐라 JS’로만 완성한 결과물이었다.
“언니, 내 것 좀 봐봐. 다들 왜 그렇게 React, React 하는지 모르겠어. 바닐라 JS로도 이렇게 멀쩡한 게시판이 뚝딱 만들어지는데.”
노트북 화면에서 눈을 떼지 않은 채, 솔라가 옆자리의 루나에게 말을 걸었다. 그녀의 목소리에는 자신의 결과물에 대한 확신이 가득했다.
루나는 잠시 하던 일을 멈추고 솔라의 화면으로 시선을 옮겼다. 간결하게 구성된 게시판을 잠시 살펴본 루나는 고개를 끄덕였다.
“깔끔하네. 새 글도 한번 써봐.”
솔라는 신이 나서 ‘글쓰기’ 버튼을 누르고 제목과 내용을 입력했다. ‘저장’ 버튼을 클릭하자, 방금 작성한 글이 맨 위에 새로운 게시물로 추가되었다.
“봐, 완벽하게 작동하잖아. 데이터가 추가되면 화면에 바로 보여주는 거, 이게 게시판의 핵심 아냐? 이걸 굳이 복잡한 라이브러리를 써서 만들 이유가 있을까?”
“그 새 게시물이 화면에 나타나는 과정을 코드로 보여줄 수 있어?”
루나의 질문은 칭찬이나 반박 대신, 코드 자체로 향했다. 솔라는 자신만만하게 코드 에디터를 열어 새 게시물을 추가하는 함수 부분을 화면에 띄웠다.
function addPost(title, content) {
const postList = document.getElementById('post-list');
const postDiv = document.createElement('div');
postDiv.className = 'post';
const titleElement = document.createElement('h3');
titleElement.textContent = title;
const contentElement = document.createElement('p');
contentElement.textContent = content;
postDiv.appendChild(titleElement);
postDiv.appendChild(contentElement);
postList.prepend(postDiv);
}
“여기. addPost 함수. 제목이랑 내용을 받아서 div 만들고, h3랑 p 태그도 만들어서 내용 채워 넣고. 마지막에 appendChild로 착착 붙여주면 끝. 간단하지.”
“응, 간단하네.”
루나는 짧게 동의하며 솔라의 화면을 응시했다.
“만약 모든 게시물에 작성자 이름, 작성일, 그리고 ‘좋아요’ 버튼을 추가해야 한다면 어떨까?”
“그것도 간단하지. createElement 몇 줄 더 쓰면 되잖아.”
솔라는 즉시 대답했지만, 머릿속으로는 코드를 수정하는 과정을 그려보고 있었다. div, h3, p에 더해서… 작성자 이름을 담을 span 태그, 작성일을 담을 또 다른 span 태그, 그리고 ‘좋아요’ 버튼을 위한 button 태그를 만들어야 한다. 버튼 안에는 ‘좋아요’ 아이콘과 숫자 카운트를 위한 span 태그가 또 필요할지도 모른다.
솔라의 손가락이 키보드 위에서 잠시 멈칫했다.
document.createElement('span'), document.createElement('button'), document.createElement('i')… 생각의 흐름을 따라 코드를 떠올리자, appendChild의 행렬이 머릿속에 길게 늘어섰다. 새로 만든 요소들을 올바른 순서와 구조로 조립하는 코드들이었다.
“…postDiv 안에 작성자 정보를 담을 authorDiv를 먼저 만들고, 그 안에 이름 span이랑 날짜 span을 appendChild 해야겠네. 그리고 contentElement 다음에는 button을 appendChild 하고…”
혼잣말처럼 중얼거리던 솔라의 미간이 살짝 좁아졌다. 불가능한 작업은 아니었다. 하지만 ‘데이터’인 작성자 이름, 작성일, 좋아요 수를 화면에 ‘표시’하기 위해 매번 이렇게 HTML 구조를 직접 손으로 짜 맞춰야 한다는 사실이 새삼스레 다가왔다. 마치 가구를 조립할 때마다 매번 목재소에 가서 나무를 자르고, 나사못을 하나하나 조여야 하는 기분이었다.
처음에 느꼈던 ‘간단함’은 어느새 ‘번거로움’으로 바뀌어 있었다.
“음… 확실히 추가할 게 많아지니까… 하나라도 빠뜨리거나 순서가 틀리면 화면이 바로 깨지겠네. 데이터를 화면에 보여주기만 하면 되는데, 그 과정을 내가 너무 세세하게 지시하고 있는 느낌이야.”
솔라는 방금 전의 자신만만했던 표정을 거두고 자신의 코드를 다시 바라보았다. 데이터가 바뀔 때마다 DOM 요소를 생성하고, 조립하고, 화면에 삽입하는 이 수동적인 과정. 정적인 페이지라면 모를까, 게시판처럼 데이터가 계속 변하는 동적인 UI에서는 이 작업이 생각보다 훨씬 귀찮고 성가신 일이라는 것을 깨달았다.
루나는 솔라의 표정 변화를 조용히 지켜보고 있었다. 솔라가 마침내 자신의 코드에서 ‘번거로움’이라는 새로운 감각을 발견한 순간이었다.
그때, 솔라의 머릿속에 방금 자신이 추가하려던 ‘좋아요’ 버튼이 스쳐 지나갔다. 새로운 의문이 꼬리를 물고 피어올랐다.
“잠깐만. 그럼 내가 이렇게 새로 DOM을 만들어서 목록에 추가하잖아. 그럼 기존에 있던 게시물에 달려있던 ‘좋아요’ 버튼은? 내가 버튼에 클릭 이벤트를 걸어놨을 텐데, 새 글이 추가되면서 목록이 통째로 바뀐다면… 그 이벤트들은 어떻게 되는 거지?”
2장: 새로고침 없는 게시판, 이벤트는 어디로?
솔라는 이전의 고민을 행동으로 옮기기 시작했다. ‘좋아요’ 버튼에 대한 의문은 코드로 직접 확인해보는 것이 가장 빨랐다. 그녀는 기존 게시물 데이터를 순회하며 화면을 그리는 함수에 ‘좋아요’ 버튼을 추가하는 코드를 재빨리 타이핑했다.
// ... 기존 코드 ...
const likeButton = document.createElement('button');
likeButton.textContent = '좋아요 ♡ 0';
likeButton.addEventListener('click', () => {
console.log('좋아요 버튼 클릭됨!');
});
postDiv.appendChild(likeButton);
// ... 기존 코드 ...
초기 게시물 목록이 화면에 그려지자, 각 게시물 아래에 ‘좋아요 ♡ 0’ 버튼이 예쁘게 자리 잡았다. 솔라는 첫 번째 게시물의 ‘좋아요’ 버튼을 망설임 없이 클릭했다. 브라우저 개발자 도구의 콘솔 창에 ‘좋아요 버튼 클릭됨!’이라는 메시지가 선명하게 찍혔다. 두 번째, 세 번째 게시물의 버튼도 마찬가지였다. 일단은 모든 것이 의도대로 작동했다.
“흠, 일단 버튼에 이벤트를 다는 건 문제없네.”
안도와 의심이 섞인 목소리로 솔라가 혼잣말을 했다. 어쩌면 자신의 걱정이 기우였을지도 모른다고 생각한 순간이었다.
“그럼 이제, 아까처럼 새 글을 추가했을 때도 기존 버튼들이 잘 작동하는지 확인해봐야지.”
솔라는 다시 글쓰기 폼에 새로운 내용을 채워 넣고 ‘저장’ 버튼을 눌렀다. 새 글이 목록 최상단에 추가되면서, 화면의 게시물 목록 전체가 다시 그려져야 했다. 솔라는 지난번처럼 createElement를 반복하는 대신, 조금 더 ‘세련된’ 방법을 떠올렸다. 배열 데이터를 HTML 문자열로 바꾸어 한 번에 렌더링하는 방식이었다.
function renderPosts() {
const postListContainer = document.getElementById('post-list');
// 데이터 배열을 HTML 문자열로 변환
const postHtml = posts.map(post => `
<div class="post">
<h3>${post.title}</h3>
<p>${post.content}</p>
<button class="like-button">좋아요 ♡ ${post.likes}</button>
</div>
`).join('');
// innerHTML을 사용해 목록 전체를 한 번에 업데이트
postListContainer.innerHTML = postHtml;
}
“createElement를 여러 번 쓰는 것보다 map으로 HTML 문자열을 만들어서 innerHTML에 넣어주는 게 코드도 훨씬 깔끔하고 편하네.”
솔라는 자신의 코드에 만족하며 중얼거렸다. 화면은 완벽했다. 새로 추가된 게시물도, 기존에 있던 게시물들도 모두 올바른 순서로, ‘좋아요’ 버튼까지 달고 말끔하게 정렬되어 있었다. 이제 마지막 관문만 남았다. 솔라는 스크롤을 내려 원래 첫 번째였던, 이제는 두 번째가 된 게시물의 ‘좋아요’ 버튼으로 마우스를 가져갔다.
클릭.
아무 일도 일어나지 않았다.
콘솔 창은 텅 비어 있었다. 솔라는 당황해서 몇 번이고 버튼을 더 클릭해봤지만, 결과는 마찬가지였다. 새로 추가된 글의 버튼은 물론, 원래 잘 작동하던 모든 게시물의 ‘좋아요’ 버튼이 전부 먹통이 되어버렸다.
“어? 왜… 왜 안 되지? 분명 아까는 잘 됐는데. 버튼도 그대로 있고, 화면에도 멀쩡히 보이는데.”
솔라의 손가락이 키보드 위에서 길을 잃었다. 눈앞의 현상을 이해할 수 없었다. 그때, 조용히 지켜보던 루나가 솔라의 화면을 가리켰다.
“방금 목록을 업데이트할 때 쓴 코드가 뭐였지?”
“응? innerHTML… 아.”
솔라의 시선이 postListContainer.innerHTML = postHtml; 코드에 멎었다. innerHTML. 안의 HTML을 ‘대체’한다. 루나는 아무 말도 하지 않았지만, 솔라는 그 짧은 침묵 속에서 문제의 실마리를 발견했다.
innerHTML은 기존에 있던 DOM 요소들을 수정하는 것이 아니었다. 그저 컨테이너 안의 모든 것을 지워버리고, 새로운 HTML 문자열로 완전히 새로운 DOM 요소들을 만드는 것이었다. 눈에 보이는 버튼은 이전과 똑같은 모양과 텍스트를 가졌을지 몰라도, 그것은 조금 전까지 이벤트 리스너를 품고 있던 그 버튼이 아니었다. 껍데기만 같은, 완전히 새로 태어난 버튼이었다. 그리고 새로 태어난 버튼에는 아무도 addEventListener를 다시 걸어주지 않았다.
“사라졌구나… 내가 맨 처음에 addEventListener로 연결해줬던 버튼들은 innerHTML이 실행되는 순간 전부 사라지고, 이벤트 리스너가 없는 새 버튼들로 교체된 거구나.”
허탈한 깨달음이었다. 데이터가 바뀔 때마다 화면을 새로 그리는 것은 당연했다. 하지만 그럴 때마다 기존에 걸어두었던 모든 상호작용, 모든 이벤트 리스너들이 허공으로 사라져 버린다는 사실은 미처 생각하지 못했다.
이 문제를 해결하려면, renderPosts 함수가 호출될 때마다, 즉 목록이 다시 그려질 때마다 모든 ‘좋아요’ 버튼을 다시 찾아서 일일이 addEventListener를 또 걸어줘야만 했다.
솔라는 눈앞이 캄캄해지는 것을 느꼈다. 이전 장에서 느꼈던 ‘수동 DOM 생성의 번거로움’과는 차원이 다른, 또 다른 종류의 수고로움이 거대한 산처럼 다가왔다.
“그럼… 데이터가 바뀔 때마다, 게시물 목록을 통째로 다시 만드는 코드도 짜야 하고, 다 만들어진 다음엔 화면에 있는 모든 버튼을 다시 찾아서 이벤트 리스너도 처음부터 다시 달아줘야 한다는 거야?”
3장: 데이터와 화면의 ‘엇박자’를 맞추는 법
솔라의 커서는 깜빡임을 멈춘 채 코드의 한가운데에 얼어붙어 있었다. 그녀의 화면에는 이전 장에서 깨달은 문제를 해결하기 위한 처절한 사투의 흔적이 남아있었다. 게시물 목록을 innerHTML로 다시 그릴 때마다 이벤트 리스너가 사라지는 문제를 해결하기 위해, 그녀는 renderPosts 함수가 끝난 직후에 호출되는 attachLikeButtonListeners라는 새로운 함수를 만들었다.
function attachLikeButtonListeners() {
const buttons = document.querySelectorAll('.like-button');
buttons.forEach(button => {
button.addEventListener('click', () => {
console.log('좋아요 버튼 다시 연결됨!');
});
});
}
function renderPosts() {
// ... (innerHTML로 목록을 그리는 코드) ...
// 렌더링 후, 이벤트 리스너를 다시 연결
attachLikeButtonListeners();
}
이 코드는 작동했다. 새 글을 추가해도, 목록이 통째로 다시 그려져도, ‘좋아요’ 버튼은 매번 끈질기게 되살아나 제 기능을 했다. 하지만 솔라의 표정은 어두웠다. 이건 해결책이라기보다 임시방편, 욱여넣은 코드처럼 느껴졌다. 데이터가 바뀔 때마다, 매번 화면의 모든 요소를 파괴하고 새로 만든 뒤, 그 위를 다시 기어 다니며 기능들을 하나하나 재부여하는 방식이었다. 비효율적이고 불안정해 보였다.
루나는 솔라의 굳은 표정을 읽었다. 그녀는 솔라의 노트북 옆에 놓인 작은 메모지 한 장을 들어 솔라 쪽으로 밀었다. 메모지에는 두 개의 단어가 적혀 있었다. ‘파괴하고 재건하기’ vs ‘필요한 부분만 수리하기’.
솔라는 메모지를 쳐다보다가 피식 웃었다. 자신의 고민을 정확히 꿰뚫어 본 문구였다.
“맞아. 지금 내 코드는 멀쩡한 건물까지 다 부수고 처음부터 다시 짓는 거나 마찬가지야. 그냥 ‘좋아요’ 숫자 하나만 바꾸고 싶은데, 게시물 목록 전체를 갈아엎고 있잖아. ‘수리’만 할 수는 없을까?”
솔라는 마치 새로운 돌파구를 찾은 사람처럼 눈을 빛냈다.
“그래, 목록 전체를 다시 그리는 renderPosts를 호출하는 대신, 클릭된 특정 게시물의 ‘좋아요’ 숫자만 바꾸는 거야. 그러면 innerHTML을 쓸 필요도 없으니 이벤트 리스너가 사라질 일도 없잖아!”
새로운 아이디어가 떠오르자 솔라의 손가락이 다시 키보드 위에서 생기를 찾았다. 그녀는 ‘좋아요’ 버튼의 클릭 이벤트 핸들러를 수정하기 시작했다.
일단, 자바스크립트 데이터 배열에서 해당 게시물을 찾아 ‘좋아요’ 숫자를 1 늘린다. 이건 간단했다.
// posts 배열에서 id가 일치하는 게시물 데이터를 찾는다.
const postData = posts.find(p => p.id === postId);
postData.likes++; // 데이터의 '좋아요' 값을 1 증가시킨다.
“자, 이제 데이터는 바뀌었어.”
솔라는 콘솔에 postData.likes를 출력해보고 숫자가 제대로 증가한 것을 확인했다. 하지만 당연하게도, 화면의 ‘좋아요 ♡ 5’라는 숫자는 미동도 하지 않았다. 그녀의 learnerMisread대로, 데이터 변수를 바꾸는 것만으로 화면이 저절로 바뀌지는 않았다. 데이터의 세계와 화면(DOM)의 세계는 별개였다.
“이제 이 바뀐 숫자를… 화면에 직접 업데이트해줘야겠지.”
솔라는 방금 클릭한 버튼이 속한 게시물 div 안에서, ‘좋아요’ 숫자를 표시하는 span 요소를 찾아야 했다. 그녀는 각 게시물 요소에 data-id 속성을 부여하고, querySelector를 이용해 화면에서 올바른 요소를 정확히 찾아내는 코드를 추가했다.
function onLikeButtonClick(event) {
const postElement = event.target.closest('.post');
const postId = postElement.dataset.id;
// 1. 데이터 업데이트
const postData = posts.find(p => p.id === postId);
postData.likes++;
// 2. 화면(DOM) 업데이트
const likeCountSpan = postElement.querySelector('.like-count');
likeCountSpan.textContent = postData.likes; // span의 텍스트를 새 숫자로 교체
}
코드를 완성한 솔라는 다시 ‘좋아요’ 버튼을 클릭했다. 드디어, 숫자가 5에서 6으로, 6에서 7로 부드럽게 올라갔다. 목록 전체를 다시 그리지 않고도, 필요한 부분만 정확히 바뀌었다. 이벤트 리스너도 그대로 살아있었다.
“됐어! 이게 훨씬 낫네. innerHTML로 다 밀어버리는 것보다 훨씬 똑똑한 방법이야.”
솔라는 만족스러운 미소를 지었다. 하지만 그 미소는 오래가지 못했다. 그녀는 자신이 작성한 onLikeButtonClick 함수를 가만히 들여다보았다. 데이터 변경 로직과 화면 변경 로직이 한데 얽혀 있었다.
그때, 루나가 조용히 입을 열었다.
“만약 사용자가 게시물 제목을 수정하는 기능을 추가한다면, 그땐 어떤 코드가 필요할까?”
“제목 수정? 그것도 비슷하겠지. onTitleEdit 같은 함수를 만들어서… 사용자가 입력한 새 제목으로 posts 배열의 title 데이터를 바꾸고… 그런 다음엔, 화면에서 해당 게시물의 h3 태그를 찾아서 textContent를 새 제목으로 바꿔주는 코드를 짜야지.”
스스로 답하면서, 솔라는 깨달았다. ‘좋아요’를 누를 때, 제목을 수정할 때, 댓글을 추가할 때, 게시물을 삭제할 때… 데이터에 변화가 생기는 모든 순간마다, 개발자인 자신이 직접, ‘어떤 데이터를 바꾸고, 그에 따라 화면의 어떤 요소를, 어떻게 바꿀지’를 일일이 코드로 지정해주어야 했다.
만약 데이터 업데이트는 했는데, 화면 업데이트 코드를 빼먹는다면? 데이터와 화면은 서로 다른 값을 보여주는 ‘엇박자’ 상태가 될 것이다. 반대로 화면만 바꾸고 데이터 업데이트를 잊어도 마찬가지였다. 이 둘의 동기화를 맞추는 책임이 온전히 개발자에게 있었다.
이전의 ‘수동 DOM 생성’이나 ‘수동 이벤트 재연결’과는 또 다른 차원의 번거로움이었다. 그것은 끊임없이 데이터와 화면의 싱크를 맞춰야 하는, 지휘자와 같은 역할이었다. 한순간이라도 집중력을 잃으면 전체 오케스트라가 불협화음을 내는, 그런 아슬아슬한 지휘.
솔라는 키보드에서 손을 떼고 길게 한숨을 쉬었다.
“결국 똑같네. DOM을 새로 만들고 이벤트를 다시 다는 수고를 피했더니, 이제는 데이터가 바뀔 때마다 화면의 어느 부분을 바꿔야 할지 내가 직접 찾아서 명령해야 하는 수고가 생겼어. 데이터와 화면을 일치시키는 이 작업을 계속 내 손으로 해야 하는구나.”
‘수동 DOM 생성’, ‘수동 이벤트 재연결’, 그리고 이제 ‘데이터와 UI의 수동 동기화’까지. 솔라는 바닐라 자바스크립트로 동적인 웹을 만드는 것의 본질이, 끝없는 수동 작업의 연속이라는 것을 받아들일 수밖에 없었다. 그녀가 처음에 느꼈던 ‘이걸로도 충분하다’는 생각은 산산조각 나 있었다.
솔라는 고개를 들어 루나를 보았다. 그녀의 눈에는 새로운 질문이 떠올라 있었다. 지금까지 발견한 모든 문제들을 가리키는, 단 하나의 근본적인 질문이었다.
“알겠어, 언니. 뭐가 문제인지 이제 확실히 알겠어. 그럼 도대체 React는… 이 모든 귀찮은 수동 작업들을 어떻게 처리한다는 거야?”
4장: 이제 ‘무엇을’ 그릴지만 말하세요: 선언형 렌더링
솔라의 질문에 답하는 대신, 루나는 자신의 노트북 화면을 솔라 쪽으로 돌렸다. 솔라가 며칠 동안 씨름했던 익숙한 게시판 프로젝트 폴더가 아니었다. 화면에는 Post.jsx 라는 이름의 파일 하나가 열려 있었다. 내용은 간결했지만, 솔라가 작성했던 코드와는 사뭇 다른 모습이었다.
솔라는 눈을 가늘게 뜨고 낯선 코드를 읽어 내려갔다. HTML 같기도 하고, 자바스크립트 같기도 한 기묘한 구문이었다.
function Post() {
const [likes, setLikes] = useState(5);
function handleLikeClick() {
setLikes(likes + 1);
}
return (
<div className="post">
<h3>React 게시물</h3>
<p>이것은 React로 만든 게시물입니다.</p>
<button onClick={handleLikeClick}>
좋아요 ♡ {likes}
</button>
</div>
);
}
“이게… React야?”
“응. 네가 만든 ‘좋아요’ 버튼이랑 똑같은 기능을 하는 부분만 간단하게 만들어 봤어.”
솔라는 코드를 뚫어지라 쳐다봤다. useState(5) 라는 줄은 낯설었지만, likes라는 변수에 숫자 5를 저장하는 것이라고 어림짐작할 수 있었다. handleLikeClick 함수 안에는 setLikes(likes + 1)이라는 한 줄이 전부였다. 그녀가 작성했던 것처럼 event 객체를 받아오지도, querySelector로 특정 DOM 요소를 찾아 헤매지도 않았다. 그저 likes라는 변수의 값을 1 더하는 것처럼 보였다.
가장 이상한 부분은 return문 안에 있는 HTML처럼 생긴 코드였다. button 태그 안에는 좋아요 ♡ {likes}라고 적혀 있었다. 중괄호로 감싸인 likes가 변수 값을 보여주는 자리인 것 같았다.
“일단… 실행해 봐.”
루나가 말했다. 브라우저에 띄워진 화면에는 코드와 똑같이 생긴 게시물이 하나 덩그러니 놓여 있었다. 솔라는 조심스럽게 ‘좋아요 ♡ 5’ 버튼을 클릭했다.
화면의 숫자가 즉시 ‘6’으로 바뀌었다.
한 번 더 클릭하자 ‘7’이 되었다. 너무나 부드럽고 즉각적인 반응이었다. 솔라는 놀라서 다시 코드로 눈을 돌렸다.
“잠깐만. setLikes는 그냥 likes라는 변수 값을 바꾸는 거 아냐? 그런데 어떻게 화면의 숫자가 바뀐 거야? 내가 짰던 코드처럼 document.querySelector로 저 버튼 안에 있는 텍스트를 찾아서 textContent를 바꿔주는 부분이 없잖아. 어디에도 없어.”
솔라는 코드의 구석구석을 샅샅이 훑었다. DOM을 조작하는 명령어는 어디에도 보이지 않았다. 마치 마법 같았다. 데이터만 바꿨을 뿐인데, 화면이 저절로 따라 바뀌었다. 솔라가 겪었던 그 모든 번거로운 ‘수동 작업’들이 흔적도 없이 사라져 있었다.
그녀의 혼란스러운 표정을 보던 루나가 모니터 옆에 나란히 놓인 두 개의 포스트잇 중 하나를 가리켰다. 솔라가 방금 전까지 붙들고 있던 바닐라 자바스크립트 코드였다.
// 솔라의 코드: onLikeButtonClick 함수
function onLikeButtonClick(event) {
// 1. DOM에서 요소를 찾아라.
const postElement = event.target.closest('.post');
// 2. 데이터를 업데이트해라.
const postData = posts.find(p => p.id === postElement.dataset.id);
postData.likes++;
// 3. 다시 DOM에서 다른 요소를 찾아라.
const likeCountSpan = postElement.querySelector('.like-count');
// 4. 그 요소의 내용을 바꿔라.
likeCountSpan.textContent = postData.likes;
}
“네 코드를 다시 봐. ‘이 버튼을 찾아서’, ‘저 숫자를 가져와서 1을 더하고’, ‘다시 저 span 태그를 찾아서’, ‘그 안의 내용을 이걸로 바꿔라’고 하나하나 명령하고 있지. 이건 ‘어떻게’ 할지를 지시하는 명령형(Imperative) 방식이야.”
루나는 말을 마치고 다른 쪽 포스트잇, 방금 솔라가 본 React 코드를 가리켰다.
“그럼 이 코드는 어때? handleLikeClick 함수는 오직 ‘좋아요 숫자를 1 늘려줘’라는 요청만 하고 있어. 화면을 어떻게 바꿀지에 대한 명령은 없어. 대신 return 부분을 봐. ‘좋아요 숫자가 likes 값이라면, 버튼은 이런 모양이어야 한다’고 최종 모습만 정의하고 있지. 이건 ‘무엇을’ 원하는지 선언하는(Declarative) 방식이야.”
‘명령형’과 ‘선언형’.
두 단어가 솔라의 머릿속에 울렸다. 그녀는 두 개의 코드를 번갈아 보았다. 차이가 명확하게 느껴졌다. 자신의 코드는 웹 브라우저라는 부하 직원에게 일을 시키는 깐깐한 상사 같았다. 하나부터 열까지 세세하게 방법을 지시해야만 했다. 실수를 줄이려면 더 많은 명령이 필요했다.
하지만 React 코드는 달랐다. 마치 유능한 관리자에게 원하는 결과물의 사진을 보여주며 “이대로 만들어 줘”라고 말하는 것과 같았다. likes가 5일 때의 모습, 6일 때의 모습, 7일 때의 모습을 그저 선언했을 뿐이다. 그 결과물을 만들기 위해 DOM 요소를 새로 만들어야 하는지, 기존 요소의 내용만 바꿔야 하는지, 이벤트 리스너를 다시 붙여야 하는지에 대한 ‘어떻게’의 과정은 React가 알아서 처리했다.
솔라는 지난 며칠간 자신을 괴롭혔던 문제들을 떠올렸다.
‘수동 DOM 생성의 번거로움’, ‘수동 이벤트 재연결의 문제’, ‘데이터와 UI의 수동 동기화의 어려움’.
React의 선언형 방식 앞에서는 이 모든 문제들이 한꺼번에 녹아내리는 것 같았다. 개발자는 더 이상 DOM을 직접 만지며 화면을 조립하고 수리할 필요가 없었다. 그저 데이터라는 ‘설계도’만 잘 관리하면, React가 그 설계도에 맞는 ‘건물(UI)’을 자동으로 지어주고 유지 보수까지 해주는 셈이었다.
“아… 알겠다. 나는 계속 ‘어떻게’ 바꿀지를 고민했는데, React는 그냥 ‘데이터가 이러면 화면은 이래야 한다’는 규칙만 정해두면 되는 거구나. 내가 할 일은 데이터 상태를 바꾸는 것뿐이고, 귀찮은 DOM 작업은 React가 대신 해주는 거였어.”
자신이 왜 바닐라 JS에 한계를 느꼈는지, 그리고 React가 왜 필요한지에 대한 명확한 그림이 그려졌다. 단순히 코드가 짧아지는 차원의 문제가 아니었다. 개발의 패러다임 자체가 바뀌는 것이었다.
납득의 한숨을 내쉰 솔라의 눈에 다시 의문이 스쳤다.
“그런데 언니, React가 알아서 해준다는 건 알겠어. 근데 ‘어떻게’? 내가 innerHTML로 목록 전체를 갈아엎었을 때처럼, React도 데이터가 바뀔 때마다 화면 전체를 그냥 무식하게 지우고 새로 그리는 건 아니야? 만약 그렇다면, 그것도 결국 비효율적인 거 아냐?”
5장: 화면이 저절로 바뀌는 마법? React의 비결, 가상 DOM
솔라의 날카로운 질문에 루나는 대답 대신 행동을 택했다. 그녀는 솔라의 브라우저에서 확장 프로그램 스토어를 열고, 검색창에 ‘React Developer Tools’라고 입력했다. 잠시 후, 파란색 원자 모양의 아이콘이 화면에 나타났다. 루나는 그것을 설치하고 브라우저를 새로고침했다.
“개발자 도구를 다시 열어봐.”
솔라가 F12 키를 누르자, 익숙한 개발자 도구 창이 떴다. 하지만 이전과는 미묘하게 달랐다. ‘Elements’, ‘Console’ 탭 옆으로, 이전에 없던 ‘Components’와 ‘Profiler’라는 새로운 탭 두 개가 생겨 있었다. 솔라는 의아한 표정으로 새로운 탭을 바라보았다. 이것이 방금 언니가 설치한 그것이었다.
“Components 탭을 누르고, 오른쪽 위에 있는 톱니바퀴 아이콘을 클릭해봐. 그리고 ‘Highlight updates when components render.’ 옵션을 켜.”
루나의 지시에 따라 솔라가 설정을 마치자, 화면에 별다른 변화는 없어 보였다. 루나는 다시 React로 만든 게시물 페이지를 가리켰다.
“이제, 아까처럼 ‘좋아요’ 버튼을 눌러봐.”
솔라는 반신반의하며 마우스를 움직여 ‘좋아요 ♡ 7’ 버튼을 클릭했다.
순간, 화면의 숫자가 ‘8’로 바뀌면서 아주 얇은 초록색 경계선이 버튼과 숫자 주변을 감싸고 반짝였다 사라졌다. 경계선은 정확히 그 부분만, 마치 외과 의사의 메스처럼, 정교하게 도려내듯 표시했다. 게시물의 제목이나 내용 부분에는 아무런 변화도 없었다. 개발자 도구의 ‘Components’ 탭에 표시된 <Post> 컴포넌트 트리에서도 likes 상태값만 바뀌었을 뿐, 다른 부분은 안정적이었다.
솔라는 눈을 동그랗게 떴다. 그녀의 머릿속을 지배하던 의심, React가 데이터를 바꿀 때마다 innerHTML처럼 화면 전체를 무식하게 갈아엎을 것이라는 오해가 안개처럼 흩어지는 순간이었다.
“전부… 다시 그리는 게 아니었어. 딱 바뀐 부분만, 저 숫자만 정확히 골라서 바꾸고 있잖아. 어떻게? 어떻게 이게 가능한 거야?”
“바로 그게 React의 핵심 비결이야.”
루나는 책상 위에 놓인 두 장의 빈 종이 중 하나를 집어 들었다.
“React는 우리가 보는 실제 화면, 즉 진짜 DOM에 직접 명령을 내리지 않아. 대신, 상태가 바뀔 때마다 메모리 안에다가 보이지 않는 가상의 DOM을 그려. 일종의 청사진이지.”
루나는 종이 위에 간단한 트리 구조를 그렸다. <div> 아래에 <h3>, <p>, <button>이 달린 모양이었다.
“네가 ‘좋아요’ 버튼을 눌러서 likes 값이 7에서 8로 바뀌면, React는 이 청사진을 새로 하나 더 만들어. likes 값이 8로 적용된 버전으로.”
루나는 옆에 있던 다른 종이에 거의 똑같은 트리 구조를 그리되, 버튼 안의 숫자만 ‘8’로 바꾸어 적었다. 이제 그녀의 앞에는 숫자만 다른 두 개의 청사진이 놓였다.
“그 다음, React는 이 두 개의 청사진을 비교하는 거야. 어디가 다른지, 아주 빠른 속도로 찾아내지.”
루나는 두 종이를 번갈아 보며 바뀐 숫자 ‘8’ 부분에 빨간 펜으로 동그라미를 쳤다.
“비교가 끝나면, React는 단 하나의 차이점, 즉 ‘button 태그 안의 텍스트가 7에서 8로 바뀌었다’는 정보만 알아내. 그리고 그제서야 실제 화면(DOM)으로 가서, 다른 곳은 전혀 건드리지 않고, 오직 그 텍스트 부분만 ‘8’로 바꾸는 최소한의 작업만 수행하는 거야. 이게 바로 가상 DOM(Virtual DOM) 이 하는 일이야.”
솔라는 숨을 죽인 채 루나의 설명을 들었다. 이제 모든 조각이 맞춰졌다.
자신이 바닐라 JS로 겪었던 그 모든 고통의 근원이 보였다. 데이터를 바꾸면 화면의 어디를 어떻게 바꿔야 할지 일일이 명령해야 했던 ‘수동 동기화’의 번거로움. React는 ‘청사진 비교’라는 자동화된 방식으로 그 문제를 해결하고 있었다. innerHTML처럼 모든 걸 부수고 다시 지어 성능을 낭비하지도 않았고, querySelector로 특정 요소를 찾아 헤매는 개발자의 수고를 요구하지도 않았다.
그녀가 3장에서 그토록 원했던 ‘필요한 부분만 수리하기’를, React는 설계 단계부터 품고 있었던 것이다.
‘수동 DOM 생성’, ‘수동 이벤트 재연결’, ‘데이터와 UI의 수동 동기화’. 자신을 괴롭혔던 문제들의 이름표를 하나씩 떠올렸다. 이 모든 수고는 결국 ‘데이터와 화면을 계속 수동으로 맞추는’ 복잡한 과정에서 파생된 것이었다. 가상 DOM은 바로 그 연결고리를 가장 효율적이고 영리하게 자동화하는 해결책이었다.
솔라는 고개를 끄덕였다. 더 이상의 질문은 없었다. 처음의 자신만만했던 확신에서 시작해, 번거로움과 좌절을 거쳐 마침내 도달한 완전한 납득이었다.
그녀는 자신의 노트북으로 시선을 돌렸다. 화면 한쪽에는 며칠간 공들여 만들었던 vanilla-board 프로젝트 폴더가 있었다. 솔라는 그 폴더를 잠시 자랑스럽게, 그리고 애틋하게 바라보았다. 잠시 후, 그녀는 그 폴더 옆에 새로운 폴더를 하나 만들었다.
bulletin-board-react
“좋았어.”
솔라가 나지막이 읊조렸다.
“이걸로 충분히 연습했어. 이제 진짜를 만들어 볼 시간이야.”