Mini Project 5 01
4차 BookShelf에서 5차 백엔드 시스템으로: 확장 지도의 핵심 이해
4차와 5차 모두 도서관리와 AI 표지를 말하기 때문에 같은 프로젝트를 한 번 더 만든 것처럼 보인다. 무엇이 유지되고 무엇이 바뀌었는지 먼저 나누지 않으면 이후 글이 전부 섞인다.
근거 · 4차 frontend 산출물/5차 backend 확장 지도
1장: 4차 BookShelf: 이미 완성된 프런트엔드
솔라의 손가락이 노트북 트랙패드 위에서 잠시 멈칫했다. 화면에는 두 개의 프로젝트 폴더가 나란히 열려 있었다. ‘4차-bookshelf-frontend’, 그리고 ‘5차-bookshelf-system’. 이름도 비슷하고, 두 프로젝트 모두 책을 관리하고 AI로 표지를 만들어주는 기능을 담고 있었다. 프로젝트 설명 문서를 훑어볼수록 솔라의 미간이 좁아졌다. 마치 똑같은 숙제를 두 번 한 것처럼 느껴졌다.
“언니, 이거 좀 이상하지 않아?”
솔라가 화면을 가리키며 거실 소파에 앉아 있던 루나를 불렀다.
“4차 프로젝트랑 5차 프로젝트, 이름도 거의 같고 기능도 겹쳐. AI 표지 생성, 책 검색, 추천 UI… 4차에서 이미 다 한 거잖아. 그런데 5차에서 이걸 왜 또 만든 거지? 그냥 기능 몇 개 추가하고 이름만 바꿔서 반복한 것 같아.”
솔라의 목소리에는 비슷한 기능을 다시 구현하는 것은 비효율적이라는, 명백한 불만이 섞여 있었다. 루나는 솔라의 노트북 화면을 잠시 들여다보더니, 대답 대신 질문을 던졌다.
“정말 똑같아 보여? 그럼 4차 프로젝트 폴더 안을 다시 한번 볼까. 우리가 만들었던 데이터는 전부 어디에 있었지?”
그 말에 솔라는 잠시 생각에 잠겼다. 데이터라니. 분명 책 정보를 추가하고 삭제하는 기능이 있었으니 어딘가에 저장되었을 터였다. 솔라는 ‘4-bookshelf-frontend’ 폴더를 더블 클릭하고 그 안의 파일 목록을 훑어 내려갔다. src 폴더, public 폴더, package.json… 수많은 설정 파일과 소스 코드 파일 사이에서 눈에 익은 이름 하나가 보였다.
db.json.
“아.”
솔라의 입에서 짧은 탄성이 터져 나왔다. 그 파일을 클릭하자, 복잡한 코드 대신 단순한 텍스트가 화면을 채웠다.
{
"books": [
{
"id": 1,
"title": "클린 코드",
"author": "로버트 C. 마틴",
"coverImageUrl": "...",
"tags": ["프로그래밍", "소프트웨어 장인정신"]
},
{
"id": 2,
"title": "객체지향의 사실과 오해",
"author": "조영호",
"coverImageUrl": "...",
"tags": ["객체지향", "모델링"]
}
]
}
마치 장난감 블록 상자 안을 들여다보는 기분이었다. 책 정보가 보기 좋게 정리되어 있었지만, 이것은 거대한 데이터베이스 시스템이 아니었다. 그저 평범한 텍스트 파일일 뿐이었다. 그때의 기억이 떠올랐다. 이 db.json 파일을 진짜 데이터베이스처럼 보이게 하려고 json-server라는 도구를 썼었다. 터미널에 간단한 명령어를 입력하면, 이 파일이 마치 진짜 서버처럼 요청에 응답해주었다.
“생각났어. 4차 프로젝트에는 진짜 데이터베이스가 없었어. 그냥 이 db.json 파일 하나로 가짜 서버를 만들어서 화면에 데이터를 보여주고, 추가하고, 삭제하는 척만 했던 거야.”
솔라의 말에 루나가 조용히 고개를 끄덕였다.
“맞아. ‘척’을 한 거지. 4차 프로젝트의 목표는 거기까지였으니까. React로 만든 예쁜 화면, OpenAI를 이용한 그럴듯한 기능, 그리고 그 모든 게 잘 작동하는 것처럼 보이게 하는 데이터 흐름. 그 자체로 완결된 하나의 ‘프런트엔드 미니프로젝트’였던 거야.”
루나의 설명을 듣고 나니 모든 것이 명확해졌다. 솔라는 두 개의 프로젝트 폴더를 다시 바라보았다. 이제는 다르게 보였다. ‘4차-bookshelf-frontend’는 미완성 부품이 아니었다. 데이터 저장을 위해 임시 방편을 썼을 뿐, 화면과 사용자 경험에 관한 한 그 자체로 완결된 독립적인 프로젝트였다. 사용자의 눈에 보이는 모든 것—검색, 필터, 추천 UI—은 이미 그 안에 완성된 형태로 존재했다.
“그랬구나. 4차 프로젝트는 5차로 가기 위한 과정이 아니라, 그 자체로 ‘프런트엔드’라는 경계 안에서 완성된 결과물이었던 거네. 데이터는 진짜가 아니었지만, 보이는 부분은 전부 진짜였던 거지.”
솔라는 4차 프로젝트의 정체성을 비로소 이해했다. 그것은 5차 프로젝트의 초안이나 시험 버전이 아니었다. 명확한 역할과 범위를 가진, 잘 만들어진 하나의 독립체였다. 프로젝트의 범위를 기능의 목록만으로 판단해서는 안 된다는 사실을 깨달은 순간이었다.
그제야 진짜 질문이 떠올랐다.
“그런데… 프런트엔드가 이렇게 멀쩡히 완성되어 있었는데, 5차는 왜 굳이 여기에 ‘백엔드’라는 걸 붙여서 이 모든 걸 확장해야만 했을까? 이대로도 충분히 잘 돌아가는 것처럼 보였는데.”
솔라의 시선은 db.json이라는 장난감 데이터 상자를 넘어, ‘5차-bookshelf-system’이라는 더 크고 복잡해 보이는 상자로 향하고 있었다.
2장: 5차 BookShelf: 백엔드 확장이라는 목표
솔라의 손가락은 ‘5차-bookshelf-system’ 폴더 아이콘 위를 맴돌았다. ‘시스템’이라는 단어가 유독 무겁게 느껴졌다. 4차 프로젝트가 눈에 보이는 ‘화면’을 완성하는 것이었다면, 5차는 보이지 않는 무언가를 더한 것이 분명했다. 궁금증을 참지 못한 솔라는 폴더를 열었다.
가장 먼저 눈에 들어온 것은 파일 목록이 아니었다. 폴더 구조 그 자체였다. Frontend 폴더와 나란히, 이전에 없던 Backend라는 이름의 폴더가 떡하니 자리를 잡고 있었다. 4차 프로젝트가 하나의 단일체였다면, 5차는 처음부터 두 개의 독립된 영역으로 나뉘어 있었다. 솔라는 먼저 프로젝트의 얼굴이라 할 수 있는 README.md 파일을 열었다.
5차 BookShelf System은 이미 완성된 4차 BookShelf frontend를 기반으로, Supabase 기반 로그인, 즐겨찾기 기능을 연결하여 사용자별 도서 관리 경험을 확장한 프로젝트입니다.
“흠… 로그인, 즐겨찾기…”
솔라는 문장을 소리 내어 읽으며 고개를 갸웃했다.
“언니, 이것도 결국 프런트엔드 기능 추가 아니야? 4차에서 책 목록 보여줬으니까, 5차에서는 로그인 화면이랑 즐겨찾기 버튼을 화면에 더 만든 거잖아. 4차에서 이미 잘 만들어진 프런트엔드가 있었으니, 거기에 기능 몇 개 더 붙인 것처럼 보여.”
솔라의 말에는 4차와 5차의 경계가 여전히 모호하다는 아쉬움이 묻어났다. 그냥 더 복잡한 버전의 프런트엔드 프로젝트처럼 느껴졌다. 루나는 솔라의 화면을 흘깃 보더니, 파일 탐색기 창을 손가락으로 가리켰다.
“그럼 그 옆에 있던 Backend 폴더 안에는 뭐가 들어있을까?”
질문에 솔라는 마지못해 README 파일을 잠시 내려두고 Backend 폴더를 클릭했다. 그 안의 파일 목록은 4차 프로젝트의 것과는 완전히 이질적이었다. src/main/java 같은 낯선 경로, pom.xml이라는 파일, 그리고 .java 확장자로 끝나는 수많은 파일들이 보였다. React 컴포넌트나 CSS 파일은 어디에도 없었다. 이것은 화면을 그리는 코드가 아니었다. 명백히, 보이지 않는 곳에서 데이터를 처리하고 무언가를 계산하기 위한, 완전히 다른 종류의 프로그램이었다.
“이건… 자바랑 스프링 부트잖아. 프런트엔드랑은 전혀 다른 세상인데?”
솔라는 혼란스러운 표정으로 두 개의 폴더, Frontend와 Backend를 번갈아 쳐다봤다. 마치 잘 닦인 자동차의 외관(프런트엔드)을 구경하다가, 난생 처음으로 그 안의 복잡한 엔진(백엔드)을 마주한 기분이었다. 둘은 함께 움직이지만, 만들어진 방식과 목적은 완전히 달랐다.
“다시 README 파일로 돌아가 볼래? 이번엔 추가된 기능 목록 말고, 이 프로젝트의 ‘주요 목표’라고 적힌 부분을 읽어봐.”
루나의 말에 솔라는 다시 README.md 파일로 시선을 돌려 스크롤을 내렸다. 그리고 이전에는 가볍게 지나쳤던 문장을 발견했다.
주요 목표는 React frontend와 Spring Boot backend를 API로 연동하고, 예외 처리와 배포 환경까지 포함한 전체 흐름을 구현하는 것이다.
솔라의 눈이 ‘API로 연동’이라는 구절에 머물렀다. 아까는 ‘로그인’, ‘즐겨찾기’ 같은 사용자 눈에 보이는 기능에만 집중하느라 이 문장의 진짜 의미를 놓치고 있었다. 5차 프로젝트의 핵심은 새로운 기능을 ‘만드는 것’에 있지 않았다. 이미 존재하는 프런트엔드와 새로 만든 백엔드라는 두 개의 독립된 시스템을 ‘연결하는 것’에 있었다.
“아…! 알겠다. 5차 프로젝트의 진짜 목표는 로그인 버튼이나 즐겨찾기 화면을 추가하는 게 아니었어. 4차에서 만든 완벽한 프런트엔드와, 지금 만든 이 스프링 부트 엔진을 API라는 통로로 잇는 것, 그 자체가 목표였던 거구나.”
‘확장’의 의미가 비로소 명확해졌다. 그것은 화면의 크기를 키우는 확장이 아니었다. 보이지 않는 영역, 즉 데이터가 실제로 저장되고 사용자가 식별되며 시스템 전체가 안정적으로 돌아가는 ‘기반’을 구축하는 차원의 확장이었다. 프런트엔드라는 무대 위의 배우(4차)는 그대로 두고, 무대 뒤의 모든 기계 장치와 조명 시스템(5차 백엔드)을 새로 구축하는 작업과 같았다.
솔라는 이제 두 프로젝트를 보며 ‘반복’이라는 단어를 떠올리지 않았다. 4차는 그 자체로 완결된 프런트엔드 프로젝트였고, 5차는 그 완성품을 가져다가 진짜 살아 움직이는 시스템으로 만들기 위한 ‘백엔드 중심의 확장 프로젝트’였다. 프로젝트의 목표를 기능 목록이 아닌, 시스템 전체의 구조적 변화로 파악해야 한다는 것을 깨달은 순간이었다.
문득 새로운 질문이 떠올랐다. 이 거대한 ‘백엔드 확장’의 첫걸음은 무엇이었을까.
“그럼 4차에서 장난감 데이터 상자처럼 썼던 json-server는 어떻게 된 거야? 이 새로운 백엔드 시스템에서는 데이터를 어디에, 어떻게 저장하는 거지? 그게 가장 큰 변화일 것 같은데.”
3장: 데이터 소스의 변화: json-server에서 Spring Boot & JPA로
솔라의 질문은 허공에 흩어지지 않았다. 그 질문은 솔라를 5차 프로젝트의 Backend 폴더 가장 깊숙한 곳으로 이끌었다. 화면은 둘로 나뉘어 있었다. 왼쪽에는 4차 프로젝트의 db.json 파일이, 오른쪽에는 5차 프로젝트 백엔드의 Book.java라는 파일이 열려 있었다. 솔라의 눈은 두 화면을 집요하게 오갔다.
왼쪽의 db.json은 익숙하고 단순했다. "title": "클린 코드" 처럼, 이름표가 붙은 데이터가 가지런히 쌓여있는 창고 같았다. 반면 오른쪽의 Book.java 파일은 낯설었다. private String title; 이라는 선언 위로 @Entity, @Table(name = "books"), @Id 같은 알 수 없는 기호들이 잔뜩 붙어 있었다. 마치 잘 정리된 창고를 보다가, 갑자기 복잡한 설계 도면을 마주한 기분이었다.
“결국 똑같은 거 아니야?”
솔라가 혼잣말처럼 중얼거렸다.
“왼쪽에선 JSON 형식으로 title을 썼고, 오른쪽에선 자바 클래스 형식으로 title을 썼을 뿐이잖아. 데이터를 담는 그릇의 모양만 바뀐 거지, 내용물은 그대로인데. 이걸 ‘가장 큰 변화’라고 할 수 있나?”
솔라의 생각은 ‘기술 스택 교체’라는 표면적인 이해에 머물러 있었다. 더 멋지고 복잡해 보이는 도구로 바꿨을 뿐, 근본적인 역할은 같다고 여겼다. 옆에서 솔라의 화면을 조용히 지켜보던 루나가 오른쪽 화면의 한 부분을 가리켰다.
“그럼 이건 어때? BookRepository.java 라는 파일.”
솔라가 루나의 손끝을 따라 해당 파일을 열었다. 파일의 내용은 더욱 황당했다.
public interface BookRepository extends JpaRepository<Book, Long> {
// ...?
}
파일은 거의 텅 비어 있었다. 몇 줄의 선언 외에는 아무런 코드도 없었다.
“이게 다야? 이걸로 어떻게 데이터를 저장하고 불러온다는 거야? 4차에서는 그래도 json-server가 파일을 읽어서 진짜 서버인 척이라도 했는데, 이건… 아무 일도 안 하는 것처럼 보여.”
솔라의 목소리엔 당혹감이 가득했다. 루나는 대답 대신, 솔라가 아까 보았던 Book.java 파일의 첫 줄을 다시 가리켰다.
“아까 봤던 @Entity라는 주석. 그게 뭘 의미하는 것 같아?”
“글쎄… 그냥 이건 ‘엔티티’라는 종류의 파일이라고 표시하는 거 아닐까?”
“표시이긴 한데, 누구에게 보여주기 위한 표시일까? 개발자? 아니면… 이 프로그램 자체?”
루나의 마지막 질문에 솔라는 잠시 말을 멈췄다. ‘프로그램 자체에게 보여주는 표시.’ 그 말이 머릿속을 맴돌았다. 솔라는 @Entity라는 키워드를 검색하기 시작했다. 몇 분 후, 솔라의 눈이 동그래졌다.
“아… 이건 그냥 주석이 아니네. 스프링 부트에게 ‘이 Book 클래스를 보고 데이터베이스에 테이블을 만들어줘. 이건 단순한 자바 객체가 아니라, 영구적으로 저장해야 할 데이터의 설계도야’라고 말하는 ‘명령어’였어.”
그 순간, 모든 것이 다르게 보이기 시작했다. 왼쪽의 db.json은 그저 데이터가 담긴 텍스트 파일에 불과했다. 누군가 내용을 바꾸거나 형식을 망가뜨려도 아무도 막지 못한다. 그냥 수동으로 관리하는 목록일 뿐이었다.
하지만 오른쪽의 Book.java 파일과 BookRepository는 달랐다. @Entity라는 선언은 Book 데이터의 구조와 규칙을 시스템 스스로가 인지하고 책임지게 만드는 약속이었다. 그리고 텅 비어 보였던 BookRepository는 JPA라는 기술을 통해 save(), findById() 같은 수많은 데이터 관리 기능을 이미 품고 있는 마법 상자와 같았다. 개발자가 데이터베이스에 직접 명령을 내리지 않아도, 이 ‘저장소(Repository)’를 통해 안전하고 일관된 방식으로 데이터를 다룰 수 있게 되는 것이다.
솔라는 비로소 깨달았다. 이것은 단순히 데이터의 저장 위치를 옮긴 것이 아니었다. 데이터에 대한 ‘책임’의 주체가 바뀐 거대한 전환이었다.
“알겠다. 4차에서 데이터는 그냥 ‘텍스트 덩어리’였어. json-server는 그 텍스트를 읽어서 전달해주는 앵무새였고. 그런데 5차에서는 백엔드가 Book이라는 데이터가 무엇인지, 어떤 구조를 가져야 하는지 직접 ‘정의’하고 있어. 데이터를 단순 보관하는 창고지기에서, 데이터의 무결성과 생명주기 전체를 관리하는 은행 시스템으로 바뀐 거구나.”
데이터 소스 전환의 진짜 의미를 파악한 솔라는 고개를 끄덕였다. 더 이상 기술 스택의 이름만 보고 변화를 판단하지 않게 되었다. 그 기술이 프로젝트 안에서 어떤 ‘역할’과 ‘책임’을 가져가는지 분석해야 한다는 것을 배웠다.
그때, 새로운 의문이 꼬리를 물고 이어졌다. 이렇게 강력한 데이터 관리 시스템을 갖게 되었다면, 단순히 책 목록만 저장하지는 않을 터였다.
“잠깐, 그러면… 이 새로운 백엔드는 사용자 정보도 이런 식으로 관리할 수 있겠네. README에서 봤던 ‘로그인’이나 ‘즐겨찾기’ 같은 기능 말이야. 그건 단순한 책 데이터와는 전혀 다른 차원의 이야기인데, 이 시스템 안에서 사용자와 책은 어떻게 관계를 맺는 거지?”
4장: 사용자 기능의 도입: Supabase 인증과 즐겨찾기
솔라의 손끝에서 5차 BookShelf 애플리케이션이 실행되었다. 이전처럼 코드와 폴더 구조를 보는 대신, 완성된 로그인 화면이 그녀를 맞았다. 이전 장에서 데이터의 흐름과 책임에 대해 고민했던 흔적은 잠시 잊고, 순수한 사용자로서 시스템을 마주했다. 테스트 계정으로 로그인하자, ‘솔라의 책장’이라는 개인화된 공간이 나타났다.
“와, 진짜 그럴듯한데.”
솔라는 감탄하며 몇 분간 기능을 탐색했다. 책을 추가하고, AI 표지 생성을 요청하고, 마음에 드는 책 옆의 별 모양 아이콘을 눌러 즐겨찾기에도 추가했다. 모든 것이 매끄럽게 작동했다. 이 모든 경험의 중심에는 새로 구축된 스프링 부트 백엔드가 굳건히 서 있을 터였다. 솔라는 만족스러운 표정으로 의자 등받이에 몸을 기댔다.
“언니, 이제 확실히 알겠어. 5차 백엔드는 정말 대단한 일을 하는구나. 내가 로그인하면 Supabase라는 서비스랑 연동해서 나를 확인하고, 책 정보를 요청하면 데이터베이스에서 꺼내주고, 내가 ‘AI 표지 생성’ 버튼을 누르면 OpenAI에 대신 요청해서 그림을 받아오고, 즐겨찾기까지 전부 관리해주고… 말 그대로 이 모든 기능의 심장부 역할을 하는 거네.”
솔라는 자신이 내린 결론에 확신을 보였다. 백엔드는 모든 복잡한 일을 처리하는 강력한 중앙 처리 장치처럼 보였다. 옆에서 지켜보던 루나는 대답 대신 노트북 쪽으로 고개를 까딱했다.
“정말 백엔드가 그 모든 일을 혼자 다 처리한다고 어떻게 확신해? 한번 직접 확인해보는 건 어때? 네가 방금 누른 버튼들이 실제로 어떤 신호를 보내고 있는지 말이야.”
루나의 말에 솔라는 잠시 멈칫했다. ‘확인’이라니. 이미 결과물이 눈앞에 있는데 뭘 더 확인한단 말인가.
“개발자 도구를 열고 ‘네트워크’ 탭을 켜 둬. 그리고 방금 했던 행동을 다시 한번 반복해보는 거야. 로그인부터 AI 표지 생성, 그리고 즐겨찾기까지.”
솔라는 반신반의하며 브라우저의 개발자 도구를 열고 네트워크 탭을 활성화했다. 텅 빈 로그 창이 나타났다. 솔라는 먼저 로그아웃했다가 다시 로그인을 시도했다. 그러자 네트워크 탭에 몇 줄의 기록이 빠르게 스쳐 지나갔다. 가장 눈에 띄는 것은 supabase.co 라는 주소로 향하는 요청이었다.
“일단 로그인은 우리 백엔드가 아니라 Supabase라는 곳과 직접 통신하는구나.”
그다음, 솔라는 새 책을 등록하고 ‘AI 표지 생성’ 버튼을 눌렀다. 이번에는 훨씬 더 흥미로운 광경이 펼쳐졌다. 수많은 요청 기록 중에 유독 낯선 주소가 눈에 띄었다. api.openai.com.
“어? 이상하다.”
솔라의 미간이 좁아졌다. 그녀의 예상대로라면, 프런트엔드가 백엔드(localhost:8080)로 ‘표지 만들어줘’라는 요청을 보내고, 백엔드가 다시 OpenAI로 요청을 보내야 했다. 하지만 네트워크 기록은 다른 이야기를 하고 있었다. 프런트엔드 애플리케이션이 OpenAI API를 직접 호출하고 있었다. 잠시 후, OpenAI로부터 이미지 주소를 응답으로 받자, 그제야 프런트엔드는 그 주소를 포함해서 백엔드 API (/api/books)를 호출하여 책 정보를 저장했다.
“이럴 수가… 백엔드는 AI 표지 생성 과정에 전혀 관여하지 않았어. 그냥 프런트엔드가 OpenAI랑 알아서 이야기하고, 그 결과를 가져와서 ‘이것 좀 저장해주세요’ 하고 부탁하니까 받아주기만 한 거네.”
마지막으로 솔라는 책 옆의 별 모양 아이콘을 클릭했다. 네트워크 탭에는 백엔드 API(/api/favorites)로 요청이 가는 것이 기록되었다. 이 부분은 그녀의 예상과 같았다.
모든 실험을 마친 솔라는 한동안 멍하니 네트워크 로그 창을 바라보았다. 자신이 생각했던 강력하고 만능인 ‘심장부’의 모습은 온데간데없었다. 대신, 각자의 역할에만 충실한 여러 부품들이 조용히 협력하는 모습이 보였다.
“알겠다… 5차 백엔드는 모든 걸 다 해주는 해결사가 아니었어. 각자 자기 역할이 명확하게 나뉘어 있었던 거야.”
솔라의 목소리가 차분하게 가라앉았다.
“사용자 인증은 ‘Supabase’라는 경비 전문가에게 맡기고, AI 그림을 그리는 창의적인 일은 ‘프런트엔드’가 직접 ‘OpenAI’라는 예술가와 소통해서 처리해. 우리 백엔드는 그저 ‘이 사용자가 어떤 책을 소유하고, 어떤 책을 즐겨찾기 했는지’처럼 자기가 책임져야 할 데이터만 정확하게 기록하고 관리하는 ‘데이터 관리 전문가’였던 거야.”
‘만능 해결사’라는 잘못된 상이 깨지자, 비로소 시스템의 진짜 구조가 보이기 시작했다. 각 시스템 구성 요소가 무엇을 하고, 더 중요하게는 무엇을 ‘하지 않는지’를 구분하는 것. 솔라는 이것이 시스템의 책임과 역할을 명확히 나누는 핵심이라는 것을 깨달았다. 프로젝트의 진짜 확장성은 무작정 많은 기능을 떠안는 것이 아니라, 각자의 역할을 분명히 정의하고 분리하는 데서 나온다는 사실을 체감한 순간이었다.
이제 4차와 5차의 차이는 더욱 명확해졌다. 5차 프로젝트는 단순히 기능을 추가한 것이 아니라, 각 기능의 책임을 전문화된 시스템 구성원들에게 분배하여 전체 시스템을 더욱 견고하고 확장 가능하게 만든 것이었다.
“좋아, 이제 각자 누가 무슨 일을 하는지는 알겠어. 설계도는 파악했지.”
솔라는 노트북을 바라보며 말했다. 하지만 새로운 이해는 곧 새로운 질문을 낳았다.
“그런데… 이건 전부 내 노트북 안에서 완벽하게 조립된 장난감 도시 같잖아. 프런트엔드, 백엔드, Supabase, OpenAI… 이렇게 각자 다른 곳을 바라보는 녀석들을 실제로 인터넷이라는 세상에 내놓으려면 어떻게 해야 하는 거지? 한 군데라도 연결이 잘못되면 모든 게 멈춰버릴 것 같은데.”
5장: 완성도를 위한 경계: 배포와 예외 처리
솔라의 노트북 화면에서는 5차 BookShelf 애플리케이션이 완벽하게 구동되고 있었다. 로그인, 책 추가, AI 표지 생성, 즐겨찾기. 모든 기능이 매끄럽게 각자의 역할을 수행하며 아름다운 협주를 만들어냈다. 하지만 솔라의 마음은 편치 않았다. 그녀의 눈에는 이 완벽한 시스템이 자기 노트북이라는 온실 속에서만 존재하는, 정교하지만 위태로운 유리성에 갇힌 것처럼 보였다.
‘이걸 어떻게 세상에 내놓지?’
이전 대화에서 마지막으로 던졌던 질문이 머릿속을 맴돌았다. 프런트엔드, 백엔드, Supabase 인증, OpenAI API. 이 모든 것들은 지금 ‘localhost’라는, 그녀의 노트북 안에서만 통용되는 주소와 약속으로 아슬아슬하게 연결되어 있었다. 솔라는 5차 프로젝트 폴더 안에서 deployment라는 이름이 붙은 문서와 설정 파일들을 찾아 열었다. 화면 가득히 알 수 없는 명령어와 환경 변수 목록이 나타났다. 마치 잘 조립된 장난감의 분해도를 보는 듯 복잡했다.
“언니, 역시 모르겠어.”
결국 솔라는 소파에 앉아 책을 읽던 루나에게 말을 걸었다.
“코드는 다 짰고, 내 컴퓨터에서는 이렇게 잘 돌아가는데… 배포는 왜 이렇게 복잡한 거야? 그냥 파일들 서버에 업로드하고 실행하면 끝나는 거 아니었어? 이건 꼭 개발이 다 끝난 뒤에 치르는 귀찮은 의식 같아.”
솔라의 목소리에는 개발의 마지막 단계라 여겼던 배포가 예상치 못한 거대한 장벽처럼 느껴지는 데 대한 당혹감이 묻어 있었다. 루나는 읽던 책을 조용히 덮고, 솔라의 노트북 화면으로 시선을 옮겼다. 하지만 복잡한 배포 스크립트 대신, 이전 장에서 솔라가 확인했던 프런트엔드 소스코드의 한 부분을 가리켰다.
“우리가 만든 프런트엔드는 백엔드와 통신하기 위해 어디로 요청을 보내지?”
“응? 그야… localhost:8080/api/books 같은 주소로 보내지.”
솔라는 당연하다는 듯 대답했다. 하지만 대답을 내뱉는 순간, 스스로의 말에 담긴 어색함을 깨달았다. localhost. ‘이 컴퓨터’라는 뜻의 그 주소는 인터넷 세상의 다른 누구도 접근할 수 없는, 오직 그녀 자신에게만 유효한 주소였다.
“그럼 이 프로젝트를 인터넷에 배포한다고 상상해 봐. 프런트엔드는 Vercel 같은 호스팅 서비스에 올라가서 my-bookshelf.com이라는 주소를 갖게 되겠지. 백엔드는 Render 같은 다른 서비스에 올라가서 my-api.onrender.com이라는 주소를 가질 거고. 그럼 my-bookshelf.com을 방문한 사용자의 브라우저는 더 이상 localhost에 요청을 보낼 수 없잖아. 어디로 요청을 보내야 할까?”
루나의 설명에 솔라는 잠시 생각에 잠겼다가, 프로젝트 폴더에서 .env.example이라는 파일을 찾아 열었다.
# Backend API Server URL
VITE_API_BASE_URL=http://localhost:8080
# Supabase
REACT_APP_SUPABASE_URL=...
REACT_APP_SUPABASE_ANON_KEY=...
파일 안에는 프런트엔드가 알아야 할 외부 주소들이 가지런히 정리되어 있었다. 하지만 값은 비어 있거나 개발용 주소로 채워져 있었다. 백엔드 프로젝트에도 비슷한 설정 경계가 있었다. 데이터베이스 주소와 접속 정보처럼 환경마다 달라지는 값들이 들어갈 자리였다. 그 순간, 솔라는 모든 조각이 맞춰지는 것을 느꼈다.
“아…! 배포는 그냥 파일만 옮기는 게 아니었구나. 시스템이 살아갈 ‘환경’을 설정해주는 거였어. 내 노트북에서는 백엔드 주소가 localhost:8080이지만, 실제 서버에 올라가면 완전히 다른 주소가 될 테니, 프런트엔드에게 그 주소를 알려줘야 해. 이게 바로 ‘환경 변수’의 역할이구나.”
그뿐만이 아니었다. 보안상의 이유로 웹 브라우저는 기본적으로 스크립트가 자기 자신(e.g., my-bookshelf.com)이 아닌 다른 출처(e.g., my-api.onrender.com)로 네트워크 요청을 보내는 것을 막는다는 사실(CORS 정책)도 떠올랐다. 백엔드 서버가 명시적으로 “저 프런트엔드에서 오는 요청은 받아도 괜찮아”라고 허락해주지 않으면 모든 API 호출은 실패할 터였다. 만약 데이터베이스 연결이 갑자기 끊어지면 어떻게 될까? 백엔드는 우아하게 실패를 알리는 대신 그대로 멈춰버릴지도 모른다.
솔라는 5차 프로젝트의 README 파일에 무심코 지나쳤던 한 문장을 다시 떠올렸다.
주요 목표는 React frontend와 Spring Boot backend를 API로 연동하고, 예외 처리와 배포 환경까지 포함한 전체 흐름을 구현하는 것이다.
이제야 그 문장의 무게가 실감 났다. ‘배포 환경까지 포함한 전체 흐름 구현’. 이것은 부가적인 작업이 아니었다. 분리된 각 시스템을 엮어 하나의 유기적인 서비스로 완성시키는, 5차 프로젝트의 핵심 목표 그 자체였다. 내 노트북이라는 안전한 항구를 떠나 인터넷이라는 거친 바다로 나아갈 때, 예측 불가능한 환경과 온갖 예외 상황 속에서도 시스템이 견딜 수 있도록 만드는 것. 그것이야말로 진정한 ‘시스템 완성도’를 측정하는 잣대였다.
“알겠다. 4차 프로젝트는 내 방 안에서 완성된 하나의 모형 배였어. 하지만 5차 프로젝트는 그 모형 배를 진짜 바다에 띄우는 일이었던 거야. 항해 지도(환경 변수)를 준비하고, 해적의 공격(보안 정책)에 대비하고, 폭풍우(예외 상황)를 만났을 때 대처하는 방법까지 모두 설계에 포함해야만 하는… ‘확장’이라는 건 결국 이 모든 걸 감당할 수 있는 시스템을 만드는 거였구나.”
솔라는 더 이상 배포를 개발 이후의 귀찮은 절차로 여기지 않았다. 그것은 프로젝트의 경계를 정의하고 생명력을 불어넣는, 가장 중요한 확장 과정의 일부였다.
모든 여정을 마친 솔라는 노트북 화면에 나란히 떠 있는 ‘4차-bookshelf-frontend’와 ‘5차-bookshelf-system’ 두 개의 폴더를 차분히 바라보았다. 그리고는 깨끗한 메모장을 하나 열어, 마치 새로운 동료에게 프로젝트를 인수인계하듯 키보드를 두드리기 시작했다.
4차 vs 5차 BookShelf: 반복이 아닌 확장의 지도
- 4차: 프런트엔드 프로토타입. UI/UX에 집중. 데이터는
json-server라는 가상 서버로 흉내 냄. - 5차: 4차 프런트엔드를 실제 서비스로 확장하는 백엔드 중심 프로젝트.
- 데이터:
json-server를 실제Spring Boot + JPA백엔드로 교체. 데이터의 소유권과 책임을 시스템이 직접 관리. - 사용자:
Supabase인증을 연동해, 단순 방문객이 아닌 ‘나의’ 책장을 소유하는 사용자를 구현. - 역할 분담: 백엔드는 만능 해결사가 아님. 인증은 Supabase에, AI 호출은 프런트엔드에 위임하며 각자의 책임을 명확히 분리.
- 완성도: 개발 환경을 넘어,
배포 환경에서의 통신(환경 변수, CORS)과 안정성(예외 처리)까지 고려하여 실제로 동작하는 전체 시스템을 구축.
- 데이터:
메모장에 적힌 명확한 문장들을 보며 솔라는 비로소 고개를 끄덕였다. 이제 그녀는 두 프로젝트가 왜 비슷해 보였는지, 그리고 실제로는 어떻게 근본적으로 다른지를 누구에게든 명확하게 설명할 수 있었다. 그것은 단순한 지식의 습득이 아니었다. 혼란스러운 현상 속에서 핵심적인 차이를 구분하고, 그 관계를 구조적으로 파악해내는 자신만의 지도를 완성한 순간이었다.