개발 프로젝트/[외주 프로젝트] 첫 외주 웹사이트 제작기: 기획부터 배포까지

배포하면서 만난 문제들: Vercel, Render, Supabase, 도메인 연결

namerong 2026. 6. 20. 00:16

이번 프로젝트를 진행하면서 가장 많이 배운 구간은 배포였다.

개발 초반에는 “프론트는 Vercel에 올리고, 백엔드는 Render에 올리면 되겠지” 정도로 생각했다.
하지만 실제 운영 환경으로 옮겨보니 배포는 단순히 코드를 서버에 올리는 일이 아니었다.

프론트엔드, 백엔드, 데이터베이스, 환경변수, CORS, 도메인, HTTPS, DNS까지 모두 연결되어야 비로소 사용자가 접속할 수 있는 하나의 서비스가 된다.

특히 이번 프로젝트는 AI의 도움을 받아 빠르게 구현한 부분도 많았기 때문에, 단순히 “배포가 됐다”에서 끝내지 않고 왜 이런 구조로 배포했는지 다시 이해하는 과정이 필요했다.

프론트와 백엔드를 따로 배포한 이유

이번 프로젝트는 프론트엔드와 백엔드를 분리해서 배포했다.
프론트엔드는 사용자가 보는 화면을 담당한다. 홈 화면, 서비스 안내, 견적문의, 후기 작성, 관리자 페이지 같은 화면들이 여기에 포함된다.
반면 백엔드는 데이터 처리와 보안이 필요한 기능을 담당한다.
예를 들어 후기 등록, 후기 삭제, 관리자 비밀번호 확인, 통계 저장, Google Sheet 데이터 파싱 같은 기능은 백엔드에서 처리했다.

처음에는 “이 정도 규모면 프론트만으로도 가능하지 않을까?”라는 생각도 했다.
하지만 후기와 관리자 기능이 들어가면서 프론트만으로 처리하기에는 위험한 부분이 생겼다.

예를 들어 관리자 비밀번호나 DB 연결 정보는 절대 브라우저에 노출되면 안 된다.
프론트엔드 코드는 결국 사용자 브라우저로 내려가기 때문에, 민감한 로직을 프론트에 두는 것은 적절하지 않다.

그래서 역할을 나누었다.

Frontend
- 화면 구성
- 사용자 입력 처리
- API 요청
- 반응형 UI

Backend
- 데이터 저장
- 비밀번호 검증
- 관리자 인증
- 통계 기록
- 외부 API 연동

이 구조를 통해 화면과 데이터 처리의 책임을 분리할 수 있었다.
포트폴리오 관점에서도 단순 정적 페이지가 아니라, 프론트와 백엔드를 분리한 운영형 웹서비스를 경험했다는 점을 설명할 수 있다.

Vercel 배포 설정

프론트엔드는 Vercel에 배포했다.

Next.js는 Vercel과 궁합이 좋다. GitHub 레포지토리를 연결하면 코드를 푸시할 때 자동으로 빌드되고 배포된다.
이 점 때문에 프론트엔드 배포는 Vercel을 선택했다.

하지만 프로젝트 구조상 주의할 점이 있었다.

이번 프로젝트는 하나의 레포 안에 Frontend와 Backend 폴더가 따로 있는 구조였다.
그래서 Vercel이 루트 폴더가 아니라 Frontend 폴더를 기준으로 빌드하도록 설정해야 했다.

Root Directory: Frontend
Build Command: npm run build
Install Command: npm install

이 설정을 잘못하면 Vercel이 Next.js 프로젝트를 찾지 못하거나, 엉뚱한 위치에서 빌드를 실행하게 된다.

또 중요한 설정은 API 주소 환경변수였다.

프론트에서는 백엔드 API를 호출해야 한다.
로컬에서는 localhost를 사용하지만, 배포 후에는 Render에 올라간 백엔드 주소를 사용해야 한다.

그래서 Vercel에는 다음 환경변수를 설정했다.

NEXT_PUBLIC_API_BASE_URL=https://백엔드주소

Next.js에서 브라우저에서도 접근해야 하는 환경변수는 NEXT_PUBLIC_ 접두사가 필요하다.
이 부분을 이해하지 못하면 “환경변수를 넣었는데 왜 undefined가 나오지?” 같은 문제가 생길 수 있다.

정리하면 Vercel 배포에서 확인해야 했던 것은 다음과 같다.

프론트 폴더를 Root Directory로 설정했는지
빌드 명령어가 맞는지
백엔드 API 주소가 환경변수에 들어갔는지
운영 도메인과 연결되었는지
최신 배포가 Ready 상태인지

Render 백엔드 배포와 환경변수

백엔드는 Spring Boot로 만들었고, Render에 배포했다.

Spring Boot는 실행 중인 서버가 필요하다.
Vercel은 프론트엔드 배포에는 편하지만, 일반적인 Spring Boot 서버를 계속 띄워두는 용도와는 맞지 않는다.
그래서 백엔드는 별도 서버 플랫폼인 Render를 사용했다.

Render에서 가장 중요했던 부분은 환경변수 설정이었다.

백엔드는 DB, Google Sheet, 관리자 비밀번호, CORS 설정 등 여러 값을 필요로 한다.
이런 값들을 코드에 직접 작성하면 보안 문제가 생긴다.

그래서 Render 환경변수로 분리했다.

DATABASE_URL
DATABASE_USERNAME
DATABASE_PASSWORD
ADMIN_PASSWORD
GOOGLE_SHEET_ID
GOOGLE_SHEET_TITLE
CORS_ALLOWED_ORIGINS

이 구조의 장점은 코드 수정 없이 운영 설정을 바꿀 수 있다는 점이다.

예를 들어 관리자 비밀번호를 바꾸고 싶다면 코드를 수정하는 것이 아니라 Render의 ADMIN_PASSWORD 값만 변경하면 된다.
DB 비밀번호나 Google Sheet ID도 마찬가지다.

또 배포 후에는 백엔드가 정상적으로 떠 있는지 확인하기 위해 health check를 사용했다.

/actuator/health

정상이라면 다음과 같은 응답을 받을 수 있다.

{
  "status": "UP"
}

이런 확인 endpoint가 있으면 배포 후 문제를 구분하기 쉽다.

사이트가 안 될 때 프론트 문제인지, 백엔드 문제인지, DB 문제인지 나눠서 확인할 수 있기 때문이다.

Supabase DB 연결

후기와 통계 데이터는 Supabase PostgreSQL에 저장했다.

처음에는 MySQL도 고민할 수 있었다. 후기, 관리자, 통계 정도의 기능이라면 MySQL로도 충분하다.
하지만 이번 프로젝트에서는 PostgreSQL을 경험해보고 싶었고,
Supabase를 사용하면 별도의 DB 서버를 직접 설치하지 않아도 된다는 장점이 있었다.

Supabase는 PostgreSQL 기반의 DB를 제공한다.
백엔드에서는 Supabase에서 제공하는 연결 정보를 환경변수로 받아 사용했다.

여기서 중요한 점은 Spring Boot에서 사용하는 DB 연결 문자열 형식이다.

일반적인 PostgreSQL 연결 정보와 Spring Boot의 JDBC URL 형식은 다를 수 있다.
Spring Boot에서는 보통 다음과 같은 형태가 필요하다.

jdbc:postgresql://host:port/database

DB 연결이 정상적으로 되지 않으면 후기 등록, 후기 조회, 관리자 통계 기능이 모두 실패한다.

그래서 배포 후에는 단순히 메인 페이지가 열리는지만 확인하면 안 됐다.
실제 DB를 사용하는 기능을 직접 테스트해야 했다.

후기 등록
후기 조회
후기 삭제
관리자 후기 조회
통계 기록

이 과정을 통해 배포에서 중요한 것은 “서버가 켜졌다”가 아니라 “실제 기능이 운영 환경에서 동작한다”는 점이라는 걸 알게 됐다.

CORS, HTTPS, 도메인, Cloudflare 설정

프론트와 백엔드를 따로 배포하면 CORS 문제가 발생할 수 있다.

CORS는 브라우저 보안 정책 중 하나다.
프론트 주소와 백엔드 주소가 다르면, 백엔드가 해당 프론트 도메인의 요청을 허용해야 API 통신이 가능하다.

운영 구조는 대략 이랬다.

Frontend: <https://yangsimsangjo.com>
Backend: <https://yangsimsangjo-api.onrender.com>

서로 다른 출처이기 때문에 백엔드에서 프론트 도메인을 허용해야 했다.

그래서 Render 환경변수에 운영 프론트 도메인을 넣고, 백엔드 CORS 설정에서 해당 도메인만 허용하도록 구성했다.

CORS_ALLOWED_ORIGINS=https://yangsimsangjo.com

개발 중에는 *로 전체 허용을 해도 편할 수 있지만, 운영에서는 실제 도메인만 허용하는 것이 좋다.
특히 관리자 API가 있는 경우에는 더 신경 써야 한다.

도메인은 Gabia에서 구매하고, Cloudflare를 통해 DNS를 관리했다.

도메인 연결 흐름은 다음과 같았다.

Gabia에서 도메인 구매
Cloudflare에 도메인 등록
Gabia 네임서버를 Cloudflare 네임서버로 변경
Cloudflare에서 DNS 레코드 설정
Vercel에 도메인 연결
HTTPS 인증서 발급 확인

도메인을 연결하면서 알게 된 점은 DNS 변경이 바로 반영되지 않는다는 것이다.
네임서버 변경이나 DNS 레코드 수정 후에는 전파 시간이 필요하다.
빠르면 몇 분 안에 되지만, 경우에 따라 더 걸릴 수도 있다.

HTTPS는 Vercel과 Cloudflare를 통해 적용되었다.

운영 사이트에서는 HTTPS가 필수에 가깝다.
후기 작성 시 이름과 비밀번호를 입력하기 때문에, 사용자가 안심하고 사용할 수 있는 기본 환경을 만들어야 했다.

Free 플랜 콜드 스타트 문제와 Starter 전환

배포 후 실제 테스트에서 가장 크게 체감된 문제는 Render 무료 플랜의 콜드 스타트였다.

무료 플랜에서는 일정 시간 요청이 없으면 서버가 잠든다.
서버가 잠든 상태에서 사용자가 다시 접속하면, 서버가 다시 시작될 때까지 기다려야 한다.

실제로 테스트해보니 견적 계산기와 후기 기능에서 서버가 깨어나는 데 약 1분 정도 걸리는 경우가 있었다.

처음에는 Google Sheet API가 느린 것인지, DB 연결이 문제인지, 프론트 로딩 방식이 문제인지 의심했다.
하지만 백엔드 서버가 잠들었다가 다시 뜨는 콜드 스타트가 주요 원인이었다.

이 문제는 운영 사이트에서는 꽤 중요하다.

사용자가 견적문의 페이지에 들어갔는데 옵션이 1분 가까이 뜨지 않으면 사이트가 고장났다고 느낄 수 있다.
후기 페이지도 마찬가지다. 실제 사용자는 로딩 원인을 알 수 없기 때문에, 단순히 “느린 사이트”로 받아들인다.

그래서 최종 운영 단계에서는 Render Starter 플랜으로 전환했다.

Starter 전환의 목적은 단순히 성능을 높이는 것보다, 사이트가 항상 활성화된 상태에 가깝게 유지되도록 하는 것이었다.

Free 플랜
- 일정 시간 요청이 없으면 서버가 잠듦
- 첫 요청 시 콜드 스타트 발생
- 견적/후기 로딩이 오래 걸릴 수 있음

Starter 플랜
- 서버가 더 안정적으로 유지됨
- 첫 요청 지연 감소
- 운영 사이트에 더 적합

이 경험을 통해 무료 배포가 항상 운영에 적합한 것은 아니라는 걸 알게 됐다.
포트폴리오나 테스트 용도라면 무료 플랜도 충분할 수 있지만, 실제 사용자가 들어오는 사이트라면 응답 속도와 안정성도 비용에 포함해서 판단해야 한다.

AI로 구현한 부분을 다시 이해하는 과정

이번 프로젝트는 AI의 도움을 많이 받아 진행했다.

처음에는 기능 구현 속도를 높이는 데 큰 도움이 되었다. 하지만 배포 과정에서는 단순히 결과물만 보고 넘어갈 수 없었다.

AI가 코드를 작성해주더라도, 실제 운영 환경에서 문제가 생기면 결국 내가 구조를 이해하고 있어야 한다.

예를 들어 이런 질문에 답할 수 있어야 했다.

프론트는 왜 Vercel에 배포했는가?
백엔드는 왜 Render에 따로 배포했는가?
DB 비밀번호는 왜 코드가 아니라 환경변수에 넣는가?
CORS 오류는 왜 발생하는가?
도메인은 Vercel과 Cloudflare 중 어디에서 관리되는가?
Render Free 플랜이 왜 느리게 느껴졌는가?

이 질문들에 답하지 못하면 프로젝트를 포트폴리오에 올리더라도 깊이 있게 설명하기 어렵다고 느꼈다.

그래서 배포가 끝난 뒤에도 전체 구조를 다시 정리했다.
단순히 “AI로 만들었다”가 아니라, AI를 활용해 구현하고 내가 구조를 이해하며 운영 가능한 형태로 정리하는 것이 중요했다.

배포 후 최종 확인한 것들

배포 후에는 다음 항목들을 확인했다.

프론트 도메인 접속 여부
백엔드 health check 상태
견적 옵션 API 응답
후기 등록/조회/삭제
관리자 로그인
관리자 후기 삭제
통계 데이터 표시
CORS 오류 여부
HTTPS 적용 여부
모바일 접속 상태

특히 중요한 것은 사용자 흐름 그대로 테스트하는 것이었다.

메인 페이지가 보인다고 해서 배포가 끝난 것이 아니다.
견적을 선택했을 때 총액이 바뀌는지, 후기를 작성하면 DB에 저장되는지, 관리자 페이지에서 삭제가 되는지까지 확인해야 했다.

실제 운영 사이트에서는 작은 오류도 사용자에게는 큰 불편으로 느껴질 수 있다.

회고

이번 배포 과정에서 가장 크게 배운 점은 “배포는 버튼 한 번으로 끝나는 작업이 아니다”라는 것이다.

Vercel, Render, Supabase, Cloudflare는 각각 맡은 역할이 다르다. 하나라도 설정이 맞지 않으면 사이트 일부 기능이 동작하지 않는다.

처음에는 배포 도구들이 많아서 복잡하게 느껴졌지만, 역할을 나누어 정리하니 이해가 쉬워졌다.

Vercel: 프론트엔드 배포
Render: 백엔드 서버 실행
Supabase: PostgreSQL DB
Cloudflare: DNS와 도메인 관리
Gabia: 도메인 구매

또 이번 경험을 통해 포트폴리오에서 단순히 “배포 경험 있음”이라고 쓰는 것보다, 어떤 문제를 만났고 어떻게 해결했는지를 설명하는 것이 훨씬 중요하다는 걸 느꼈다.

특히 Render Free 플랜의 콜드 스타트 문제를 직접 겪고 Starter로 전환한 경험은 실제 운영 관점에서 좋은 학습이었다.

다음 프로젝트에서는 개발 초반부터 배포 구조를 먼저 그려볼 것 같다.

어떤 서비스에 올릴지, 환경변수는 무엇이 필요한지, DB는 어디에 둘지, 도메인은 어떻게 연결할지 미리 정리하면 배포 단계에서 훨씬 덜 흔들릴 것 같다.