Frontend 01
정적 페이지에서 서버 CRUD 게시판까지 가는 프론트엔드 여정
HTML, CSS, JavaScript, React, API 연동이 각각 따로 보이고 왜 한 게시판 안에서 순서대로 배워야 하는지 흐릿하다.
근거 · 교안 p3-p5
1장: 게시판의 첫걸음: 정적 웹페이지의 ‘뼈대’ 만들기
솔라의 노트북 화면에는 며칠 뒤 시작될 프론트엔드 과정의 강의 계획표가 떠 있었다. 커서는 ‘1일차: 개발 환경과 HTML/CSS’라는 글자 위에서 깜빡이기만 할 뿐, 아래로 내려갈 줄을 몰랐다. 솔라는 화면을 켠 채로 의자 등받이에 몸을 기댔다. 머릿속에는 온통 ‘움직이는 게시판’에 대한 기대감으로 가득 차 있었다. 사용자가 글을 쓰고, ‘등록’ 버튼을 누르면 목록에 새 글이 착 나타나고, 다른 사람들은 댓글을 다는, 살아있는 웹페이지. 그런데 첫날 배운다는 건 고작 HTML과 CSS였다.
“언니, 역시 좀 이상해.”
거실 테이블에서 자신의 노트북으로 무언가 작업하던 루나가 고개를 들었다.
“게시판을 만든다면서, 왜 시작이 HTML/CSS인 걸까? 이건 그냥 웹사이트 꾸미는 기술 아니야? 글자 크기 바꾸고, 색깔 입히고… 나는 그런 것보다 빨리 글쓰기 기능을 만들고 싶은데.”
솔라의 목소리에는 조바심이 섞여 있었다. 그녀에게 HTML/CSS는 동적인 게시판의 핵심 기능과는 거리가 먼, 부차적인 꾸미기 요소처럼 느껴졌다. 진짜 중요한 건 JavaScript나 React 같은 기술이라고 생각했다.
루나는 솔라의 말을 잠잠히 듣고는, 자신의 노트북을 가볍게 툭툭 쳤다.
“솔라, 우리 머릿속에 있는 그 ‘게시판’을 지금 당장 눈앞에 꺼내본다고 상상해 봐. 코드로. 아주 간단하게.”
“코드로 바로?”
“응. 복잡한 기능은 다 빼고. 그냥 게시판에 꼭 있어야 할 것들만. 예를 들면 뭐가 있을까?”
루나의 말에 솔라는 잠시 생각에 잠겼다. “음… 일단 ‘게시판’이라는 큰 제목이 있어야 할 거고. 글 목록이 보일 공간, 그리고 새 글을 쓸 수 있는 입력 창들? 제목 쓰는 칸, 내용 쓰는 칸, 작성자 이름 쓰는 칸 같은 거. 그리고 ‘글쓰기’ 버튼도.”
“좋아. 그럼 딱 그것들만 한번 만들어 보자.”
루나는 솔라의 노트북 옆에 나란히 앉아 새 코드 편집기 창을 열었다. 그리고 솔라가 말한 요소들을 하나씩 HTML 태그로 바꾸어 입력하기 시작했다.
<h1>우리들의 게시판</h1>
<div>
<h2>첫 번째 글</h2>
<p>이건 첫 번째 게시글 내용입니다.</p>
<span>작성자: 루나</span>
</div>
<hr>
<input type="text" placeholder="제목을 입력하세요">
<textarea placeholder="내용을 입력하세요"></textarea>
<input type="text" placeholder="작성자">
<button>글 등록</button>
솔라는 루나가 치는 코드와, 브라우저에 나타나는 결과물을 번갈아 보았다. 정말 딱 자신이 말한 요소들만 앙상하게 화면에 나타났다. 제목, 글 한 덩어리, 그리고 입력을 위한 네모난 칸들과 버튼 하나. 모든 요소가 아무런 스타일 없이 위에서 아래로 순서대로 놓여 있었다.
“이게… 다야? 버튼을 눌러도 아무 일도 안 일어나잖아. 그냥 글자랑 상자들만 있네.”
솔라는 실망한 기색을 감추지 않았다. 역시 자신의 생각이 맞았다. 이건 그냥 껍데기일 뿐이다.
루나는 말없이 고개를 끄덕였다. 그리고 화면 위 ‘글 등록’ 버튼을 마우스 커서로 가리켰다.
“응, 지금은 아무것도 안 하지. 그런데 솔라, 만약 화면에 이 ‘버튼’이라는 것 자체가 없다면, 우리가 나중에 ‘이걸 누르면 글이 등록되게 해줘’라는 명령을 어디에 내릴 수 있을까?”
“…….”
“‘제목을 입력하는 칸’이 없다면, 사용자가 입력한 제목을 컴퓨터가 어떻게 알아챌 수 있을까? 어디서부터 어디까지가 제목인지 어떻게 구분하고?”
루나의 질문은 솔라의 머릿속을 파고들었다. 솔라의 시선은 브라우저 화면의 밋밋한 버튼과 코드 편집기의 <button>글 등록</button> 태그 사이를 오갔다. 처음에는 그저 ‘보이기 위한’ 장식이라고 생각했던 것들의 역할이 다르게 보이기 시작했다.
이 버튼은 지금 아무런 행동(behavior)도 하지 않는다. 하지만 이 태그가 있기에 ‘이것은 버튼이다’라는 정체성(identity)이 생긴다. 이 정체성이 있어야만, 나중에 ‘이 버튼을 누르면, 저기 있는 입력 칸의 내용을 가져와서 목록에 추가해’라는 구체적인 행동을 부여할 수 있다. 제목 입력 칸, 내용 입력 칸도 마찬가지였다. 그 칸들이 먼저 자리를 잡고 ‘여기는 제목이 들어올 자리’, ‘여기는 내용이 기록될 공간’이라고 구조를 정의해 주어야, 비로소 동적인 기능이 들어설 땅이 생기는 것이었다.
“아…!”
솔라의 입에서 작은 탄성이 터져 나왔다.
“그러니까 이건 꾸미는 게 아니었네. 모든 기능이 들어설 ‘자리’를 미리 만드는 거였어. 게시판의 뼈대를 세우는 작업이었구나. 어디에 창문이 달릴지, 어디에 문을 놓을지 정하는 것처럼.”
솔라는 방금 전 자신이 떠올렸던 ‘강의 계획표’를 다시 생각했다. 왜 HTML/CSS가 첫날에 있었는지 이제야 명확하게 이해되었다. 그것은 가장 기초적이면서도 가장 먼저 필요한, 전체 구조를 잡는 일이었기 때문이다. 동적인 기능을 추가하는 것은 그 뼈대 위에서 벌어지는 다음 단계의 일이었다.
솔라의 얼굴에 깨달음의 빛이 스쳤다. 이제 HTML/CSS는 더 이상 지루한 꾸미기 기술이 아니었다. 앞으로 만들 모든 기능의 기반이 될 ‘정적 구조 스케치’였다.
문득, 솔라는 다시 앙상한 웹페이지를 바라보았다. 뼈대의 중요성은 알겠다. 하지만 여전히 이 페이지는 죽어있는 것처럼 보였다.
“좋아, 뼈대는 완벽히 이해했어. 그럼 이제 이 뼈대에 살을 붙여서 진짜 움직이게 만들려면 어떻게 해야 해? 저 ‘글 등록’ 버튼을 눌렀을 때, 내가 쓴 글이 진짜 목록에 착! 하고 나타나게 하는 거 말이야.”
2장: 정적 뼈대에 ‘생명’ 불어넣기: JavaScript와 DOM
솔라는 노트북 화면에 떠 있는 앙상한 게시판 페이지를 말없이 바라보았다. ‘글 등록’ 버튼 위로 마우스 커서를 가져갔다가, 의미 없는 클릭을 한번 해 보았다. 당연하게도, 아무런 변화가 없었다. 페이지는 어제 루나 언니와 만들었던 그 모습 그대로, 완벽한 ‘정적 구조 스케치’ 상태로 멈춰 있었다. 뼈대의 중요성은 알겠지만, 이 죽어있는 듯한 모습은 여전히 답답했다.
그때 옆에 있던 루나가 솔라의 코드 편집기 화면으로 손을 뻗어, </body> 태그 바로 위에 새로운 태그 한 줄을 추가했다.
<script src="app.js"></script>
그리고는 프로젝트 폴더에 app.js라는 빈 파일을 생성했다. 화면에는 아주 작은 변화였지만, 솔라는 직감적으로 이것이 다음 단계로 넘어가는 신호임을 알아차렸다. 텅 빈 파일과 멈춰있는 버튼을 번갈아 보던 솔라가 입을 열었다.
“언니, 이 app.js라는 게… 혹시 저 ‘글 등록’ 버튼에 마법을 거는 열쇠야? 버튼을 누르면 내가 쓴 글이 진짜 목록에 나타나게 하는 거.”
솔라는 슬쩍 걱정스러운 마음을 덧붙였다. “그런데 그게 가능해? 페이지를 새로고침하지 않고도? 내가 알기로 JavaScript는 그냥 글자 반짝이게 하거나, 광고 팝업 띄우는 것 같은 시각적인 효과에만 쓰는 줄 알았는데…”
루나는 솔라의 말을 듣고는, 새로 만든 빈 app.js 파일을 가리켰다.
“우리가 어제 만든 HTML 문서는 브라우저가 읽어서 화면에 보여주는 ‘설계도’ 같은 거야. 브라우저는 이 설계도를 바탕으로 살아있는 문서 객체 모델(DOM)이라는 걸 만들어. 그리고 이 app.js 파일 안에서, 우리가 그 살아있는 모델에게 말을 걸 수 있어.”
“말을 건다고?”
“응. ‘저기 ‘글 등록’ 버튼, 누가 널 클릭하면 나한테 알려줘.’ 또는 ‘저기 제목 입력 칸, 안에 무슨 글자가 쓰여 있는지 말해줘.’ 하고 명령을 내리는 거지.”
루나의 비유에 솔라는 고개를 갸웃거렸다. 여전히 알쏭달쏭했다. 루나는 키보드를 솔라 쪽으로 조금 더 밀어주었다.
“말로 설명하는 것보다, 직접 해보는 게 빨라. 솔라, 네가 원하는 기능을 다시 한번 순서대로 말해볼래? ‘글 등록’ 버튼을 눌렀을 때 무슨 일이 일어나야 하지?”
솔라는 화면의 입력 칸들과 글 목록을 번갈아 보며 차근차근 정리했다. “첫째, ‘글 등록’ 버튼을 누르면. 둘째, 제목, 내용, 작성자 입력 칸에 있는 글자들을 가져와. 셋째, 그 글자들을 가지고 새로운 게시글 덩어리를 만들어서, 기존 글 목록 위에 착 붙여주는 거지.”
“완벽해. 그걸 그대로 코드로 옮겨보자.”
루나의 안내에 따라, 솔라는 app.js 파일에 코드를 입력하기 시작했다. 먼저, HTML 문서의 특정 요소들을 ‘찾아서’ 변수에 담는 작업이었다.
const postButton = document.querySelector('button');
const titleInput = document.querySelector('input[type="text"]');
// ... 다른 입력 칸들도 마찬가지로 선택
“document.querySelector… 아, 이게 ‘문서(document)에서 저 선택자(selector)에 해당하는 요소를 찾아줘’라는 명령이구나.”
솔라는 HTML 코드의 <button> 태그와 JavaScript 코드의 postButton 변수가 연결되는 것을 눈으로 확인했다. 뼈대 위에 그려진 ‘버튼’이라는 자리에 드디어 이름표를 붙여준 느낌이었다.
다음은 버튼의 ‘클릭’이라는 행동을 감지하는 부분이었다.
postButton.addEventListener('click', function() {
// 버튼이 클릭되었을 때 실행할 작업들
});
이제 남은 것은 솔라가 말한 두 번째와 세 번째 단계, 즉 입력 값을 가져와 새 글을 만들어 붙이는 작업이었다. 루나는 함수 안쪽을 채워 넣도록 솔라를 이끌었다.
postButton.addEventListener('click', function() {
// 2. 입력 칸의 값을 가져온다
const title = titleInput.value;
// ... 내용과 작성자 값도 가져온다
// 3. 새로운 게시글 덩어리를 만든다
const newPost = document.createElement('div');
newPost.innerHTML = `
<h2>${title}</h2>
<p>...</p>
<span>...</span>
`;
// 4. 기존 글 목록 영역에 새로운 글을 추가한다
const postsContainer = document.querySelector('div'); // 실제로는 더 구체적인 선택자가 필요하겠지만
postsContainer.prepend(newPost);
});
코드가 완성되자 솔라는 잠시 숨을 골랐다. document.createElement는 새로운 HTML 요소를 만드는 주문이었고, prepend는 그것을 맨 위에 추가하는 주문이었다. 모든 조각이 맞춰진 것 같았다.
떨리는 마음으로 브라우저로 돌아가 입력 칸에 ‘새로운 글입니다!’라고 제목을 입력하고, 내용과 이름을 채웠다. 그리고 마침내, ‘글 등록’ 버튼을 클릭했다.
찰나의 정적. 그리고…
화면이 새로고침되지 않았는데도, 멈춰 있던 게시판 목록의 가장 위에 방금 자신이 입력한 ‘새로운 글입니다!’라는 제목의 글이 나타났다.
“우와…! 됐어! 진짜로 생겼어!”
솔라는 저도 모르게 탄성을 질렀다. 페이지를 새로 불러오지 않고도 화면의 내용이 바뀌었다. 이건 마법 같았다. 죽어있던 뼈대가 자신의 명령에 따라 스스로 움직인 것이다. JavaScript는 그저 시각 효과를 위한 장난감이 아니었다. HTML이라는 정적인 구조에 동적인 ‘생명’을 불어넣는 핵심 도구였다.
“이게… 이게 바로 ‘DOM 조작’이구나! 브라우저가 만들어 놓은 문서 객체 모델을 JavaScript로 직접 주무르는 거네.”
솔라는 깨달았다. HTML이 건물의 뼈대라면, JavaScript와 DOM은 그 건물 안의 엘리베이터를 움직이고, 전등을 켜고, 문을 열고 닫는 자동 제어 시스템과 같았다. 정적인 공간에 상호작용과 기능을 부여하는 것. 그것이 바로 JavaScript의 진짜 역할이었다.
신이 난 솔라는 글을 몇 개 더 등록했다. ‘두 번째 글’, ‘세 번째 글’이 목록에 차곡차곡 쌓였다. 이제 제법 게시판다운 모습이었다. 뿌듯하게 화면을 보던 솔라의 미간에 갑자기 희미한 주름이 잡혔다.
“언니, 그런데… 만약에 글을 수정하는 기능도 넣고 싶으면 어떡하지? 각 글마다 ‘수정’ 버튼을 달아야 할 텐데. 방금 우리가 만든 코드는 새 글을 추가할 때 ‘수정’ 버튼까지 같이 만들어주진 않잖아. 그리고 새로 추가된 글의 수정 버튼은 또 어떻게 찾아내서 이벤트를 걸어주지? 글이 10개가 되면, 10개의 버튼을 전부 다르게 관리해야 하나? 갑자기 코드가 엄청 복잡해질 것 같아.”
솔라의 머릿속에 성공의 기쁨만큼이나 거대한 혼란이 밀려오기 시작했다. 지금의 방식으로는 기능이 조금만 추가되어도 모든 것이 뒤엉켜 버릴 것 같았다.
3장: 복잡한 동적 기능을 ‘정돈’하기: React의 상태 관리
솔라의 노트북 화면 속 app.js 파일은 더 이상 지난번의 깔끔한 모습이 아니었다. 스크롤바는 눈에 띄게 짧아져 있었고, 파일 안은 document.querySelector, createElement, addEventListener 같은 주문들이 어지럽게 얽혀 있었다. 성공의 기쁨에 취해 ‘수정’과 ‘삭제’ 기능을 무작정 추가해 보려던 시도의 흔적이었다. 새로 추가된 글의 ‘수정’ 버튼을 누르면, 해당 글의 내용(p 태그)이 입력창(textarea)으로 바뀌어야 했다. 그리고 다시 ‘저장’ 버튼을 누르면 입력창의 내용이 p 태그로 되돌아와야 했다.
하지만 코드는 솔라의 뜻대로 움직여주지 않았다. 어떤 글의 수정 버튼을 눌러야 할지, 그 버튼에 해당하는 글 데이터가 무엇인지 찾아내는 것부터가 고역이었다. 새 글을 추가할 때마다 새로 생기는 버튼들에는 이벤트 리스너가 제대로 붙지 않았고, 어찌어찌 붙여도 코드는 금세 꼬여버렸다. 마치 수십 개의 실을 한 번에 바늘귀에 꿰려는 것처럼, 기능 하나를 고치면 다른 두 개가 망가졌다. 모든 것을 직접 찾고, 만들고, 붙이고, 바꾸는 방식은 한계에 다다른 듯 보였다.
솔라는 결국 키보드에서 손을 떼고 한숨을 내쉬었다.
“언니, 이거 완전 스파게티가 되어버렸어.”
솔라의 지친 목소리에 루나가 옆으로 다가와 복잡하게 얽힌 코드를 들여다보았다. 솔라는 화면을 가리키며 하소연했다.
“그냥 글마다 수정, 삭제 버튼만 달고 싶었을 뿐인데, 뭐가 뭐랑 연결된 건지 하나도 모르겠어. ‘3번 글의 수정 버튼’을 어떻게 정확히 찾아서, ‘3번 글의 내용’만 바꾸게 하지? 이걸 JavaScript로만 하려니까 코드가 너무 지저분해져. 물론 어떻게든 욱여넣으면 되긴 하겠지만… 이게 맞는 방법 같지가 않아. 프론트엔드 개발자들이 다 이렇게 하나하나 직접 관리하는 거야?”
솔라는 강의 계획표에서 스쳐 지나갔던 ‘React’라는 단어를 떠올렸다. 하지만 곧 고개를 저었다. “무슨 라이브러리 같은 걸 쓰면 된다고 듣긴 했는데, 이렇게 간단한 기능에 그런 복잡한 걸 쓰는 건 배보다 배꼽이 더 큰 거 아닐까?”
루나는 어지러운 코드와 시무룩한 솔라의 얼굴을 번갈아 보았다. 그리고는 솔라의 코드 편집기 옆에 새로운 빈 창을 하나 더 열었다.
“솔라, 우리가 지금 하고 있는 일을 두 가지로 나눠서 생각해 보자.”
루나는 키보드로 두 개의 주석을 쳤다.
// 1. '데이터' 관리: 우리 게시판의 글 목록
// 2. '화면' 관리: 데이터를 HTML로 보여주고, 변경하는 일
“지금 솔라의 코드는 이 두 가지 일을 한 곳에서 뒤섞어 처리하고 있어. ‘수정 버튼을 누르면’이라는 화면의 이벤트를 감지해서, ‘이 글의 내용을 입력창으로 바꿔줘’라고 화면을 직접 조작하고 있지. 데이터는 그저 화면 어딘가에 텍스트로 존재할 뿐이고.”
“맞아. 그래서 헷갈리는 거야. 데이터랑 화면이 딱 붙어 있으니까.”
“그럼 만약에, 우리가 ‘화면’을 직접 건드리는 일을 그만두면 어떨까?”
루나의 말에 솔라의 눈이 동그래졌다. “화면을 안 건드린다고? 그럼 어떻게 글을 추가하고 수정해?”
“대신 우리는 ‘데이터’에만 집중하는 거야.”
루나는 빈 창에 새로운 형태의 코드를 작성하기 시작했다. 이번에는 HTML 태그가 아니었다. 순수한 JavaScript 배열이었다.
// 우리 게시판의 유일한 '데이터'
const postsData = [
{ id: 1, title: '첫 번째 글', content: '이건 첫 번째 게시글 내용입니다.' },
{ id: 2, title: '두 번째 글', content: '이건 두 번째 게시글 내용입니다.' },
{ id: 3, title: '세 번째 글', content: '방금 추가한 글!' }
];
“이게 우리 게시판의 ‘상태(state)’야. 지금 화면에 보여야 할 모든 정보를 담고 있는 유일한 진실이지. 화면은 그저 이 데이터를 바탕으로 그려진 그림일 뿐이라고 생각하는 거야.”
루나는 이어서 React의 구문법을 사용해 코드를 재구성했다. 기존의 복잡한 app.js 파일의 내용이 놀랍도록 단순한 구조로 변하기 시작했다. document.querySelector나 createElement 같은 DOM 조작 명령어가 보이지 않았다. 대신 useState라는 낯선 함수와, HTML처럼 생긴 이상한 코드 조각들이 나타났다.
function Board() {
const [posts, setPosts] = useState(postsData); // 'posts'라는 상태를 선언
// 새 글 추가 함수
const handleAddPost = (newPost) => {
setPosts([newPost, ...posts]); // 그냥 데이터 배열을 새로 만들어서 교체!
};
return (
<div>
{/* 입력 폼 컴포넌트 */}
<PostForm onAddPost={handleAddPost} />
{/* 글 목록: posts 배열을 순회하며 Post 컴포넌트를 그린다 */}
{posts.map(post => (
<Post key={post.id} title={post.title} content={post.content} />
))}
</div>
);
}
솔라는 코드를 보며 눈을 떼지 못했다. handleAddPost 함수 안에는 화면을 조작하는 코드가 단 한 줄도 없었다. 그저 setPosts라는 함수를 호출해 posts라는 데이터 배열을 새로운 배열로 ‘바꿔치기’할 뿐이었다. 하지만 브라우저에서는 놀랍게도 새 글이 목록에 추가되었다.
“잠깐만… createElement도 없고 prepend도 없는데 어떻게 화면이 바뀌는 거야? 우린 그냥 posts라는 배열을 바꿨을 뿐이잖아.”
“바로 그거야.” 루나가 setPosts 부분을 가리켰다. “우리는 더 이상 ‘제목 입력 칸의 값을 가져와서, div를 만들고, h2와 p를 만들어서, 목록의 맨 위에 붙여라’ 같은 구체적인 지시를 내리지 않아. 대신 React에게 이렇게 말하는 거지. ‘이게 새로운 데이터야. 이걸로 화면 좀 알아서 다시 그려줘.’”
솔라의 머릿속에서 무언가 ‘탁’ 하고 끊어지는 느낌이 들었다. 이전 방식은 마치 부하 직원에게 하나부터 열까지 모든 절차를 일일이 지시하는 사장님 같았다. 하지만 React 방식은 달랐다. 유능한 중간 관리자(React)에게 ‘최종 보고서는 이 내용으로 바뀌었으니 반영해줘’라고 목표(새로운 상태)만 던져주는 것과 같았다. 세부적인 실행은 관리자가 알아서 처리한다.
“아…! 그러니까 우리는 ‘데이터’라는 설계도만 잘 관리하면, React가 알아서 화면이라는 건물을 지어주고 리모델링까지 해주는 거구나. 수정 기능을 만들 때도, DOM 요소가 아니라 그냥 저 postsData 배열에서 원하는 글을 찾아서 내용만 바꿔주면… React가 알아서 화면을 새로 그려주겠네!”
솔라는 깨달았다. React는 단순히 복잡하기만 한 라이브러리가 아니었다. 뒤엉킨 ‘데이터’와 ‘화면’의 관계를 명확하게 분리하고, 개발자가 데이터 관리에만 집중할 수 있도록 돕는 강력한 도구였다. 복잡한 동적 기능을 ‘정돈’해주는 체계적인 방법론, 바로 ‘상태 기반 UI 구축’이었다. 어지러웠던 스파게티 코드가 역할별로 나뉜 깔끔한 도시락처럼 보이기 시작했다.
신이 난 솔라는 새로 만든 React 게시판에 글을 몇 개 더 추가했다. 코드는 훨씬 간결했고, 기능 추가에 대한 두려움도 사라졌다. 이제 게시판이 제법 그럴듯한 모습을 갖췄다. 뿌듯한 미소를 지으며 솔라는 습관적으로 브라우저의 새로고침 버튼을 눌렀다.
화면이 깜빡이고, 다시 하얗게 변했다. 그리고 방금까지 화면을 가득 채웠던 게시글들이 온데간데없이 사라졌다. 처음의 텅 빈 게시판 페이지만이 덩그러니 남아 있었다.
“어…?”
솔라의 얼굴에서 웃음기가 사라졌다.
“맞다… ‘상태’는 그냥 JavaScript 변수일 뿐이니까. 페이지를 새로고침하면 전부 날아가는구나. 이건 훨씬 나아졌지만, 여전히 나 혼자만 볼 수 있고 끄면 사라지는 장난감이잖아. 이 데이터를 영원히 저장하고 다른 사람들도 보게 하려면 어떻게 해야 하지?”
4장: 게시판 데이터를 ‘영속적’으로: API 연동의 역할
솔라의 노트북 화면은 텅 비어 있었다. 방금 전까지 여러 개의 게시글로 채워져 있던 목록은 온데간데없고, ‘우리들의 게시판’이라는 제목만 덩그러니 남아있었다. 솔라는 무의식적으로 새로고침 단축키를 한 번 더 눌렀지만, 결과는 같았다. 코드 편집기에는 분명 useState를 통해 관리되던 게시글 데이터가 있었지만, 그건 브라우저가 꺼지거나 새로고침되면 사라지는 신기루 같은 존재였다.
옆에서 이 모습을 지켜보던 루나는 아무 말 없이 자신의 노트북에서 검은 터미널 창을 하나 열었다. 그리고 몇 가지 명령어를 입력하자, 창에는 JSON Server is running 이라는 메시지가 떴다. 솔라의 노트북과는 별개로, 완전히 독립된 프로그램 하나가 조용히 작동하기 시작했다. 마치 옆집에 작은 가게가 문을 연 것 같았다.
“언니, 그거 뭐야?”
솔라는 자신의 텅 빈 화면과 루나의 검은 창을 번갈아 보며 물었다.
“이거… 내 게시판이랑 무슨 상관이야? 난 지금 글이 전부 날아가서 속상한데. React로 상태를 관리하는 게 훨씬 편해지긴 했지만, 결국 이건 나 혼자 쓰는 일기장이고, 끄면 사라지는 장난감이잖아. 데이터를 영원히 저장하고 다른 사람들도 보게 하려면 결국 서버라는 게 필요하다는 건 알겠어. 근데 그건 백엔드 개발자들 일이잖아? 강의 계획표에 ‘서버 연동 CRUD’라는 말이 있긴 했는데, 프론트엔드 개발자가 서버까지 알아야 하는 거야?”
솔라의 목소리에는 막막함이 묻어났다. 서버는 멀고 어려운, 자신의 영역이 아닌 것처럼 느껴졌다.
루나는 방금 켠 검은 터미널 창을 손가락으로 가리켰다.
“솔라, 네가 만든 React 게시판을 레스토랑 손님이라고 생각해 봐. 그리고 이 검은 창은 모든 요리 재료를 보관하고, 주문을 받아 음식을 만드는 주방이야. 지금 네 손님은 음식을 주문하고 싶어도, 주방의 존재조차 모르고 말 거는 방법도 몰라. 그래서 자기가 가져온 간식(초기 상태 데이터)만 먹다가, 자리를 뜨면(새로고침) 모든 게 사라지는 거지.”
“주방…”
“응. 프론트엔드 개발자가 주방 안에서 직접 요리(서버 로직)를 할 필요는 없어. 하지만, 메뉴판을 읽고 웨이터를 불러서 ‘이 메뉴로 주문할게요’라고 정확히 말(API 요청)할 줄은 알아야 해. 그래야 주방에서 만든 따끈한 요리(서버 데이터)를 받아서 먹을 수 있으니까.”
루나는 솔라의 코드 편집기에서 Board 컴포넌트를 열고, useState로 초기 데이터를 설정하던 부분을 지웠다. 대신 useEffect라는 낯선 훅을 추가했다.
function Board() {
const [posts, setPosts] = useState([]); // 처음엔 빈 배열!
useEffect(() => {
// 컴포넌트가 처음 화면에 그려질 때, 주방에 요리를 주문하자
fetch('http://localhost:3001/posts')
.then(response => response.json())
.then(data => {
setPosts(data); // 주방에서 받은 요리로 상을 차린다
});
}, []); // 맨 처음 한 번만 실행되도록
// ... (이하 생략)
}
“fetch…?” 솔라가 낯선 단어를 중얼거렸다.
“응. ‘가져오다’. 이게 바로 손님이 웨이터를 부르는 행동이야. 저 주소(http://localhost:3001/posts)가 바로 우리가 찾아갈 주방의 주소고. 이렇게 주문하면, 주방은 약속된 요리(게시글 목록 데이터)를 보내줘.”
솔라는 반신반의하며 코드를 저장하고 브라우저를 새로고침했다. 처음에는 텅 빈 화면이 잠깐 보였다. 하지만 이내, 마치 마법처럼, 솔라가 한 번도 직접 입력한 적 없는 ‘서버에서 온 첫 번째 글’, ‘서버 테스트용 글’ 같은 게시물들이 화면에 나타났다. 루나의 ‘주방’ 컴퓨터에 미리 저장되어 있던 데이터였다. 솔라는 놀라서 다시 새로고침 버튼을 눌렀다. 이번에도 데이터는 사라지지 않고 그대로 나타났다.
“우와! 데이터가… 안 사라져! 페이지를 열 때마다 저기 저 검은 창에서 데이터를 가져오는구나!”
휘발성 메모리에 의존하던 장난감이 드디어 외부의 영속적인 데이터와 연결된 순간이었다. 하지만 아직 절반의 성공이었다.
“그럼 글을 새로 쓰는 건? ‘글 등록’ 버튼을 누르면, 이 데이터가 내 컴퓨터에만 추가되는 게 아니라 저 주방에도 전달되어야 진짜 저장되는 거잖아.”
솔라는 스스로 문제의 핵심을 짚어냈다. 루나는 고개를 끄덕이며 기존의 handleAddPost 함수를 수정하도록 안내했다.
const handleAddPost = (newPostData) => {
// 1. 먼저 주방에 새로운 요리를 만들어달라고 주문한다
fetch('http://localhost:3001/posts', {
method: 'POST', // '주세요'가 아니라 '만들어주세요' 라는 주문서
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPostData), // 주문할 음식의 레시피
})
.then(response => response.json())
.then(savedPost => {
// 2. 주방에서 요리가 완성되었다고 연락이 오면, 그 때 손님 상에 음식을 올린다
setPosts([savedPost, ...posts]);
});
};
이전에는 그저 setPosts만 호출하던 간단한 함수가, 이제는 fetch를 통해 ‘주방’과 복잡한 대화를 나누는 코드로 바뀌었다. 솔라는 떨리는 마음으로 입력창에 ‘드디어 성공!’이라는 제목의 글을 쓰고 ‘글 등록’ 버튼을 눌렀다. 글은 즉시 화면에 추가되었다. 그리고 더 놀라운 일이 벌어졌다. 루나가 가리키는 db.json이라는 파일 안에, 방금 솔라가 작성한 ‘드디어 성공!’이라는 글이 그대로 기록되어 있었다.
솔라는 마지막 확인을 위해 브라우저를 다시 새로고침했다. 화면에는 ‘서버에서 온 글’들과 함께, 방금 자신이 추가한 ‘드디어 성공!’이라는 글까지 모두 완벽하게 나타났다.
“됐어…! 이제 진짜 게시판이야!”
솔라의 얼굴에 환한 미소가 번졌다. 서버는 더 이상 남의 영역이 아니었다. 프론트엔드는 단순히 보이는 것을 만드는 일을 넘어, 멀리 떨어진 데이터 저장소와 소통하여 ‘살아있는’ 데이터를 화면에 그려내는 역할까지 한다는 것을 깨달았다. fetch 안에 담긴 주소와 method, body 같은 옵션들은 바로 클라이언트와 서버가 서로 데이터를 주고받기 위해 정해놓은 ‘규약’이었다. 메뉴판의 언어였다.
API 연동은 프론트엔드 개발자가 백엔드의 영역을 침범하는 것이 아니라, 오히려 자신의 역할인 ‘데이터를 사용자에게 보여주는 일’을 완수하기 위한 필수적인 다리 놓기 작업이었던 것이다.
모든 것이 해결된 것 같았다. 하지만 솔라의 시선은 문득, 아직 기능이 없는 ‘수정’과 ‘삭제’ 버튼에 머물렀다.
“언니, 이제 글을 가져오고(Read) 새로 쓰는 거(Create)는 알겠어. 그럼 이미 있는 글을 수정하거나(Update) 지우는(Delete) 건 어떻게 말해야 해? 그것도 다른 주문 방법이 있는 거야?”
5장: 프론트엔드 여정의 완성: CRUD 게시판 통합
솔라의 시선은 자신의 노트북 화면에 떠 있는 게시글 목록의 한 부분에 고정되어 있었다. ‘서버에서 온 글’, ‘드디어 성공!’ 같은 제목 옆에는, 지난번 DOM을 직접 조작하며 만들다 포기했던 ‘수정’과 ‘삭제’ 버튼이 회색 빛으로 자리 잡고 있었다. 이전과 달리 이제는 각 글이 React 컴포넌트로 깔끔하게 분리되어 있었고, 새로고침해도 데이터가 사라지지 않았다. 하지만 저 버튼들은 여전히 껍데기뿐이었다.
‘글 목록 가져오기(Read)’는 useEffect 안의 fetch로 해결했다. ‘새 글 쓰기(Create)’는 POST 메소드를 사용한 fetch로 해결했다. 솔라는 자연스럽게 다음 단계를 떠올렸다. 글을 수정하는 ‘Update’와 지우는 ‘Delete’도 각각 PUT이나 DELETE 메소드를 사용한 fetch 호출로 해결될 터였다. 각자 다른 주문서로 주방에 새로운 요청을 보내는 것뿐이니까. 하지만 어쩐지 마음 한편이 개운하지 않았다.
“언니.”
솔라는 옆에 있는 루나를 불렀다.
“수정이랑 삭제 기능도 이제 API로 구현하면 될 것 같아. 그런데… 뭔가 이상해. 결국 HTML로 버튼 만들고, React로 상태 관리하고, API로 서버에 요청 보내고. 각자 자기 할 일만 하는 거잖아. 처음부터 끝까지 하나의 흐름으로 연결되어 있다는 느낌이 잘 안 들어. 여전히 HTML, React, API가 따로따로 노는 것처럼 보여. 그냥… 필요한 부품을 그때그때 가져다 쓰는 느낌?”
솔라의 말은 프론트엔드 개발의 여정이 여전히 분절된 기술들의 집합처럼 느껴진다는 의미였다. 하나의 완성된 게시판 안에서 이 모든 것들이 어떻게 유기적으로 맞물려 돌아가는지에 대한 전체 그림, 그 거대한 톱니바퀴의 움직임이 보이지 않았다.
루나는 솔라의 화면과 고민하는 얼굴을 잠시 바라보더니, 키보드를 가져가 코드 편집기 옆에 새 창을 열었다. 코드를 치는 대신, 간단한 텍스트로 그림을 그리기 시작했다.
+----------------+
| HTML/CSS | <-- 사용자가 보는 '화면' (정적 뼈대)
| [글1] [수정] [삭제] |
| [글2] [수정] [삭제] |
+----------------+
↑
| React가 그려줌
↓ 사용자가 클릭
+----------------+
| React | <-- 화면을 그리는 '설계도'
| State: [글1, 글2] |
+----------------+
↑
| 데이터 동기화
↓
+----------------+
| API/Server | <-- 데이터의 '영구 저장소'
| DB: [글1, 글2] |
+----------------+
“솔라, 이게 우리가 지금까지 만든 게시판의 지도야.”
루나가 그린 간단한 도식은 세 개의 상자로 이루어져 있었다. 맨 위는 사용자가 보는 화면(HTML), 가운데는 화면을 그리는 데이터 설계도(React State), 맨 아래는 데이터의 영구 저장소(Server)였다.
“우리는 이제 이 지도 위에서 사용자의 행동이 어떤 여행을 하는지 따라가 볼 거야. 자, ‘글 2’ 옆에 있는 ‘삭제’ 버튼을 사용자가 클릭했어. 여행은 어디서 시작되지?”
루나의 질문에 솔라는 맨 위 상자의 ‘[삭제]’ 버튼을 손으로 짚었다. “여기서. HTML 화면에서.”
“좋아. 그 다음엔 어디로 갈까?”
솔라는 잠시 생각하다 가운데 상자를 가리켰다. “버튼의 onClick 이벤트 핸들러가 호출되겠지. 그건 React 컴포넌트 안에 있으니까… 이쪽으로 와.”
“정확해. 그럼 React는 무슨 일을 하지?”
“음… handleDelete(2번_글_ID) 같은 함수를 실행할 거야. 그리고… 이제 서버에 알려줘야지. 이 글을 지워달라고.” 솔라의 손가락은 가운데 상자에서 아래쪽, API/Server 상자로 이동했다. “fetch('/posts/2', { method: 'DELETE' }) 같은 주문서를 보내.”
“그리고?”
“서버는 데이터베이스에서 2번 글을 삭제하고, ‘삭제 완료!’라고 응답을 보내주겠지.”
여기까지는 솔라도 예상했던 흐름이었다. 하지만 루나는 멈추지 않고 다시 물었다. “응답을 받은 다음이 중요해. 서버에서 글이 삭제됐다고, 화면이 저절로 바뀌나?”
“아니.” 솔라는 즉시 대답했다. “서버는 서버고, 화면은 화면이니까.”
“그럼 화면에서 ‘글 2’를 사라지게 만들려면 뭘 해야 할까?”
그 순간, 솔라의 머릿속에 흩어져 있던 조각들이 하나의 그림으로 맞춰지기 시작했다.
“…React의 ‘상태’를 바꿔야 해!”
솔라의 손가락이 다시 아래쪽 서버 상자에서 가운데 React 상자로 빠르게 거슬러 올라왔다.
“API 요청이 성공했다는 응답을 받으면, 그 .then() 안에서 setPosts를 호출해야 해! 현재 posts 배열에서 2번 글만 뺀 새로운 배열로. setPosts(posts.filter(p => p.id !== 2)) 처럼.”
“그렇게 상태가 바뀌면, 마지막으로 무슨 일이 일어나지?”
“React가… 화면을 다시 그려.” 솔라의 손가락은 마침내 가운데 React 상자에서 맨 위 HTML 상자로 돌아왔다. 화면에서 ‘글 2’가 사라지는 마지막 종착지였다.
삭제 버튼 클릭 한 번에 시작된 여행의 전체 경로가 눈앞에 그려졌다.
화면 클릭(HTML) → 이벤트 핸들러(React) → API 요청(JS Fetch) → 데이터 처리(Server) → API 응답 → 상태 업데이트(React State) → 화면 다시 그리기(React-DOM)
이것은 분절된 기술의 나열이 아니었다. 하나의 목표, 즉 ‘사용자의 행동을 데이터와 화면에 동기화한다’는 목표를 위해 유기적으로 연결된 거대한 순환 고리였다. HTML은 구조를 제공하며 여행의 출발점이 되고, React는 상태와 UI를 연결하는 지휘자 역할을 하며, API는 그 여행이 클라이언트를 넘어 서버까지 닿도록 길을 열어주는 통로였다.
“이제 알겠어…” 솔라가 나지막이 말했다. “기술 하나하나가 각자 일을 하는 게 아니었어. 게시판이라는 하나의 시스템 안에서 각자의 역할을 맡아 순서대로 움직이는 거였구나. 마치 공장의 컨베이어 벨트처럼.”
솔라는 더 이상 각 기술 스택을 개별적인 학습 영역으로 보지 않았다. 그것들은 ‘정적 구조’에서 시작해 ‘동적 상호작용’을 거쳐 ‘체계적인 상태 관리’와 ‘영속적인 서버 통신’으로 프로젝트를 ‘점진적으로 확장’해나가는 과정에서 만나는, 필연적인 문제 해결 도구들이었다.
문득, 솔라는 며칠 전 자신을 막막하게 했던 ‘강의 계획표’를 떠올렸다. 왜 HTML/CSS로 시작해서 JavaScript를 거쳐 React와 API로 나아가는지, 이제는 명확하게 설명할 수 있었다. 솔라는 책상 위 빈 노트를 펼치고 펜을 들었다. 그리고 ‘나만의 맛집 지도 만들기’라는 새로운 프로젝트 제목을 적었다. 그 아래에는, 더 이상 누군가 정해준 순서가 아닌, 자신이 이해한 흐름에 따라 개발 로드맵을 직접 그리기 시작했다.
- 화면 뼈대 만들기 (HTML/CSS): 지도 영역, 맛집 목록, 정보 창의 정적인 모습부터 스케치한다.
- 초기 상태 관리 (React): 가짜 맛집 데이터 배열(
useState)을 만들고, 그 데이터를 기반으로 화면에 목록을 렌더링한다. - 데이터 영속화 (API 연동):
useEffect로 실제 서버에서 맛집 목록을 불러오고, 새로운 맛집을 추가(POST)하는 기능을 구현한다. - 전체 CRUD 완성: 기존 맛집 정보를 수정(
PUT)하고 삭제(DELETE)하는 흐름까지 지도에 따라 완성한다.
노트 위에 그려진 단계적 확장 계획은, 솔라가 프론트엔드 개발의 여정을 완전히 자신의 것으로 만들었음을 보여주는 증거였다. 하나의 완성된 시스템을 구축하는 전체적인 시야, 그것이 바로 이 긴 여정의 끝에서 솔라가 얻은 가장 값진 ‘전체 시스템 통합’이라는 도구였다.