Mini Project 4 01

BookShelf 프로젝트 지도: 기능 목록 너머의 아키텍처 읽기

도서 CRUD와 AI 표지 생성, 추천이 한 프로젝트에 같이 들어오면 무엇이 핵심인지 흐려진다. 단순히 기능 목록을 나열하면 4차가 어떤 frontend 산출물이었는지 보이지 않는다.

근거 · 4차 교안/BookShelf README

BookShelf 프로젝트 지도: 기능 목록 너머의 아키텍처 읽기 대표 이미지

1장: 백엔드 없는 도서 관리의 비밀

솔라가 노트북 화면에 떠 있는 프로젝트 개요를 찌푸린 얼굴로 들여다보고 있었다. 짧은 한 문장이었지만, 어딘가 아귀가 맞지 않는 느낌에 좀처럼 시선이 다음으로 넘어가지 않았다.

BookShelf는 backend code 작성 없이 완성된 frontend 미니프로젝트다.

책꽂이에 꽂힌 책들처럼, 머릿속 단어들은 각자 제자리를 아는 듯했다. ‘프론트엔드’, ‘백엔드 코드 없음’, ‘미니프로젝트’. 하지만 이 단어들이 한 문장으로 엮이자 서로 밀어내는 자석처럼 어색한 긴장감을 만들었다. 솔라는 고개를 들어 거실 테이블 맞은편에 앉은 루나를 불렀다.

“언니, 나 이거 도저히 이해가 안 돼. 이 BookShelf 프로젝트, 책을 새로 등록하고, 정보를 수정하고, 목록에서 삭제하는 기능이 전부 있잖아. 데이터가 어딘가에 저장되고 관리된다는 뜻인데, 어떻게 백엔드 코드가 하나도 없을 수가 있어? 혹시 우리가 모르는 숨겨진 서버가 따로 있는 거 아닐까?”

솔라의 목소리에는 당연한 사실을 부정당한 듯한 억울함이 섞여 있었다. 데이터를 생성하고(Create), 읽고(Read), 갱신하고(Update), 삭제하는(Delete) 기능, 이른바 CRUD가 있다면 데이터베이스와 통신하는 백엔드 서버가 있는 것은 거의 공식이나 다름없었다. 그런데 ‘백엔드 코드가 없다’니. 마치 엔진 없이 달리는 자동차를 설명하는 것 같았다.

루나는 솔라의 화면을 잠시 들여다보더니, 조용히 고개를 끄덕였다.

“그렇게 생각하는 게 자연스러워. 기능만 보면 당연히 서버가 있어야 할 것 같으니까. 그럼 이렇게 한번 상상해볼래? 솔라 네가 컴퓨터 메모장으로 오늘 할 일 목록을 쭉 적는다고 말이야.”

“메모장?”

갑작스러운 비유에 솔라가 되물었다.

“응. 할 일을 추가하고, 다 끝낸 일은 지우고, 내용을 수정할 수도 있겠지. 잘 작동하잖아.”

“그야 그렇지만….” 솔라는 루나의 의도를 따라가려 애썼다. “만약 실수로 저장 안 하고 꺼버리거나, 컴퓨터를 재부팅하면 애써 적어둔 목록이 전부 사라져 버리잖아. 그리고 다른 프로그램이 내 메모장 파일에 ‘세 번째 할 일 지워줘’ 같은 명령을 보낼 수도 없고.”

“바로 그거야.”

루나가 말을 받았다. “우리 BookShelf 앱도 똑같은 문제가 있어. 사용자가 책을 추가하면 그 정보는 어딘가에 영구적으로, 컴퓨터를 껐다 켜도 사라지지 않게 기록되어야 해. 그리고 우리 앱은 그 기록을 관리하는 곳에 ‘이 책 정보를 저장해줘’라거나 ‘저 책 정보를 가져와줘’ 같은 요청을 보낼 수 있어야 하고. 보통 그 복잡하고 중요한 역할을 하는 게 바로 백엔드 서버야.”

솔라는 “그치! 그러니까 백엔드가 있어야 한다니까!”라고 말하려다 입을 다물었다. 루나의 설명은 자신의 생각이 틀렸다고 말하는 대신, 그 생각이 왜 논리적인지를 먼저 인정해주고 있었다. 하지만 이야기는 거기서 끝나지 않을 터였다.

루나는 말을 이었다. “하지만 ‘백엔드 서버’를 만든다는 건, 도시를 건설하는 것과 비슷해. 길을 내고, 건물을 짓고, 수도관을 연결하는 것처럼 신경 쓸 게 아주 많아. 우리는 단지 책 몇 권을 저장하는 작은 집만 필요했는데 말이지. 그래서 도시를 건설하는 대신, 아주 영리한 조립식 주택을 쓰기로 한 거야.”

“조립식 주택?”

“응. 우리 프로젝트의 json-server라는 도구가 바로 그 조립식 주택이야. 이건 우리가 db.json이라고 이름 붙인 단순한 텍스트 파일을 하나 정해주면, 그 파일을 마치 진짜 데이터베이스 서버인 것처럼 행동하게 만들어줘. 외부에서 ‘책 목록을 보여줘’라는 요청이 오면 파일 내용을 읽어서 보여주고, ‘이 책을 추가해줘’라는 요청이 오면 파일에 새로운 내용을 적어 넣지. 스스로 데이터베이스인 척, API 서버인 척 연기하는 거야.”

그 순간, 솔라의 머릿속을 가로막고 있던 안개가 걷히는 듯했다. ‘숨겨진 서버’가 있긴 있었다. 하지만 그건 복잡한 코드로 짜인 거대한 시스템이 아니라, 정해진 규칙에 따라 단순한 파일 하나를 관리해주는 경량 도구, 일종의 ‘가짜 API 서버’였던 것이다.

“아…!”

솔라의 입에서 짧은 탄성이 터져 나왔다. “그래서, 그래서였구나! 우리가 직접 데이터베이스를 연결하고 API 하나하나 코드를 짜는 대신, json-server라는 도구가 그 모든 걸 흉내 내주고 있었던 거네. 프로젝트 설명에 무심코 지나쳤던 json-server라는 단어가 바로 이 모든 마법의 정체였어. 마법이 아니라, 정해진 역할을 하는 똑똑한 도구.”

솔라는 다시 노트북 화면의 그 문장을 보았다. BookShelf는 backend code 작성 없이 완성된 frontend 미니프로젝트다. 이제야 각 단어들이 제자리를 찾은 듯 편안하게 읽혔다. ‘백엔드 코드 없음’은 json-server가 대신해주었기에 가능한 선언이었다.

한 고비를 넘기자 새로운 질문이 고개를 들었다. 솔라는 생각에 잠겼다.

“좋아, 이제 json-server가 가짜 서버 역할을 한다는 건 명확히 알겠어. 그런데… 한 가지가 더 궁금해졌어. 내 눈앞에 보이는 이 화면, 그러니까 React와 Vite로 만든 앱에서 ‘저장’ 버튼을 딱 눌렀을 때, 그 신호가 어떻게 바다 건너 섬에 있는 것 같은 json-server까지 날아가서 ‘책 정보를 저장하라’는 명령이 되는 거지? 그 둘은 대체 어떻게 서로 대화하는 거야?“

2장: 화면과 데이터를 잇는 통로

솔라는 거실 테이블에 커다란 스케치북을 펼쳐놓고 있었다. 어젯밤 머릿속을 맴돌던 질문을 눈에 보이는 그림으로 옮겨보려는 참이었다. 스케치북 왼쪽에는 ‘사용자 화면 (React)’이라고 적힌 네모 상자를, 오른쪽에는 ‘데이터 저장소 (json-server)’라고 적힌 둥근 원을 그렸다. 두 도형은 꽤 멀리 떨어져 있었다. 마치 서로 다른 섬처럼.

이제 가장 중요한 단계가 남았다. 화면에서 사용자가 책 정보를 입력하고 ‘저장’ 버튼을 누르는 순간, 그 정보가 어떻게 데이터 저장소 섬까지 전달되는지를 그리는 것이었다. 솔라는 잠시 펜을 허공에 맴돌게 하다가, ‘사용자 화면’ 상자에서부터 굵은 화살표 하나를 길게 뽑아 ‘데이터 저장소’ 원을 향해 그었다. 하지만 그리자마자 무언가 이상하다는 느낌이 들었다. 화살표는 너무 단순하고 막연했다. 마치 지도에 서울과 부산을 표시하고 그저 선 하나를 쭉 그어놓은 것과 같았다. 고속도로인지, 철도인지, 항공로인지 알 수 없는 불분명한 선.

“으음, 이게 아닌데.”

솔라는 펜 끝으로 화살표 중간을 툭툭 건드렸다. “화면을 만드는 코드 안에서, ‘저장’ 버튼을 누르면 json-server의 특정 주소로 데이터를 보내라!’ 같은 복잡한 명령어가 잔뜩 들어있어야 할 것 같아. 그러면 화면을 만드는 코드랑 데이터를 보내는 코드가 뒤죽박죽 섞여서, 나중에 고치기도 엄청 힘들겠는데….”

그 혼잣말을 들은 루나가 솔라의 스케치북을 들여다보았다. 막막하게 그어진 화살표와 솔라의 고민 어린 표정을 번갈아 본 루나는, 프로젝트 파일 목록이 떠 있는 노트북 화면을 솔라 쪽으로 돌려주었다. 그리고 파일 트리에서 src 폴더 안의 한 파일을 손가락으로 가리켰다.

src/bookService.js

“솔라, 네 그림에는 한 가지 중요한 역할이 빠져있어. 만약 화면과 데이터 저장소가 서로 다른 언어를 쓰는 두 나라라고 상상해봐. 둘 사이에 직접 소리를 지르는 대신, 양쪽 언어에 모두 능통한 전문 통역사를 중간에 두면 어떨까?”

“통역사?”

“응. src/bookService.js 파일이 바로 그 통역사야. React와 Vite로 만든 화면은 그저 ‘이 책 좀 저장해주세요’ 하고 간단하게 요청만 하면 돼. 그럼 이 통역사가 그 요청을 받아서, 데이터 저장소가 알아들을 수 있는 전문적인 형식(HTTP 요청)으로 바꿔서 전달해주지. 반대로 저장소에서 데이터를 가져올 때도 마찬가지고.”

루나의 설명은 솔라의 막막한 화살표 위에 새로운 길을 그려주었다. ‘사용자 화면’과 ‘데이터 저장소’ 사이에 새로운 중간 경유지가 생긴 것이다. 솔라는 루나의 말을 곱씹으며 스케치북에 새로운 상자를 그렸다. ‘사용자 화면’과 ‘데이터 저장소’ 사이에 ‘bookService.js (통역사)’라는 이름의 작은 직사각형을 그려 넣었다.

그리고 아까 그었던 굵고 막연한 화살표를 지우개로 지웠다. 대신 새로운 화살표를 그렸다. 첫 번째 화살표는 ‘사용자 화면’에서 나와 ‘bookService.js’로 향했다. 그 위에는 ‘책 저장 요청!’이라고 적었다. 두 번째 화살표는 ‘bookService.js’에서 나와 ‘데이터 저장소’로 향했다. 그 위에는 ‘(전문용어로 번역된) 저장 명령 전달’이라고 썼다.

그림이 완성되자, 뒤엉켜 있던 생각의 실타래가 마법처럼 풀렸다. 이제 각자의 역할이 명확했다. 화면은 예쁘게 보이고 사용자의 행동을 받는 일에만 집중하면 된다. 데이터 통신이라는 복잡하고 기술적인 일은 bookService.js라는 전문가에게 전부 맡겨버리면 그만이었다. 코드가 섞일 일도, 복잡해질 걱정도 없었다.

“아하!”

솔라는 무릎을 탁 쳤다. “그래서 프로젝트 설명에 src/bookService.js가 ‘통로’라고 쓰여 있었구나! 화면과 데이터 저장소라는 두 공간을 안전하고 깔끔하게 이어주는 전용 통로였던 거야. 그냥 선 하나가 아니라, 역할이 분명한 하나의 건물이었네.”

솔라는 자신의 스케치북에 그려진 명쾌한 흐름도를 뿌듯하게 바라보았다. 데이터가 화면에서 출발해, 서비스라는 통로를 거쳐, 저장소에 안전하게 도착하는 여정이 한눈에 들어왔다. 이제 책 데이터를 등록하고 수정하는 원리는 완전히 이해됐다. 하지만 그 순간, 솔라의 시선이 프로젝트 기능 목록의 다른 항목에 꽂혔다. ‘AI를 이용한 책 표지 생성’.

단순히 데이터를 주고받는 것과는 차원이 다른 기능이었다. 솔라는 루나를 보며 새로운 질문을 던졌다.

“언니, 책 정보 CRUD는 이제 이 그림으로 완전히 이해했어. 그런데 표지를 만들어주는 AI 기능은 훨씬 복잡해 보이는데… 이것도 비슷한 원리로 작동하는 거야? AI 통역사 같은 게 또 있는 건가?“

3장: AI, 마법이 아닌 또 하나의 서비스

솔라는 다시 스케치북을 펼쳤다. 어제 그린 그림이 마음에 든 듯, 손가락으로 흐름을 한번 쓱 훑었다. ‘사용자 화면’에서 출발한 요청이 ‘bookService.js’라는 명확한 통로를 지나 ‘데이터 저장소’에 도착하는, 이제는 제법 익숙해진 길이었다. 이 구조 덕분에 책 정보를 저장하고 수정하는 원리가 명쾌하게 정리되었다. 하지만 만족감도 잠시, 솔라는 그림 옆 빈 공간에 펜을 가져다 대고는 이내 멈칫했다.

프로젝트의 또 다른 기능, ‘AI 책 표지 생성’을 이 그림에 어떻게 녹여내야 할지 막막했다. 솔라는 잠시 고민하다, 일단 아는 것부터 그려보기 시작했다. 왼쪽에는 똑같이 ‘사용자 화면’ 상자를 그렸다. 그리고 저 멀리 오른쪽에는 ‘AI 서버’라는 새로운 목적지를 그렸다. 문제는 그 사이였다. 데이터를 저장하는 것과는 차원이 다른, ‘창작’을 하는 AI 아닌가. 솔라는 이것이 json-server와는 전혀 다른, 훨씬 복잡하고 특별한 통로를 필요로 할 거라고 지레짐작했다. 어쩌면 우리가 모르는, AI 기능만을 위한 별도의 백엔드 서버가 또 숨어있는 건 아닐까?

“AI 통역사 같은 게 또 있는 거냐고 물었었지.”

솔라의 고민을 읽은 듯, 루나가 조용히 말을 걸었다. 루나는 솔라가 그린 엉거주춤한 그림을 보고는, 노트북을 돌려 다시 한번 파일 목록을 보여주었다. 이번에는 bookService.js 바로 아래에 있는 다른 파일이었다.

src/coverService.js

“솔라, 네가 어제 그린 그림 옆에 새로운 길을 하나 더 그려보자. 출발지는 같아. ‘사용자 화면’.”

루나의 말에, 솔라는 ‘사용자 화면’ 상자에서 아래쪽으로 새로운 화살표를 뽑아냈다.

“그리고 이 길에도 어제와 똑같은 ‘통역사’가 있어. 바로 src/coverService.js 파일이야. 화면은 그저 ‘이 책 제목으로 표지 만들어줘’라고 간단히 요청만 하면, 이 파일이 모든 복잡한 과정을 대신 처리해줘.”

솔라는 ‘사용자 화면’에서 뻗어 나온 화살표 끝에 ‘coverService.js’라는 새로운 통로 상자를 그렸다. 어제 그렸던 ‘bookService.js’ 바로 아래에 나란히 위치했다. 두 개의 통로가 마치 한 건물에 있는 별개의 출입문처럼 보였다.

“그럼… 이 통로의 목적지는 어디야?”

“OpenAI의 이미지 생성 API 서버.” 루나가 답했다. “우리가 직접 만든 서버가 아니라, OpenAI라는 회사가 만들어놓고 ‘누구나 규칙에 맞춰 사용료를 내면 쓸 수 있어요’하고 열어놓은 외부 서비스지.”

솔라는 ‘coverService.js’에서 화살표를 길게 뽑아 ‘OpenAI 서버 (외부)’라고 적힌 목적지로 연결했다. 그림이 완성되자, 솔라는 잠시 숨을 멈췄다. 두 개의 흐름도가 스케치북에 나란히 그려졌다.

  1. 책 데이터 관리: UI → bookService.jsjson-server (내부)
  2. AI 표지 생성: UI → src/coverService.jsOpenAI 서버 (외부)

두 그림은 놀라울 정도로 닮아 있었다. 출발지는 ‘UI’로 같았다. 중간에는 각자의 목적에 맞는 ‘서비스(통로)’ 파일이 있었다. 유일한 차이점은 최종 목적지가 프로젝트 ‘내부’의 가짜 서버냐, 아니면 OpenAI라는 회사에서 운영하는 ‘외부’의 진짜 서비스냐 하는 것뿐이었다. AI라고 해서 특별한 마법이 있는 게 아니었다. 그저 또 하나의, 목적지가 다를 뿐인 서비스 호출이었던 것이다.

“아…!”

깨달음이 파도처럼 밀려왔다. 솔라는 자신의 편견이 깨지는 소리를 들었다. “AI 기능이라고 해서 뭔가 완전히 다른 원리일 줄 알았어. 하지만 결국 이것도 프론트엔드 화면에서 출발해서, 정해진 통로를 거쳐, 약속된 서버와 대화하는 거였네. bookService와 구조적으로 똑같잖아!”

‘OpenAI browser-side service’라는 프로젝트 설명의 한 구절이 머릿속에서 환하게 빛났다. 브라우저 측에서, 즉 프론트엔드에서 직접 외부 서비스를 호출한다는 뜻이었다. 별도의 복잡한 백엔드를 거치는 게 아니라. React와 Vite로 만든 프론트엔드가 단순히 사용자가 보는 화면을 그리는 배우 역할만 하는 게 아니었다. 때로는 json-server를, 때로는 OpenAI를 지휘하며 전체 흐름을 만들어내는 오케스트라의 ‘조율자(오케스트레이터)’ 역할을 하고 있었던 것이다.

“바로 그거야.” 루나가 미소 지으며 말했다. “현대의 프론트엔드는 이렇게 여러 서비스들을 조율하는 역할을 점점 더 많이 하게 돼. 우리가 만든 작은 API든, 거대 기업의 AI 서비스든 가리지 않고 말이야.”

솔라는 이제 BookShelf 프로젝트가 단순한 기능의 나열이 아닌, 하나의 일관된 철학을 가진 아키텍처로 보이기 시작했다. 프론트엔드를 중심으로 모든 것을 조율하는 구조.

루나가 마지막 질문을 던졌다. “그럼, 이 구조를 완전히 이해했는지 확인해볼까? 만약 우리가 여기에 책 내용을 몇 줄 입력하면 AI가 핵심 문장을 요약해주는 ‘도서 요약’ 기능을 추가하고 싶다면, 솔라 넌 어떻게 할 것 같아? 이 구조에 맞춰서 말이야.”

솔라는 더 이상 망설이지 않았다. 그녀는 스케치북의 비어있는 마지막 공간에 자신 있게 펜을 가져갔다.

“간단해.” 솔라가 말하며 세 번째 흐름도를 그리기 시작했다. “먼저 src/summaryService.js 같은 새로운 통로를 하나 만들 거야. 그리고 화면의 ‘요약하기’ 버튼이 이 새로운 통로를 호출하도록 연결하겠지. 이 통로는 외부의 요약 전문 API 서버와 대화하는 방법을 알고 있을 거고. 그럼 우리 프로젝트의 ‘프론트엔드 중심’ 구조를 전혀 깨지 않고도 새로운 AI 기능을 얼마든지 더할 수 있어.”

스케치북에는 이제 UI라는 하나의 지휘자가 bookService, coverService, 그리고 가상의 summaryService라는 여러 통로를 통해 각기 다른 오케스트라 단원(서버)들을 지휘하는 모습이 선명하게 그려져 있었다. 솔라는 더 이상 기능 목록의 복잡함에 압도당하지 않았다. 대신, 그 기능들을 꿰뚫는 명확한 원리, 아름다운 질서를 발견한 참이었다.