Mini Project 4 08
잘 도는 프론트엔드가 남기는 백엔드 질문: 책임 경계의 확장
4차가 충분히 동작한다면 왜 5차에서 다시 backend를 붙여야 하는지 헷갈린다. 반대로 5차를 위해 4차를 미완성처럼 낮춰 말하는 것도 어색하다.
근거 · 4차 완성점/5차 전환 질문
1장: 4차의 ‘완성’은 무엇을 완성했나?
솔라가 노트북 화면의 한 문장을 노려보며 펜 끝으로 책상을 톡톡 두드리고 있었다. 프로젝트 다음 단계를 안내하는 문서였다. 그중에서도 유독 한 문장이 솔라의 발목을 잡았다.
“4차 BookShelf는 frontend 제품으로 완성도가 있지만, 바로 그 완성도 때문에 실제 backend API, domain persistence, auth relation, deployment boundary라는 다음 질문이 선명해진다.”
“이게 무슨 말이야?”
결국 솔라는 혼잣말을 내뱉고 말았다. 완성도가 높기 때문에 질문이 생긴다니. 앞뒤가 맞지 않는 말처럼 들렸다. 4차 프로젝트를 끝냈을 때, 솔라는 정말로 무언가를 ‘완성’했다고 생각했다. 사용자가 책을 추가하고, AI로 멋진 표지를 만들고, 저장된 책들 사이에서 검색과 추천까지 받는다. 화면은 매끄럽게 움직이고 모든 기능이 눈앞에서 완벽하게 동작했다. 그런데 이제 와서 그 완성이 다음 질문을 불러온다니, 마치 4차 프로젝트의 성취를 깎아내리는 것처럼 느껴져 마음이 복잡했다.
“뭐가 잘 안 풀려?”
소파에 앉아 책을 읽던 루나가 조용히 물었다. 솔라는 기다렸다는 듯이 화면을 가리키며 불퉁하게 말했다.
“언니, 이것 봐. 4차 프로젝트가 완성도가 높아서 다음 질문이 생긴대. 완성했으면 된 거지, 왜 자꾸 질문이 생긴다는 건지 모르겠어. 5차에서 백엔드를 붙이는 건 알겠는데, 그럼 4차는 미완성이었다는 뜻인가? 그것도 이상하잖아. 내 눈앞에선 모든 게 완벽하게 돌아갔는데.”
솔라의 말에는 자신이 만든 결과물에 대한 애정과 약간의 억울함이 섞여 있었다. 루나는 잠시 생각하더니 책을 덮고 솔라의 옆으로 다가왔다. 화면 속 문장을 잠시 들여다본 루나가 부드러운 목소리로 말했다.
“솔라 네 말이 맞아. 4차 프로젝트는 분명히 완성된 결과물이야. 특히 사용자가 눈으로 보고 직접 손으로 조작하는 ‘경험’을 아주 잘 만들어냈지. 칭찬이 맞아. 그럼 우리, 그 칭찬을 조금 다른 각도에서 볼까?”
루나는 의자를 끌어와 앉으며 새로운 판을 열었다.
“상상해 보자. 솔라 네가 만든 그 멋진 BookShelf 앱을, 너 혼자 쓰는 게 아니라… 천 명쯤 되는 사람들이 동시에 쓰기 시작하는 거야. 다들 자기만의 책을 추가하고 싶어 해.”
“천 명이나? 와, 그럼 진짜 성공한 거네!”
솔라의 얼굴에 잠시 화색이 돌았다. 자신의 코드가 수많은 사람에게 가치를 주는 상상. 언제나 가슴 뛰는 일이었다. 하지만 루나는 미소를 지으며 결정적인 질문을 던졌다.
“그럼. 아주 성공했지. 자, 지금 이 순간, 1초 동안 천 명이 각자 다른 책을 ‘저장’ 버튼을 눌렀어. 어떻게 될까?”
“어떻게… 되긴. 천 개의 책이 저장되겠지?”
솔라는 자신 있게 대답했지만, 곧바로 목소리가 작아졌다. 무언가 이상하다는 걸 직감했다. 4차 프로젝트에서 책 정보는 db.json이라는 파일 하나에 저장되었다. 솔라 혼자 쓸 때는 아무 문제 없었다. 하지만 천 명이 동시에 그 파일 하나에 내용을 추가하려고 달려든다면?
“아…”
솔라의 입에서 짧은 탄식이 터져 나왔다. 머릿속에 그림이 그려졌다. 여러 사람이 하나의 공책에 동시에 글씨를 쓰려는 것처럼, 서로의 내용을 덮어쓰거나 파일 자체가 망가져 버릴지도 모른다. 운이 좋아 순서대로 처리된다 해도, A라는 사람이 추가한 책이 B라는 사람의 책장에 보이는 이상한 상황이 펼쳐질 것이다. 애초에 ‘내 책장’과 ‘남의 책장’이라는 구분 자체가 없었다.
“그리고 내가 어제 추가한 책 목록을, 오늘 다른 컴퓨터에서 열면 그대로 있을까?”
루나의 추가 질문에 솔라는 고개를 저었다.
“아니. 내 컴퓨터 브라우저에만 저장되니까. 다른 기기에선 텅 빈 책장으로 보이겠지.”
그제야 솔라는 자신이 놓치고 있던 것을 깨달았다. 자신이 본 ‘완성’은 지극히 한정된 조건 속에서의 완성, 즉 ‘개발자 한 명이 자신의 컴퓨터에서 기능을 시연하는 관점’에서의 완성이었다. 모든 것이 완벽하게 돌아갔던 이유는, 오직 한 명의 사용자만이 예측 가능한 방식으로 시스템을 사용했기 때문이다.
솔라는 다시 화면 속 문장을 보았다.
“4차 BookShelf는 frontend 제품으로 완성도가 있지만…”
“아! ‘프론트엔드 제품으로서의 완성도’라는 말이… 딱 그 책임 범위만 한정해서 말하는 거였구나.”
솔라가 중얼거렸다. 화면을 설계하고, 사용자의 입력에 반응하고, 주어진 데이터를 예쁘게 보여주는 역할. 그 역할 안에서는 완벽했다. 하지만 그 책임의 경계를 넘어서는 순간, 즉 여러 사용자가 등장하고, 데이터가 영원히 보존되어야 하며, 어떤 기기에서든 동일한 경험을 제공해야 하는 ‘제품화 관점’에서는 수많은 질문이 터져 나올 수밖에 없었다.
‘완성’이라는 단어에 갇혀 있던 시야가 트이는 느낌이었다. 4차 프로젝트의 가치가 깎인 것이 아니었다. 오히려 그 완성도 높은 프론트엔드 경험 덕분에, 그것을 진짜 세상에 내놓기 위해 무엇이 더 필요한지가 명확하게 드러나고 있었다.
“이제 알겠어. 4차의 완성은 끝이 아니라, 다음 단계의 책임이 무엇인지 보여주는 이정표였네.”
개운해진 솔라의 표정을 보며 루나는 작게 고개를 끄덕였다. 하지만 솔라의 머릿속에는 방금 전의 사고 실험이 남긴 새로운 궁금증이 싹트고 있었다.
“그런데 언니, 좀 헷갈려. json-server랑 db.json도 어쨌든 내 요청에 응답해서 데이터를 주고받았잖아. 가짜였지만 API처럼 작동했는데, 왜 이게 실제 제품에서는 한계가 되는 거지? 진짜 백엔드 API는 정확히 뭐가 다른 거야?“
2장: Mock 데이터에서 실제 데이터로: json-server의 책임 경계
솔라의 노트북 화면에는 두 개의 창이 나란히 띄워져 있었다. 왼쪽은 db.json 파일의 내용이 보이는 코드 에디터, 오른쪽은 json-server가 로컬 주소에서 실행되고 있음을 알리는 터미널이었다. 솔라는 파일의 books 배열 안에 있는 JSON 객체 하나를 물끄러미 바라봤다. { "id": 1, "title": "...", "author": "..." }. 너무나 명료하고 단순한 구조였다.
지난번 루나와의 대화 이후, 솔라는 json-server가 왜 ‘진짜’가 아닌지 납득하기 위해 직접 코드를 다시 들여다보기로 했다. 프론트엔드에서 책 추가 요청을 보내면, json-server는 정말 마법처럼 이 db.json 파일에 한 줄을 추가해 주었다. 목록을 요청하면 이 파일의 내용을 고스란히 돌려주었다. 솔라가 경험한 것은 분명 ‘요청과 응답’이라는 API의 핵심 동작이었다. 가짜라고 하기엔 너무나 진짜처럼 그럴싸하게 움직였다. ‘경험’은 완벽했다. 그런데 왜 이것이 ‘실제’ 제품의 한계가 된다는 걸까.
“바로 그거야.”
솔라의 혼잣말을 들은 듯, 루나가 어느새 다가와 화면을 들여다보며 말했다. 루나는 솔라의 어깨너머로 db.json 파일을 가리켰다.
“솔라 네가 지금 느끼는 그 ‘그럴싸함’이 바로 json-server의 역할이자, 동시에 그것이 넘을 수 없는 경계이기도 해.”
“역할이자 경계라니? 그게 무슨 말이야?”
솔라가 묻자, 루나는 책상 위 포스트잇 한 장과 펜을 집어 들었다. 그러고는 식당의 주문서처럼 간단한 표를 그렸다. ‘손님 요청’, ‘웨이터’, ‘주방’ 세 칸으로 나뉜 표였다.
“4차 프로젝트에서 json-server는 아주 훌륭한 ‘웨이터’였어. 손님, 즉 프론트엔드가 ‘책 목록 주세요’라고 주문하면, 주방, 즉 db.json 파일에 가서 내용을 그대로 가져다줬지. ‘이 책 추가해주세요’라고 하면, 주방 선반에 책을 올려두고 왔고. 정확하고 빠른 웨이터야.”
루나는 ‘웨이터’ 칸에 json-server라고 적었다. 솔라는 고개를 끄덕였다. 완벽한 비유였다.
“그런데 솔라야, 만약 손님이 이미 있는 책을 또 추가해달라고 주문하면 어떻게 될까? 예를 들어 ‘해리포터 1권’이 책장에 있는데, 또 ‘해리포터 1권’을 추가해달라고 하는 거야.”
“어떻게 되긴. 그냥 추가되겠지. db.json에 똑같은 책이 두 개 생길 거야.”
솔라는 자신 있게 대답했다. json-server는 그런 걸 판단하지 않으니까.
“맞아. 훌륭한 웨이터는 손님의 주문을 판단하지 않고 그대로 주방에 전달하니까. 하지만 ‘주방장’이라면 어떨까? 주방장은 ‘같은 책은 두 번 등록할 수 없습니다’라는 레시피, 즉 가게의 규칙을 알고 있겠지. 그리고 웨이터에게 ‘이 주문은 받을 수 없으니 손님에게 알려드려’라고 돌려보낼 거야.”
루나는 ‘주방’ 칸 위에 작게 ‘주방장(규칙)‘이라고 덧붙였다.
순간 솔라의 머릿속에 번개가 쳤다. json-server는 데이터가 ‘책’이라는 사실조차 몰랐다. 그저 JSON 형식의 텍스트 덩어리로 취급할 뿐이었다. ‘책 제목은 반드시 있어야 한다’, ‘같은 책은 중복 등록할 수 없다’와 같은, 이 서비스의 핵심적인 규칙, 즉 **도메인 규칙(Domain Rule)**을 적용할 주체가 없었던 것이다.
루나는 말을 이었다.
“한 걸음 더 나아가 볼까? 만약에, 천 명의 손님이 동시에 각자 다른 책을 추가해달라고 주문하면? 우리 웨이터는 천 개의 주문서를 들고 동시에 주방 문 하나로 달려드는 셈이야. 주방 선반은 하나뿐인데 말이지.”
지난번 사고 실험에서 어렴풋이 느꼈던 문제가 선명해졌다. db.json이라는 단 하나의 파일을 동시에 수정하려는 시도는 데이터를 엉망으로 만들거나, 누군가의 요청을 누락시킬 수밖에 없었다. 실제 백엔드는 이런 동시 요청들을 안전하게 줄 세우고, 순서대로 처리하며, 처리 후에는 데이터가 영원히 보존되도록 데이터베이스라는 훨씬 더 견고한 저장소에 기록한다. 데이터의 **영속성(Persistence)**과 **일관성(Consistency)**을 책임지는 것이다.
“아…”
솔라는 탄식과 함께 자신이 무엇을 착각했는지 깨달았다. 자신은 프론트엔드 개발자로서 ‘요청을 보내면 응답이 온다’는 **API 계약(API Contract)**의 겉모습, 즉 ‘경험’에만 집중하고 있었다. 하지만 실제 백엔드의 책임은 그 계약을 이행하기 위해 보이지 않는 곳에서 데이터의 규칙을 강제하고, 수많은 요청을 안전하게 처리하며, 결과를 영속적으로 보관하는 데 있었다. json-server는 이 모든 책임을 건너뛴 채, 그저 약속된 형태의 응답을 흉내 내는 모의 배우(mock)에 불과했다.
솔라는 다시 한번 프로젝트 문서의 그 문장을 떠올렸다.
“json-server/db.json이 제공한 REST 경험을 실제 backend API contract와 domain rule로 옮겨야 한다.”
이제 이 문장은 더 이상 솔라를 혼란스럽게 하지 않았다. ‘경험을 옮긴다’는 말의 의미를 명확히 알 것 같았다. 프론트엔드와 json-server가 주고받았던 약속의 형식은 유지하되, 그 약속을 이행하는 주체를 규칙도, 책임감도 없는 ‘웨이터’에서 모든 것을 책임지는 ‘주방장’으로 교체하라는 뜻이었다. 4차의 완성은 5차에서 만들어야 할 진짜 백엔드 API가 어떤 모습이어야 하는지 알려주는 훌륭한 청사진이었던 셈이다.
“그럼 데이터 관련 처리는 모두 백엔드, 즉 주방장에게 맡겨야 한다는 거구나. 이제 확실히 알겠어.”
자신감을 되찾은 솔라가 말했다.
“책을 추가하고, 목록을 보고, 검색하는 건 모두 데이터에 대한 거니까. 아, 그런데 AI 표지 생성은 좀 다른 것 같아. 그건 그냥 프론트엔드에서 OpenAI API를 호출하고 결과를 받아와서 보여주는 거잖아. 데이터베이스에 뭘 저장하고 말고 할 게 없으니까, 이건 백엔드 책임이 아니지?”
솔라의 말에 루나는 잠시 생각에 잠기는 듯하더니, 조용히 되물었다.
“그 OpenAI API를 호출할 때 필요한 API 키는, 어디에 어떻게 입력했지?“
3장: API 키는 왜 숨겨야 하는가? 보안 책임의 전환
솔라는 자신의 주장을 증명이라도 하듯, 4차 프로젝트의 소스 코드를 열었다. 화면에는 AI 표지 생성을 담당하는 AIGenerateCover.jsx 파일의 내용이 나타났다. 사용자가 책 제목을 입력하면, OpenAI API를 호출해서 이미지 URL을 받아오는, 간결하고 우아한 코드였다. 솔라는 이전 대화의 끝에서 던졌던 자신의 말을 되새겼다. 데이터베이스와 상관없는 이 기능만큼은 온전히 프론트엔드의 영역이라고.
“이것 봐, 언니. 여긴 데이터베이스에 뭘 저장하거나 복잡한 규칙을 따질 필요가 없어. 그냥 사용자한테 API 키를 입력받아서, 그걸로 OpenAI에 요청 보내고, 결과 이미지를 보여주면 끝이야. 아주 깔끔한 프론트엔드 책임이지.”
솔라의 목소리에는 이 간결한 설계에 대한 자부심이 묻어났다. 그녀는 fetch 함수가 포함된 줄을 손가락으로 가리켰다. json-server와는 달리, 이 부분은 외부 서비스와 직접 통신하며 ‘진짜’ 일을 하고 있었다. 백엔드의 도움이 필요 없는 독립적인 기능처럼 보였다.
루나는 말없이 솔라의 노트북 화면을 들여다보았다. 코드의 논리를 훑어본 루나는, 잠시 후 브라우저를 열어달라고 부탁했다. 그리고는 깃허브 주소를 입력했다. 화면에는 솔라가 작업했던 4차 BookShelf 프로젝트의 공개 저장소(Public Repository) 페이지가 나타났다. 프로젝트의 모든 코드가 누구나 볼 수 있도록 공개되어 있었다.
루나가 조용히 말했다. “이 코드, 지금 이 순간에도 전 세계 누구나 볼 수 있는 거지? 그럼 아까 네가 가리켰던 그 파일로 한번 가볼래?”
솔라는 잠시 머뭇거리다, 루나가 말한 대로 저장소에서 AIGenerateCover.jsx 파일을 찾아 클릭했다. 방금 전 로컬 편집기에서 보던 코드와 똑같은 내용이 하얀 웹페이지 배경 위에 나타났다. 코드의 각 줄이 선명하게 보였다. 평소에는 뿌듯하게만 느껴졌던 공개된 코드가, 오늘따라 어쩐지 발가벗겨진 듯한 느낌을 주었다.
루나가 결정적인 질문을 던졌다.
“이 코드를 실행하는 브라우저가 OpenAI API를 호출하려면, 반드시 필요한 게 딱 하나 있지. 뭘까?”
“API 키…겠지.”
솔라는 당연하다는 듯 대답했지만, 심장이 쿵 하고 내려앉는 것을 느꼈다. 공개된 코드. 그리고 API 키. 두 단어가 머릿속에서 충돌했다.
“맞아. 그럼 이 BookShelf가 실제 제품이 되었다고 상상해 보자. 두 가지 방법이 있겠네. 첫째, 모든 사용자에게 각자 OpenAI API 키를 발급받아서 입력하라고 안내한다. 둘째, 우리 회사 이름으로 발급받은 하나의 키를 앱에 심어놓고 모든 사용자가 쓰게 한다.”
루나는 두 가지 선택지를 제시했다. 솔라는 곧바로 첫 번째 방법의 문제점을 떠올렸다. AI 표지 생성이 이 앱의 핵심 기능인데, 사용자가 직접 다른 사이트에 가서 회원가입하고, 키를 발급받고, 그걸 복사해서 붙여 넣어야 한다면? 열에 아홉은 그 과정에서 지쳐서 앱 사용을 포기할 것이다. 제품의 매력이 급격히 떨어진다.
그렇다면 남은 것은 두 번째 방법. 회사 키를 사용하는 것이다. 하지만…
”…회사 키를 앱에 심어놓으면, 그 키가 브라우저로 전송된다는 거잖아. 그럼…”
솔라의 말끝이 흐려졌다. 땀이 나는 것 같았다. 공개된 소스 코드를 통해 누구나 앱의 작동 방식을 알 수 있다. 개발자 도구를 열어 네트워크 요청을 몇 번만 들여다보면, 회사의 OpenAI API 키가 고스란히 노출될 것이다.
그것은 마치, 가게의 공용 계산을 위해 점원에게 건네준 법인 신용카드의 번호와 CVC 코드를, 가게 앞 대형 스크린에 24시간 내내 띄워놓는 것과 같았다. 누군가 그 키를 훔쳐서 자신의 서비스에 사용하거나, 비트코인을 채굴하거나, 온갖 종류의 이미지 수만 개를 생성하며 막대한 비용을 발생시켜도 막을 방법이 없다. 다음 달 청구서가 날아오기 전까지는 아무도 그 사실을 모를 수도 있다.
편리하고 효율적이라고 생각했던 클라이언트 측 API 호출이, 사실은 엄청난 보안 구멍이자 비용 폭탄이 될 수 있는 시한폭탄이었던 것이다. 책임의 소재가 완전히 잘못되었다.
“알겠어… 알겠어, 이제. API 키 같은 민감한 정보는 절대 프론트엔드에 두면 안 되는 거였어. 이건 백엔드의 책임이었네.”
솔라가 탄식하며 말했다. ‘신중히 다뤄야 한다’는 문서의 문장이 이제야 명확한 의미로 다가왔다. 그것은 조심하라는 경고가 아니라, 아예 책임의 주체를 바꾸라는 지시였다.
루나는 고개를 끄덕이며, 종이에 간단한 그림을 그리기 시작했다. [사용자 브라우저] → [우리의 백엔드 서버] → [OpenAI API].
“그래서 우리에겐 믿을 수 있는 대리인이 필요한 거야. 사용자의 브라우저는 우리 백엔드 서버에 ‘표지 만들어줘’라고만 요청하는 거지. API 키는 우리 서버 안에 안전하게 숨겨두고. 그럼 우리 서버가 대리인, 즉 **서버 사이드 프록시(Server-side Proxy)**가 되어서 OpenAI에 요청을 보내고, 결과를 받아서 다시 사용자에게 전달해 주는 거야. 이렇게 하면 API 키는 절대 외부로 노출되지 않아.”
프론트엔드의 책임은 ‘AI 표지 생성을 요청한다’까지였다. 그 요청을 받아 민감한 정보를 사용해 외부 서비스와 통신하고, 결과를 안전하게 돌려주는 것은 온전히 백엔드의 책임으로 확장되어야 했다. 데이터뿐만 아니라, 외부와의 연결에 대한 보안 책임까지도 백엔드로 넘어가고 있었다.
“데이터 처리도 백엔드, API 키 보안도 백엔드. 점점 내 BookShelf 앱이 혼자서는 할 수 있는 게 없어지는 기분이야.”
솔라는 허탈하게 웃으며 중얼거렸다. 그러다 문득, 자신의 앱 화면을 다시 떠올렸다.
“아, 그래도 이건 괜찮지 않을까? 내가 설정한 다크 모드나, 책 정렬 순서 같은 거 말이야. 이건 그냥 내 컴퓨터 브라우저에만 저장되는 개인 설정이잖아. localStorage에 저장해 뒀는데, 다른 사람이 볼 일도 없고, 훔쳐갈 민감한 정보도 아니니까. 이건 정말로 백엔드가 필요 없겠지?“
4장: 개인화와 확장성: localStorage의 한계와 사용자 계정
세 번의 대화를 거치며 솔라는 조금 지쳐 있었다. 데이터 규칙, API 보안. 프론트엔드 개발자의 자부심으로 쌓아 올린 BookShelf 앱의 멋진 기능들이 하나둘 백엔드의 책임 영역으로 넘어가는 것을 속수무책으로 지켜본 기분이었다. 허탈하게 웃던 솔라는 자신의 마지막 보루를 떠올렸다.
솔라는 노트북 옆에 놓여 있던 자신의 스마트폰을 집어 들었다. 그리곤, 개발 서버 주소를 입력해 BookShelf 앱을 열었다. 하얀 화면이 잠시 로딩되더니, 익숙한 책장 앱이 나타났다. 하지만 어딘가 낯설었다. 노트북 화면에서는 분명 다크 모드를 적용해 검은 배경이 펼쳐져 있었는데, 스마트폰 화면은 눈부신 흰색이었다. 가장 결정적으로, 그동안 정성껏 추가했던 책들이 단 한 권도 보이지 않는 텅 빈 책장이었다.
물론 솔라는 그 이유를 기술적으로는 알고 있었다. 다크 모드 설정이나 책 정렬 순서 같은 개인화 정보는 브라우저의 localStorage에 저장된다. localStorage는 이름 그대로 해당 기기, 해당 브라우저에만 종속되는 작은 저장 공간이다. 노트북 크롬 브라우저의 localStorage와 스마트폰 크롬 브라우저의 localStorage는 완전히 별개의 공간이다. 하지만 머리로 아는 것과 눈앞의 현실이 주는 이질감은 차원이 달랐다. ‘내’가 만든 ‘내’ 책장 앱인데, 기기가 달라졌다는 이유만으로 나를 알아보지 못하는 낯선 존재가 되어버린 것이다.
“바로 그거네.”
솔라의 미간에 잡힌 미세한 주름을 포착한 듯, 루나가 조용히 입을 열었다. 루나는 노트북 화면의 다크 모드 책장과, 스마트폰 화면의 텅 빈 라이트 모드 책장을 번갈아 가리켰다.
“솔라 네가 지금 느끼는 그 배신감. 그게 바로 localStorage를 이용한 개인화의 명확한 한계야.”
“배신감까지는 아니지만… 좀 허무하긴 하네. 이건 보안 문제도 아니고, 여러 사람이 동시에 쓰는 문제도 아니잖아. 그냥 나 혼자 쓰는 내 설정인데. 이것마저 백엔드가 필요하다고 하면, 프론트엔드는 정말 껍데기만 남는 거 아닐까?”
솔라의 목소리에는 서운함이 묻어났다. 프론트엔드의 독립성을 지키려는 마지막 항변이었다. localStorage는 분명 민감한 정보도 아니고, 다른 사람과 공유할 필요도 없는, 지극히 개인적인 데이터를 다루기에 완벽한 도구처럼 보였다.
루나는 말없이 책상 위의 포스트잇 두 장을 떼어냈다. 한 장에는 ‘노트북’이라고 쓰고, 다른 한 장에는 ‘스마트폰’이라고 썼다. 그러고는 두 포스트잇을 멀찍이 떨어뜨려 놓았다.
“이건 두 개의 독립된 섬이야. 각 섬에는 ‘localStorage’라는 이름의 보물창고가 하나씩 있지. 노트북 섬의 창고에 보물을 넣어두고, 스마트폰 섬에 가서 그 보물을 찾으려고 하면, 찾을 수 있을까?”
“당연히 없겠지. 애초에 다른 섬이니까.”
“맞아. 지금 솔라 네가 한 경험이 바로 그거야. 다크 모드 설정, 책 정렬 순서 같은 보물은 노트북 섬에만 묻혀 있어. 그래서 스마트폰 섬은 그 보물의 존재 자체를 모르지.”
솔라는 고개를 끄덕였다. 너무나 당연한 비유였다.
“그럼, 이 두 섬에 사는 ‘솔라’라는 주민이 언제나 같은 보물을 사용하게 하려면 어떻게 해야 할까? 섬을 옮겨 다닐 때마다 보물을 새로 만들거나, 힘들게 배에 싣고 옮겨 다녀야 할까?”
루나의 질문에 솔라는 잠시 생각에 잠겼다. 이 섬에서 저 섬으로 이사할 때마다 짐을 다 챙겨가는 건 너무 번거롭다. 더 좋은 방법이 있을 것 같았다.
”…아니. 차라리 두 섬의 중간쯤에, 아무 데도 속하지 않은 중립적인 공간에 ‘솔라 전용 창고’를 하나 만드는 게 낫겠어. 그리고 노트북 섬에 있든, 스마트폰 섬에 있든, 필요할 때마다 그 중앙 창고에 접속해서 보물을 꺼내 쓰는 거지.”
말을 내뱉는 순간, 솔라는 자신이 한 말의 의미를 깨달았다. 두 섬의 중간. 아무 데도 속하지 않은 중립적인 공간. 필요할 때마다 접속해서 쓰는 중앙 창고. 그것은 다름 아닌 서버였다. 그리고 그 창고가 ‘솔라 전용’임을 증명하는 열쇠는 바로 ‘사용자 계정’이었다.
localStorage에 저장된 개인화는 ‘기기’에 귀속된 반쪽짜리 개인화였다. 진정한 개인화는 사용자가 어떤 기기에서 접속하든 동일한 경험을 제공하는 것, 즉 데이터와 설정이 ‘사람’을 따라다니는 것이었다. 이를 위해서는 사용자를 식별하고(인증), 사용자별 데이터를 안전하게 보관하고 제공하는(서버 저장소) 책임 주체가 필요했다. 그 책임 역시 백엔드의 몫이었다.
솔라는 프로젝트 문서의 한 구절을 다시 떠올렸다.
“localStorage 개인화는 사용자 계정과 서버 저장 관계로 확장될 수 있다.”
이제 이 문장은 더 이상 프론트엔드의 영역을 빼앗는 것처럼 들리지 않았다. 오히려 localStorage로 구현했던 개인화라는 ‘경험’의 프로토타입을, ‘제품’ 수준으로 완성시키기 위한 자연스러운 진화 경로를 안내하는 문장으로 읽혔다. ‘확장’이라는 단어의 의미가 명확해졌다. 그것은 기능의 확장이 아니라, 개인화라는 경험이 미치는 범위, 즉 책임의 경계가 ‘기기’에서 ‘사용자’로 확장되는 것을 의미했다. 이 확장을 위해서는 **사용자별 영속성(User-specific Persistence)**을 보장하는 서버의 역할이 필수적이었다.
“알겠어. 진짜 ‘개인화’는 내 설정이 나를 따라다니는 거였구나. 내가 기계를 따라다니는 게 아니라. 그러려면 내 모든 활동과 설정이 기록되는 본진이 필요하고, 그 본진이 바로 백엔드 서버였네.”
솔라는 노트북과 스마트폰을 번갈아 보며 말했다. 두 개의 분리된 섬이 아니라, 하나의 중앙 서버에 연결된 두 개의 터미널로 보이기 시작했다. 프론트엔드의 역할이 축소된 것이 아니었다. 사용자와 가장 가까운 곳에서 최고의 경험을 제공한다는 프론트엔드의 핵심 역할은 그대로였다. 다만, 그 최고의 경험을 어떤 상황에서든 일관되게 제공하기 위해, 보이지 않는 곳에서 더 넓은 책임을 짊어져 줄 든든한 파트너가 필요하다는 사실을 깨달았을 뿐이다.
자신의 BookShelf 앱이 점점 더 복잡하고 거대한 시스템으로 변모하고 있다는 사실에 조금은 막막함이 느껴졌다. 솔라는 머릿속으로 새로운 시스템의 그림을 그렸다. 사용자 화면을 그리는 프론트엔드 서버, 데이터와 비즈니스 로직을 처리하는 백엔드 서버, 그리고 데이터를 영구적으로 저장하는 데이터베이스. 최소 세 개의 구성 요소가 서로 통신해야 한다.
그때, 또 다른 종류의 질문이 고개를 들었다.
“언니, 그럼 이제 내 BookShelf는 프론트엔드, 백엔드, 데이터베이스라는 세 부분으로 나뉜 거네. 내 컴퓨터에서 개발할 때는 다 같이 있으니까 괜찮은데… 이걸 실제 인터넷 세상에 배포하면 프론트엔드는 my-bookshelf.com 같은 주소에, 백엔드는 api.my-bookshelf.com 같은 다른 주소에 놓이게 되는 거 아냐? 서로 다른 곳에 사는 애들이 어떻게 안전하게 통신하지? 당장 CORS 오류부터 날 것 같은데… 데이터베이스 접속 정보 같은 비밀번호는 또 어디에 둬야 안전한 거야?“
5장: 현실 세계의 배포: 책임 경계 확장의 최종 퍼즐
솔라의 책상 위에는 그녀가 막 그려낸 시스템의 지도가 놓여 있었다. 하얀 종이 위, 펜으로 그린 투박한 그림이었다. 한쪽에는 ‘내 컴퓨터’라는 커다란 원 안에 [프론트엔드], [백엔드], [DB] 세 개의 상자가 옹기종기 모여 있었다. 화살표들은 상자들 사이를 자유롭게 오갔다. 모든 것이 하나의 울타리 안에서 평화롭게 공존하는 모습이었다.
그 옆에는 ‘현실 세계’라고 이름 붙인 또 다른 그림이 있었다. 여기서는 상자들이 각자 멀리 떨어진 섬처럼 그려져 있었다. [프론트엔드] 상자는 my-bookshelf.com이라는 주소 위에, [백엔드] 상자는 api.my-bookshelf.com이라는 다른 주소 위에 놓였다. [DB] 상자는 아예 외부와 단절된 채 백엔드 뒤에 숨어 있었다. 솔라는 프론트엔드 섬에서 백엔드 섬으로 향하는 화살표 위에 커다랗고 붉은 물음표를 그려 넣었다. 분명 길은 있는데, 건널 수 없는 바다처럼 느껴졌다.
지난 대화들을 통해 데이터, 보안, 사용자 정보의 책임이 왜 백엔드로 넘어가야 하는지는 분명해졌다. 하지만 그 결과로 분리된 시스템의 각 조각들을 어떻게 다시 안전하게 연결해야 하는지에 대한 질문은, 이전과는 전혀 다른 종류의 막막함으로 다가왔다. 내 컴퓨터 안에서 한 가족처럼 지내던 구성원들이 이제 각자 다른 주소로 이사 간 셈이다. 이들이 서로를 어떻게 알아보고, 어떻게 믿고 대화할 수 있을까?
“이 그림을 보니까 더 확실해졌어.”
솔라가 중얼거렸다. 그녀의 시선은 붉은 물음표에 고정되어 있었다.
“내 컴퓨터에선 다 같은 ‘localhost’라는 한 지붕 아래 살았으니까, 옆집에 말 걸 듯이 통신하면 됐어. 하지만 현실 세계에선 프론트엔드와 백엔드가 사는 도시가 달라. 브라우저 보안 정책 때문에 my-bookshelf.com에서 api.my-bookshelf.com으로 보내는 요청은 바로 거절당할 거야. CORS 오류. 그리고 백엔드가 데이터베이스에 접속할 때 필요한 비밀번호는… 이사 가는 집에 떡하니 붙여둘 수도 없고. 정말 어디에 둬야 안전한 거지?”
솔라의 질문은 더 이상 ‘왜 백엔드가 필요한가’가 아니었다. ‘분리된 백엔드를 어떻게 운영해야 하는가’라는, 훨씬 더 현실적인 문제로 넘어와 있었다.
루나는 솔라가 그린 두 개의 지도를 조용히 들여다보았다. 개발 환경과 배포 환경의 차이를 이보다 더 명확하게 보여주는 그림은 없었다. 루나는 ‘현실 세계’ 그림에서 백엔드 상자를 손가락으로 가리켰다.
“솔라, 4차 프로젝트에서 백엔드의 역할은 뭐였지? json-server 말이야.”
“그냥 웨이터. 프론트엔드가 달라는 대로 db.json 파일을 읽어서 전달해 주는 역할.”
“맞아. 그런데 이 새로운 그림에서 백엔드는 더 이상 단순한 웨이터가 아니야. 이 섬의 ‘총독’이자 ‘경비대장’이 되어야 해.”
“총독? 경비대장?”
솔라가 되물었다. 너무 거창한 비유처럼 들렸다.
“생각해 봐. api.my-bookshelf.com이라는 섬에 아무나 드나들게 할 순 없잖아. 경비대장은 성문에서 방문객의 신분증을 확인하고, 허가받은 사람만 들여보내야 해. ‘아, my-bookshelf.com에서 오셨군요. 당신은 등록된 아군이니 통과.‘라고 말해주는 거지. 이게 바로 백엔드 서버가 CORS 정책을 설정해서 특정 출처(origin)의 요청만 허용하는 역할이야. 프론트엔드가 마음대로 할 수 있는 게 아니라, 백엔드가 허락해야만 가능한 통신이지.”
솔라의 머릿속에서 브라우저가 ‘저기 말 걸어도 돼요?‘라고 묻는 OPTIONS 요청(preflight request)을 보내고, 백엔드 서버가 ‘그래, 괜찮아’라고 응답 헤더를 보내주는 장면이 그려졌다. 그제야 CORS 오류가 단순히 귀찮은 문제가 아니라, 서버가 자신의 영토를 지키는 주체적인 보안 활동의 일부임을 깨달았다. 책임의 소재가 명확해졌다.
“그럼 데이터베이스 비밀번호는? 그건 총독의 금고 열쇠 같은 거네.”
솔라가 스스로 질문을 이어갔다.
“맞아. 그 열쇠를 아무나 볼 수 있는 코드에 적어두면 안 되겠지. 총독만이 아는 비밀스러운 장소, 즉 배포되는 서버 환경에만 주입되는 환경 변수(Environment Variable) 같은 곳에 보관해야 해. 백엔드 애플리케이션은 실행될 때, 외부 세상과 격리된 그 환경에서만 비밀번호를 꺼내서 데이터베이스에 접속하는 거야. 프론트엔드는 그 비밀번호의 존재 자체를 알 필요도 없고, 알아서도 안 돼.”
이제 모든 조각이 맞춰지고 있었다. 프론트엔드와 백엔드의 분리는 단순히 코드를 나누는 물리적 행위가 아니었다. 각자가 맡은 역할과 환경에 맞춰 책임의 경계를 명확히 긋는 과정이었다. 프론트엔드는 사용자에게 최상의 경험을 제공하는 책임을, 백엔드는 시스템의 핵심 자산(데이터, 비즈니스 규칙, 민감 정보)을 보호하고 관리하며, 전체 시스템의 안정적인 운영을 책임지는 역할을 맡는다. 이것이 바로 **배포 환경 설정 및 운영 책임(Deployment & Operational Responsibility)**이었다.
솔라는 자신이 처음 프로젝트 문서를 보고 혼란스러워했던 순간을 떠올렸다. 그리고 천천히, 그 문장의 의미를 되짚었다.
“4차 BookShelf는 frontend 제품으로 완성도가 있지만, 바로 그 완성도 때문에 실제 backend API, domain persistence, auth relation, deployment boundary라는 다음 질문이 선명해진다.”
솔라는 펜을 들어 자신이 그린 ‘현실 세계’ 지도 위, 백엔드 상자 옆에 주석을 달기 시작했다.
backend API&domain persistence: 데이터 규칙을 강제하고 영속성을 보장하는 ‘주방장’의 역할. (Ch.2)auth relation: API 키를 안전하게 보관하고, 사용자별 데이터를 관리하는 ‘보안 대리인’이자 ‘개인 창고지기’의 역할. (Ch.3, Ch.4)deployment boundary: 그리고 오늘 깨달은 것. CORS 정책으로 통신을 제어하고, DB 접속 정보 같은 비밀을 관리하는 ‘총독’이자 ‘경비대장’의 역할.
네 개의 질문은 결국 하나의 방향을 가리키고 있었다. ‘책임 경계의 확장’. 4차 프로젝트의 프론트엔드가 사용자 경험이라는 자신의 책임을 완벽하게 수행했기 때문에, 그 경험을 현실 세계에서 지탱해 줄 더 넓고 굳건한 책임의 영역이 필요하다는 사실이 역설적으로 드러난 것이다.
“이제야 알겠어.”
솔라가 완성된 지도를 보며 말했다.
“4차의 완성이 미완성이었던 게 아니었어. 4차 프론트엔드가 자기 역할을 너무 잘 해내서, 그 빛나는 결과물을 지키기 위한 ‘요새’가 필요하다는 걸 증명한 거였어. 5차 프로젝트는 4차의 기능을 반복하는 게 아니라, 4차가 필요로 하는 그 요새를 짓는 거였네.”
솔라의 시선은 더 이상 혼란스럽지 않았다. 눈앞의 복잡한 시스템 지도는 이제 넘어야 할 장애물이 아니라, 자신이 만들어 갈 견고한 제품의 청사진으로 보였다. 잘 만든 프론트엔드는 끝이 아니라, 더 큰 시스템을 향한 가장 선명한 첫걸음이었다.