Mini Project 5 11

배포와 트러블슈팅을 통해 5차 미니프로젝트의 완성도 판단하기

CRUD, AI 표지, 로그인, 즐겨찾기가 있으면 완성처럼 보인다. 하지만 미니프로젝트 산출물은 실행, 배포, 오류 대응, 설명 가능성까지 봐야 한다.

근거 · 교안 최종 산출물/GitHub 배포·문제 해결 정리

배포와 트러블슈팅을 통해 5차 미니프로젝트의 완성도 판단하기 대표 이미지

1장: README: 단순 기능 목록을 넘어선 프로젝트 설계도

솔라는 거실 테이블에 노트북을 탁 소리 나게 내려놓았다. 막 완성한 5차 미니프로젝트의 결과물들이 화면 가득 펼쳐져 있었다. 뿌듯함을 감추지 못한 목소리가 경쾌하게 울렸다.

“언니, 드디어 끝났어! 내 프로젝트 좀 봐. README랑 발표 PPT도 다 만들었어. CRUD, AI 표지 생성, 로그인, 즐겨찾기까지, 기능 목록 쫙 뽑아놓으니까 정말 그럴듯하지? 이 정도면 ‘완성’이라고 할 수 있겠지?”

솔라의 말처럼, README 파일에는 프로젝트의 주요 기능들이 스크린샷과 함께 보기 좋게 정리되어 있었다. 사용자가 책 정보를 등록하고, 인공지능으로 멋진 표지를 만들고, 자신만의 즐겨찾기 목록을 관리하는 흐름이 한눈에 들어왔다. 모든 기능이 매끄럽게 작동하는 것을 확인했기에 솔라의 자신감은 하늘을 찔렀다.

루나는 소파에 앉아 책을 읽다 말고 솔라의 노트북 화면으로 시선을 옮겼다. 솔라가 정리해놓은 기능 목록을 잠시 훑어보던 루나는, 칭찬 대신 자신의 노트북을 열어 다른 프로젝트의 README 파일을 화면에 띄웠다. 작년에 우수 프로젝트로 꼽혔던 한 팀의 결과물이었다.

“이 문장 한번 볼래?”

루나가 가리킨 곳에는 복잡한 기술 용어들로 빽빽한 한 문장이 적혀 있었다.

본 프로젝트는 기능 구현뿐 아니라 Vercel을 이용한 프론트엔드 배포, Render를 이용한 백엔드 배포, 그리고 Supabase의 PostgreSQL/Auth를 활용한 데이터베이스 및 사용자 인증을 구축하였습니다. 또한, 환경변수 관리, CORS 정책 설정, Dockerfile을 통한 실행 환경 구성 및 DB 연결 과정에서 발생한 troubleshooting 경험을 함께 정리하였습니다.

순간 솔라의 말문이 막혔다. Vercel, Render, Supabase, CORS, Dockerfile… 모르는 단어는 아니었다. 이번 프로젝트를 하며 솔라 역시 어렴풋이 거쳐온 과정들이었다. 하지만 솔라는 그것들을 프로젝트의 ‘완성도’를 보여주는 핵심 요소라고는 생각하지 못했다. 그저 기능 구현을 위해 거쳐야 하는 귀찮고 자잘한 절차쯤으로 여겼을 뿐이다. 자신의 README가 잘 차려입은 모델의 앞모습 사진이라면, 루나가 보여준 README는 그 모델의 해부도처럼 느껴졌다. 온갖 낯선 이름들이 뒤엉켜 있어 어디부터 이해해야 할지 막막했다.

“이건… 기능 설명이 아니잖아. 그냥 자기가 쓴 기술들을 전부 나열한 거 아니야?” 솔라가 볼멘소리로 물었다.

“그렇게 보일 수도 있겠네.” 루나는 담담하게 대답했다. “그럼 우리, 이 README에서 ‘기능’이랑 관계없어 보이는 단어들만 한번 찾아볼까?”

루나는 솔라의 노트북 옆에 깨끗한 메모장을 하나 펼쳤다. 솔라는 반신반의하며 루나가 띄워놓은 모범 README를 다시 들여다보았다. 그리고 천천히, 기능과 직접 관련 없어 보이는 단어들을 찾아 소리 내어 읽기 시작했다.

“음… ‘Vercel 프론트엔드 배포’… ‘Render 백엔드 배포’… 이건 기능이 아니지. 그리고 ‘Supabase PostgreSQL/Auth’… 이것도 우리가 사용한 도구 이름이고. ‘환경변수 관리’, ‘CORS 정책 설정’… 아, ‘Dockerfile’도 있네. 마지막으로 ‘DB 연결 troubleshooting’까지.”

하나씩 읊을 때마다 단어들은 메모장에 차곡차곡 쌓였다. 목록이 완성되자 솔라는 잠시 말을 잃었다. 메모장을 채운 단어들은 개별 기능처럼 사용자 눈에 직접 보이지는 않는다. 하지만 이 단어들이 없었다면 프로젝트는 솔라의 노트북 안에서만 존재하는 죽은 코드 뭉치에 불과했을 것이다.

솔라는 비로소 깨달았다. 자신의 README는 프로젝트가 ‘무엇을 하는지’만 보여줬다. 하지만 좋은 README는 프로젝트가 ‘무엇으로 만들어졌고’, ‘어떤 환경에서 살아 움직이며’, ‘어떤 문제들을 이겨냈는지’까지 증명하고 있었다. 단순한 기능 명세서가 아니라, 프로젝트의 정체성을 담은 설계도이자 이력서였던 것이다.

“내 프로젝트가 어떤 행동을 하는지만 적어놨는데… 이 README는 프로젝트의 신분증 같아.” 솔라가 혼잣말처럼 중얼거렸다. 자신의 프로젝트를 다시 보니, 화려한 기능들 뒤에 숨어있던 복잡한 배포 구조와 수많은 결정의 순간들이 그제야 눈에 들어왔다.

생각이 거기에 미치자, 메모장에 적힌 단어들 중 유독 눈에 띄는 것들이 있었다.

“그런데 언니, 좀 이상해. 프론트엔드는 Vercel, 백엔드는 Render로 배포했다고? 왜 굳이 다른 서비스를 쓴 거지? Dockerfile은 또 뭐고? 배포 과정이 왜 이렇게 복잡한 거야? 그냥 한 군데다 올리면 되는 거 아닌가? 이런 복잡한 결정들이 정말 ‘완성도’랑 그렇게 큰 관련이 있을까?”

솔라의 손가락은 ‘Vercel’과 ‘Render’, 그리고 ‘Dockerfile’이라는 단어 위를 머뭇거리며 맴돌고 있었다. 기능 구현 너머의 세상, 그 첫 갈피에서 새로운 질문이 모습을 드러내고 있었다.

2장: 배포의 의사결정: Render의 오인과 Dockerfile의 역할

솔라는 전날 메모장에 적었던 단어들을 다시 바라보았다. ‘Vercel’, ‘Render’, ‘Dockerfile’. 마치 외국어 단어 시험지처럼 낯설게 느껴졌다. 이 복잡한 구조가 ‘완성도’와 관련 있다는 언니의 암시를 쉽사리 받아들이기 어려웠다. 왜 이렇게 해야만 했을까?

솔라는 빈 종이를 꺼내 간단한 아키텍처 다이어그램을 그리기 시작했다. 왼쪽에는 ‘프론트엔드 (Vercel)’, 오른쪽에는 ‘백엔드 (Render)’. 그리고 그 사이에 화살표를 그었다. 단순하고 명쾌했다. 하지만 ‘Dockerfile’이라는 상자는 어디에 두어야 할지 알 수 없었다. 백엔드 상자 옆에 둬야 하나? 아니면 완전히 다른 차원의 존재인가? 결국 솔라는 ‘Dockerfile’ 상자를 다른 한쪽에 덩그러니 그려놓고는 펜을 내려놓았다. 그녀의 그림 속에서, 배포 구조는 여전히 불필요하게 분리되고 복잡해 보일 뿐이었다.

루나는 솔라가 그려놓은 다이어그램과, 그 옆에 고립된 ‘Dockerfile’ 상자를 말없이 들여다보았다. 그리고는 다시 모범 프로젝트의 README 파일이 띄워진 자신의 노트북 화면을 솔라 쪽으로 돌렸다.

“솔라, 그 README에서 배포 환경을 설명한 부분을 자세히 한번 읽어볼래? 그냥 기술 이름만 훑지 말고, 그들이 왜 그렇게 했는지에 대한 ‘이야기’를 찾는다고 생각하면서.”

솔라는 루나의 말에 따라 화면을 들여다보았다. 처음에는 그저 기술 용어의 나열처럼 보였던 문장들 사이에서, 유독 눈에 띄는 대목을 발견했다. 프로젝트의 백엔드 배포 과정을 설명하는 부분이었다.

백엔드 서버는 Render 서비스를 통해 배포되었습니다. 하지만 저희 프로젝트는 프론트엔드(React)와 백엔드(Spring Boot)가 하나의 저장소에 공존하는 모노레포(monorepo) 구조를 가지고 있습니다. 이 때문에 Render 플랫폼이 저희 백엔드 프로젝트를 Node.js 프로젝트로 잘못 인식하는 문제가 발생했습니다.

“어? Render가 프로젝트를 잘못 인식했다고?” 솔라가 의아하다는 듯 중얼거렸다. 그녀의 생각에 이것은 명백한 ‘문제’이자 ‘실패’였다. 플랫폼 선택이 잘못되었거나, 프로젝트 구조에 결함이 있었다는 신호처럼 들렸다. 그냥 다른 플랫폼을 쓰면 해결될 일이 아니었을까?

“계속 읽어봐.” 루나가 조용히 재촉했다.

솔라는 다음 문장으로 시선을 옮겼다.

이 문제를 해결하기 위해, 저희는 Dockerfile을 작성하여 명시적으로 Java 17 실행 환경을 구축하고, 이를 기반으로 서버를 실행하도록 Render 설정을 변경하였습니다. 이를 통해 플랫폼의 자동 감지 기능에 의존하지 않고, 원하는 기술 스택으로 안정적인 배포 환경을 구성할 수 있었습니다.

순간, 솔라의 머릿속을 스치는 것이 있었다. 종이 위에 덩그러니 놓여있던 ‘Dockerfile’ 상자가 제자리를 찾아 움직이기 시작했다. 그것은 별개의 기술이 아니었다. Render라는 플랫폼의 ‘오인’이라는 문제 상황을 해결하기 위한 결정적인 ‘해결책’이었다.

이 팀은 문제가 발생했을 때 플랫폼을 탓하거나 포기하지 않았다. 대신 Docker라는 도구를 가져와 ‘이 프로젝트는 자바 17로動動해야 해!’라고 플랫폼에게 강제하는 방법을 택했다. 그저 ‘Render를 썼다’와 ‘Dockerfile을 썼다’는 사실의 나열이 아니었다. ‘Render가 오해해서 Dockerfile로 해결했다’는 하나의 완결된 의사결정의 기록이었다.

“아…” 솔라가 나지막한 탄성을 내뱉었다. “그러니까… 배포가 복잡했던 건 그냥 복잡한 게 아니라, 문제를 해결한 흔적이구나. ‘우리는 이런 문제도 해결할 수 있는 팀이다’라는 증거 같은 거네.”

솔라는 자신이 처음 적었던 메모장을 다시 보았다. ‘Vercel’, ‘Render’, ‘Dockerfile’. 이제 이 단어들은 더 이상 단순한 기술 이름으로 보이지 않았다. Vercel은 프론트엔드 배포에 최적화된 선택이었을 것이고, Render는 백엔드 배포를 위한 전략적 결정이었을 것이다. 그리고 Dockerfile은 그 전략을 실행하는 과정에서 마주친 예기치 못한 문제를 돌파하기 위한 무기였던 셈이다. 이 모든 선택과 해결의 과정이 모여 프로젝트의 ‘완성도’를 증명하고 있었다. 기술 목록은 곧 그들의 전투 기록이었다.

자신의 프로젝트를 떠올렸다. 기능 구현에만 급급했지, 왜 이 기술을 선택했고 배포 과정에서 어떤 어려움이 있었는지 제대로 기록해두지 않았다. 발표 PPT에는 그저 ‘Vercel과 Render 사용’이라고 한 줄 적어놓은 게 전부였다. 누군가 “왜 Vercel을 썼나요?”라고 물으면 “다들 쓰니까요” 외에는 할 말이 없었다. 완성은 기능의 개수가 아니라, 모든 결정에 대한 ‘설명 가능성’에서 온다는 것을 어렴풋이 깨달았다.

자신감이 붙은 솔라는 README의 다른 부분도 이런 관점으로 읽어보기 시작했다. 그러다 스크롤을 멈춘 곳은 ‘Troubleshooting’ 섹션이었다. 그곳에는 개발 과정에서 겪었던 온갖 오류들이 빼곡히 적혀 있었다.

Failed to fetch, CORS 오류, Supabase 로그인 후 즐겨찾기 실패

배포의 의사결정은 이제 이해가 됐다. 하지만 이건 또 다른 문제였다. 솔라는 고개를 갸웃거렸다.

“언니, 이건 알겠어. 그런데 이건 그냥 실패 기록이잖아. ‘로그인하다가 실패했어요’ 같은 흔한 오류를 이렇게 전시하는 게 왜 완성도를 높여주는 거지? 오히려 프로젝트가 불안정해 보이는 거 아닐까?”

3장: 트러블슈팅: 오류 기록이 곧 ‘프로젝트의 생명력 증명’

배포 과정의 의사결정 기록이 왜 중요한지 깨닫고 난 후, 솔라는 의욕적으로 자신의 README 파일을 수정하기 시작했다. 하지만 손가락은 키보드 위에서 금세 길을 잃었다. ‘Troubleshooting’이라는 빈 제목 아래, 커서만 외롭게 깜박였다.

솔라의 프로젝트 노트 한구석에는 개발 과정에서 마주쳤던 오류들이 날것 그대로 적혀 있었다. CORS 에러, DB 연결 시간 초과, 로컬에선 되는데 배포하면 왜인지 모르게 멈춤. 그것들은 며칠 밤낮으로 솔라를 괴롭혔던 상처이자, 어떻게든 해결하고 황급히 잊어버리고 싶었던 기억이었다. 이런 실패담을 README나 발표 PPT에 공개적으로 기록하는 것은 마치 자신의 약점을 전시하는 것처럼 느껴졌다. 다른 사람의 프로젝트에 적힌 오류 목록을 볼 때는 ‘불안정해 보인다’고 생각했던 바로 자신이었다.

결국 솔라는 ‘Troubleshooting’ 항목을 지울까 고민하며 노트북 화면만 멍하니 바라보았다. 그 모습을 지켜보던 루나는, 솔라의 고민을 읽었다는 듯이 자신의 노트북에서 어제 보았던 모범 프로젝트 README의 ‘Troubleshooting’ 부분을 다시 화면에 띄워줬다.

“솔라, 여기 ‘CORS 오류’라고 적힌 항목이 있지? 이 팀이 만약 이 오류에 대해 두 가지 방식으로 기록을 남겼다고 상상해보자.”

루나는 화면 옆에 텍스트 편집기를 열고 두 개의 짧은 시나리오를 타이핑하기 시작했다.

시나리오 A

CORS 오류

  • 현상: 프론트엔드에서 백엔드로 API 요청 시 CORS 오류 발생.
  • 해결: 백엔드 설정에서 모든 외부 요청(*)을 허용하여 해결함.

시나리오 B

CORS 오류 (Preflight Request)

  • 현상: 프론트엔드에서 특정 API(즐겨찾기 추가) 요청 시, 브라우저가 먼저 보내는 OPTIONS 요청(Preflight)에서 실패.
  • 원인 분석: 서버의 CORS 설정에 OPTIONS 메서드가 허용되어 있지 않았고, 허용 출처(Allowed Origin)가 명시적이지 않아 발생한 문제로 파악.
  • 해결: Spring Security 설정에서 allowedMethodsOPTIONS를 추가하고, allowedOrigins에는 실제 프론트엔드가 배포된 Vercel 주소만 명시적으로 지정하여 보안을 유지하며 문제를 해결함.

“자, 네가 이 팀을 채용하는 면접관이라면, 어떤 기록을 본 팀에게 더 마음이 갈 것 같아?”

솔라는 말없이 두 시나리오를 번갈아 보았다. 시나리오 A는 솔라가 자신의 노트에 적어놓은 메모와 크게 다르지 않았다. ‘어떻게든 되게 했다’는 결과만 있을 뿐, 과정은 생략되어 있었다.

하지만 시나리오 B는 달랐다. 같은 ‘CORS 오류’라는 단어에서 시작했지만, 이야기는 전혀 다른 방향으로 흘러갔다. 이 팀은 오류가 왜 발생했는지(Preflight RequestOPTIONS 메서드) 정확히 진단했다. 그리고 단순히 모든 것을 허용하는 쉬운 길을 택하지 않고, 보안까지 고려(Vercel 주소만 명시적으로 지정)하여 문제를 해결했다.

이 기록은 단순한 오류 보고가 아니었다. ‘우리는 문제를 이렇게 깊이 있게 분석하고, 더 나은 방식으로 해결할 줄 아는 팀입니다’라는 강력한 자기소개서였다. 오류는 더 이상 부끄러운 흉터가 아니었다. 문제 해결 능력을 증명하는 빛나는 훈장이었다.

“아…” 솔라의 입에서 나지막한 탄성이 터져 나왔다. “오류가 있었다는 사실 자체가 중요한 게 아니구나. 그 오류를 어떻게 다루었는지를 보여주는 게 핵심이네.”

이제 솔라는 모범 프로젝트 README에 나열된 오류 목록을 새로운 눈으로 볼 수 있었다.

Failed to fetch/ERR_CONNECTION_REFUSED, Supabase 로그인 후 즐겨찾기 실패, Render 배포 시 JAVA_HOME is not set

이것들은 더 이상 프로젝트의 결함 목록이 아니었다. 프론트와 백엔드의 통신, 외부 서비스와의 연동, 배포 환경 설정 등 프로젝트의 생명력을 유지하기 위해 반드시 거쳐야 했던 격전지들의 기록이었다. 이 기록을 남겼다는 것 자체가 프로젝트가 살아 움직이며 성장했다는 가장 확실한 증거였다.

솔라는 자신의 프로젝트 노트를 다시 펼쳤다. ‘왜인지 모르게 멈춤’이라고 적었던 메모를 지우고, 당시의 상황을 떠올리며 천천히 다시 적어 내려가기 시작했다. ‘DB 연결 시간 초과’는 왜 발생했었더라? 원인을 파고들자, 데이터베이스 커넥션 풀에 대한 고민이 수면 위로 떠올랐다.

생각이 거기에 미치자, README의 또 다른 낯선 부분이 눈에 들어왔다. Supabase DB 설정 프로파일에 적혀있던, 의미를 알 수 없던 숫자들의 나열이었다.

Hikari maximum-pool-size: 3, minimum-idle: 0, idle-timeout: 30000ms…

“언니, 그런데 이건 또 뭐야? 이런 세세한 설정값까지 적어두는 게 완성도랑 무슨 상관이 있는 거지? 이건 너무 지엽적인 거 아니야?”

4장: 숨겨진 디테일: Hikari 설정에서 읽는 운영의 깊이

솔라는 자신의 프로젝트 노트를 뚫어지라 쳐다보고 있었다. 전날 밤, ‘왜인지 모르게 멈춤’이라고 적었던 메모 옆에 새로 추가한 ‘DB 연결 시간 초과 가능성’이라는 글자가 선명했다. 자신의 트러블슈팅 기록을 더 깊이 파고들수록, 문제의 뿌리가 어렴풋이 보이기 시작했다. 하지만 그 뿌리를 어떻게 파헤쳐야 할지 막막했다.

그녀는 자신의 백엔드 프로젝트 설정 파일(application.yml)을 열었다. 데이터베이스 연결 정보 아래, 몇 줄의 기본 설정 외에는 텅 비어 있었다. 그 빈 공간과 어제 보았던 모범 프로젝트 README의 한 구절이 머릿속에서 강렬한 대비를 이루었다.

Supabase profile: hikari: maximum-pool-size: 3, minimum-idle: 0, idle-timeout: 30000, max-lifetime: 600000

솔라의 프로젝트가 텅 빈 공터라면, 저 프로젝트는 모든 구획이 정밀하게 설계된 도시 같았다. 이 숫자들은 대체 뭘까? ‘3’, ‘0’, ‘30000’… 마치 암호처럼 느껴졌다. 이런 미세한 숫자 조정이 프로젝트의 완성도와 대체 무슨 상관이란 말인가.

“언니, 이거 좀 봐.” 솔라는 자신의 텅 빈 설정 파일과 모범 README의 해당 부분을 나란히 띄워놓고 루나를 불렀다. “내 프로젝트 노트에 ‘DB 연결 시간 초과’ 문제를 겪었다고 썼잖아. 어쩌면 이게 원인이었을 수도 있겠다는 생각이 들어. 그런데 이 숫자들, 너무 지엽적인 거 아니야? 발표 PPT에 넣을 내용도 아닌데, 고작 연결 몇 개 더 만들고 몇 초 더 기다리게 하는 게 그렇게 중요해?”

솔라의 목소리에는 이 숫자들을 이해하고 싶다는 마음과, 동시에 이런 사소한 것에까지 신경 써야 하냐는 볼멘소리가 섞여 있었다.

루나는 말없이 솔라의 화면을 들여다본 뒤, 깨끗한 종이 한 장을 가져와 가운데에 네모 하나를 그렸다. “이게 우리 동네에 새로 생긴 작은 카페라고 해보자. 그리고 손님은 우리 앱에 들어오는 사용자의 요청이야.”

그러고는 네모 안에 작은 동그라미 세 개를 그렸다. “이 동그라미는 커피를 만드는 바리스타야. maximum-pool-size: 3은 이 카페에 최대로 고용할 수 있는 바리스타가 3명이라는 뜻이지. 자, 네가 이 카페 사장이라면, 바리스타 수를 어떻게 조절하고 싶어?”

“음… 많으면 많을수록 좋지 않을까? 손님이 몰려도 빨리빨리 커피를 내줄 수 있으니까.” 솔라가 즉답했다. 당연한 생각이었다.

“좋은 생각이네. 그럼 바리스타를 100명으로 늘려보자. maximum-pool-size: 100. 어떨까?” 루나는 웃으며 말했다.

“100명? 이 작은 카페에?” 솔라는 상상만으로도 어이가 없다는 듯 웃었다. “바리스타들이 서 있을 자리도 없겠네. 그리고 손님이 없을 땐 다들 놀고 있을 거 아냐. 월급은 월급대로 나가고. 이건 완전 낭비지.”

“바로 그거야. 바리스타 한 명 한 명은 카페의 소중한 자원(리소스)이니까. 데이터베이스 연결도 마찬가지야. 연결 하나를 만들고 유지하는 데는 서버의 메모리와 CPU 자원이 소모돼. 우리가 사용하는 Supabase 무료 버전처럼 자원이 한정된 환경에서 불필요하게 많은 연결을 미리 만들어두는 건, 작은 동네 카페에 바리스타 100명을 고용하는 것과 같아.”

순간 솔라의 머릿속이 환해졌다. maximum-pool-size는 단순히 숫자가 아니었다. ‘우리가 가진 자원이 어느 정도인지 알고 있다’는 선언이었다.

“그럼 반대로 바리스타를 1명만 두면 어때? maximum-pool-size: 1.” 루나가 다시 물었다.

“손님 두 명만 동시에 와도 한 명은 무조건 기다려야 하네. 세 번째, 네 번째 손님은 하염없이 줄만 서 있을 거고… 그러다 지쳐서 그냥 가버릴 수도 있겠다.”

“그게 바로 네가 겪었던 ‘DB 연결 시간 초과’ 문제와 비슷해. 요청은 계속 들어오는데, 서버가 데이터베이스와 연결할 통로(바리스타)를 제때 내주지 못하는 거지.”

이제야 모든 조각이 맞춰졌다. maximum-pool-size: 3. 이 숫자는 ‘우리는 무료 서비스의 제한된 자원 안에서 운영된다는 사실을 인지하고 있으며, 3명 정도의 바리스타(연결)가 우리 서비스의 트래픽을 감당하기에 가장 효율적인 균형점이라고 판단했다’는, 아주 논리적이고 전략적인 의사결정의 결과물이었다. minimum-idle: 0(손님이 없으면 바리스타를 놀게 하지 않는다)이나 idle-timeout: 30000(30초간 일이 없으면 퇴근시킨다) 같은 다른 설정들도 모두 같은 맥락에서 이해할 수 있었다.

솔라는 다시 모범 프로젝트의 README를 보았다. 이제 그 숫자들은 암호가 아니라, 프로젝트의 안정성과 효율성을 위해 얼마나 깊이 고민했는지를 보여주는 훈장처럼 보였다. 기능이 ‘동작한다’는 것을 넘어, 이 프로젝트가 외부 환경의 제약을 이해하고 그 안에서 ‘살아남을 수 있다’는 생존력을 증명하는 디테일이었다.

“결국 이것도 ‘설명 가능성’의 문제구나. 나는 그냥 기본값을 썼는데, 그건 ‘왜 이 값을 써야 하는지 몰랐다’는 고백이나 마찬가지였네.”

README, 배포 아키텍처, 트러블슈팅 기록, 그리고 운영 파라미터 설정까지. 프로젝트의 완성도를 판단하는 여러 겹의 증거들을 하나씩 해독해낸 솔라는 뿌듯함을 느꼈다. 하지만 동시에 새로운 고민이 고개를 들었다. 각 증거들이 무엇을 의미하는지는 알겠다. 하지만 이것들을 어떻게 하나로 엮어 ‘그래서 이 프로젝트는 완성도가 높다’ 혹은 ‘아직 부족하다’고 종합적으로 판단할 수 있을까? 면접관 앞에서 이 모든 조각들을 엮어 하나의 그림으로 보여줘야 한다면, 어디서부터 이야기를 시작해야 할까?

5장: 종합적 완성도: 설명 가능성으로 프로젝트를 ‘완전’하게

솔라는 거실 테이블 위에 여러 장의 메모를 펼쳐 놓았다. 지난 며칠간 루나와 함께 분석했던 모범 프로젝트의 흔적들이었다.

첫 번째 메모에는 ‘README: 프로젝트의 신분증’이라고 적혀 있었다. 두 번째 메모는 ‘배포 아키텍처: 문제 해결의 흔적 (Render/Dockerfile)’이라는 제목으로 복잡한 화살표들이 그려져 있었다. 세 번째는 ‘트러블슈팅 기록: 생명력의 증거 (CORS/Preflight)’, 네 번째는 ‘운영 파라미터: 생존력의 디테일 (Hikari Pool)’이라는 글자와 함께 카페 비유가 그려져 있었다.

모든 조각을 이해했다. 기능 목록 너머에 있는 완성도의 기준들을 하나씩 해독해낸 것이다. 하지만 솔라는 여전히 개운하지 않은 표정이었다. 조각들은 흩어져 있었고, 하나의 그림으로 맞춰지지 않았다.

“알겠어. README도 잘 써야 하고, 배포 결정에도 이유가 있어야 하고, 오류 기록도 남겨야 하고, DB 연결 설정 같은 디테일도 챙겨야 한다는 거잖아.” 솔라가 혼잣말처럼 중얼거렸다. “그런데 이걸 다 합쳐서 어떻게 ‘이 프로젝트는 완성도가 높다’고 말할 수 있지? 그냥 이 항목들을 다 했는지 체크리스트를 만드는 건가? 마치 점수 매기듯이?”

솔라의 생각은 프로젝트의 완성도를 평가하는 것이 마치 채점표에 동그라미를 치는 행위처럼 느껴졌다. 각 항목의 중요성은 알겠지만, 그것들을 합쳤을 때 어떤 의미가 되는지, 그 종합적인 ‘판단’의 기준이 무엇인지 모호했다. 이 모든 증거를 엮어낼 강력한 접착제가 필요했다.

루나는 솔라의 고민을 잠시 지켜보다가, 의외의 제안을 던졌다.

“솔라, 지금 네가 그 모범 프로젝트 팀의 일원이라고 상상해 봐. 그리고 나는 너를 채용하고 싶은 면접관이야.” 루나는 자세를 바로잡고 목소리를 가다듬었다. “자, 솔라 님. 이력서에 적어주신 미니프로젝트에 대해 설명해주시겠어요? 저희는 솔라 님이 이 프로젝트가 ‘잘 만들어졌다’고 생각하는 이유가 궁금합니다. 3분 드릴게요.”

갑작스러운 역할극에 솔라는 당황했지만, 이내 마음을 다잡았다. 이것이 자신이 마주한 문제의 핵심을 관통하는 질문임을 직감했다. 그녀는 펼쳐놓은 메모들을 빠르게 훑어보며 머릿속으로 할 말을 정리하기 시작했다.

“네, 면접관님. 제가 진행한… 아니, 우리 팀이 진행한 프로젝트에 대해 설명드리겠습니다.”

솔라는 심호흡을 한번 하고 말을 이었다.

“저희 프로젝트는 기능적으로… CRUD, AI 표지 생성, 로그인과 즐겨찾기가 모두 구현되어 있습니다. 그리고 저희는 배포도 성공적으로 마쳤습니다. 프론트엔드는 Vercel, 백엔드는 Render를 사용했습니다. Render가 저희 프로젝트를 잘못 인식하는 문제가 있어서 Dockerfile을 사용해 해결했습니다. 또, 개발 과정에서 CORS 오류나 DB 연결 실패 같은 문제들을 겪었고, 그것을 트러블슈팅 문서에 기록해두었습니다. 마지막으로, Supabase 무료 티어의 한계를 고려해서 Hikari 커넥션 풀 사이즈를 3으로 설정하는 등…”

말을 하면 할수록 솔라의 목소리는 점점 작아졌다. 자신이 그저 펼쳐놓은 메모의 제목들을 순서대로 읽고 있다는 사실을 깨달았기 때문이다. 이것은 설명이 아니라 나열이었다. 각각의 사실은 흥미롭지만, 그래서 이 프로젝트가 왜 ‘잘 만들어졌는지’에 대한 설득력 있는 답변은 되지 못했다. 면접관의 얼굴(루나)은 점점 미묘한 표정이 되어가고 있었다.

“……그래서… 완성도가 높다고 생각합니다.” 솔라는 힘없이 말을 맺었다. 정적이 흘렀다.

루나는 잠시 동안 면접관의 표정을 유지하다가, 이내 부드러운 미소를 지으며 원래의 모습으로 돌아왔다.

“방금 네가 한 건, 박물관 가이드가 ‘이쪽엔 공룡 뼈가 있고, 저쪽엔 토기가 있습니다’라고 말한 것과 같아. 사실이지. 하지만 관람객이 정말 듣고 싶은 건 ‘이 공룡 뼈와 토기가 발견된 지층을 분석해보니, 당시 이곳에 어떤 문명이 어떻게 살았는지 알 수 있습니다’라는 ‘이야기’ 아닐까?”

이야기. 그 단어가 솔라의 머리를 강하게 내리쳤다. README, 배포, 트러블슈팅, 설정값… 이것들은 흩어진 증거가 아니라, 하나의 이야기를 구성하는 플롯 포인트였던 것이다. ‘우리가 어떤 문제를 만나, 어떻게 고민하고, 어떤 결정을 내려서 결국 프로젝트를 완성시켰는가’에 대한 영웅 서사시.

솔라는 눈을 감고 다시 한번 3분 스피치를 구상했다. 이번에는 흩어진 증거들을 ‘설명 가능성’이라는 단단한 끈으로 엮기 시작했다.

다시 눈을 떴을 때, 그녀의 눈빛은 확신으로 차 있었다.

“다시 해볼게, 언니.”

솔라는 이번엔 루나를 보지 않고, 가상의 면접관이 앉아있을 허공을 응시했다.

“네, 면접관님. 저희 프로젝트는 단순히 기능이 ‘실행’되는 것을 넘어, 왜 그렇게 만들었는지 모든 과정을 ‘설명’할 수 있다는 점에서 완성도가 높다고 생각합니다.”

“첫째, 저희는 모든 기술적 결정에 대한 ‘왜?’에 답할 수 있습니다. README의 아키텍처 다이어그램에서 볼 수 있듯, 프론트엔드 배포에 Vercel을, 백엔드에 Render를 선택한 것은 각 플랫폼의 장점을 최대한 활용하기 위한 전략적 판단이었습니다. 특히 Render가 모노레포 구조를 오인했을 때, 저희는 단순히 플랫폼을 바꾸는 대신 Dockerfile로 실행 환경을 통제하는 길을 택했습니다. 이는 저희 팀이 마주한 문제를 회피하지 않고, 근본적으로 이해하고 해결하는 역량을 갖추었음을 보여주는 증거입니다.”

“둘째, 저희는 실패를 감추지 않고 ‘자산’으로 만들었습니다. 트러블슈팅 기록에 남겨진 CORS 오류 해결 과정은, 저희가 단순히 ‘모두 허용’으로 문제를 덮는 것이 아니라 브라우저의 Preflight 요청 메커니즘을 이해하고 보안을 고려하여 해결했음을 증명합니다. 이 기록들은 프로젝트가 불안정하다는 증거가 아니라, 어떤 돌발 상황에도 대응할 수 있는 저희 팀의 운영 능력을 보여주는 훈장입니다.”

“마지막으로, 저희는 보이지 않는 부분의 ‘안정성’까지 고민했습니다. Supabase 무료 티어의 제한된 자원 하에서 HikariCP 설정을 최적화한 것은, 우리 서비스가 실제 운영 환경의 제약을 이해하고 그 안에서 어떻게 효율적으로 살아남을 수 있는지 깊이 고민했다는 증거입니다.”

솔라는 숨을 한번 골랐다.

“결론적으로 저희 프로젝트의 완성도는 단순히 기능의 개수에 있지 않습니다. 실행, 배포, 오류 대응, 그리고 이 모든 과정에 대한 ‘설명 가능성’까지 확보했을 때 비로소 프로젝트는 ‘완성’된다고 믿습니다. 저희는 그 모든 과정을 증명할 수 있습니다.”

3분 스피치가 끝났을 때, 솔라는 스스로가 한 말에 전율했다. 이제야 비로소, 처음 루나가 보여주었던 모범 README의 그 길고 복잡했던 문장이 완벽하게 이해되었다.

본 프로젝트는 기능 구현뿐 아니라 Vercel을 이용한 프론트엔드 배포, Render를 이용한 백엔드 배포... Dockerfile을 통한 실행 환경 구성 및 DB 연결 과정에서 발생한 troubleshooting 경험을 함께 정리하였습니다.

그것은 기술의 나열이 아니었다. 한 편의 잘 짜인 이야기의 목차였던 것이다. 솔라는 자신의 노트북을 열었다. 텅 비어 있던 README 파일과, 기능 목록만 나열했던 발표 PPT 초안이 이전과 다르게 보였다. 그곳은 더 이상 부끄러운 실패나 귀찮은 절차를 기록하는 공간이 아니었다. 자신의 프로젝트가 얼마나 치열하게 살아남아 지금에 이르렀는지, 그 위대한 여정을 기록할 역사의 장이었다. 솔라는 키보드에 손을 올리고, 자신의 프로젝트가 가진 첫 번째 이야기부터 써 내려가기 시작했다.