백엔드와 네트워크, 그리고 배포 — 내 화면을 세상에 잇기
한 줄 요약
localhost에 갇혀 있던 화면이 왜 남에게 안 보이는지부터 시작해 네트워크·백엔드·보안·배포를 훑고, 프론트와 백엔드를 붙인 서비스를 만들어 배포했다.
1. 오늘 배운 것
오늘의 산출물은 코드가 아니라 “내 서비스를 세상에 내보내는 데 필요한 것들의 지도” 였다. 지도를 손에 쥐어야 AI에게 “이렇게 만들어 줘”라고 자신 있게 말할 수 있다는 것이 수업의 전제였다.
| 순서 | 무엇에 답하는가 |
|---|---|
| 왜 백엔드인가 | 내가 만든 화면은 왜 남에게 안 보이는가 |
| 주소가 화면이 되기까지 | 인터넷은 어떻게 나를 그 서버까지 데려가는가 |
| 요청과 응답 | 브라우저와 서버는 무슨 말로 대화하는가 |
| 백엔드가 하는 일 | 서버 안에서는 무슨 일이 벌어지는가 |
| 안전하게 만들기 | AI가 짜준 코드에서 무엇이 위험한가 |
| 세상에 내보내기 | 어디에 올리고, 얼마가 드는가 |
1-1. 왜 백엔드가 필요한가 — 세 가지 증상, 하나의 원인
| 증상 | 진짜 원인 |
|---|---|
| 새로고침하면 입력한 게 사라진다 | 데이터를 저장해 둘 곳이 없다 |
| 친구에게 주소를 줘도 안 열린다 | localhost는 “내 컴퓨터” 라는 뜻이다 |
| 노트북을 닫으면 화면도 꺼진다 | 서비스가 내 컴퓨터에서만 돌아간다 |
localhost가 핵심이었다. 이건 “지금 이 컴퓨터”를 가리키는 특별한 주소다.
그래서 친구가 자기 컴퓨터에서 localhost:3000을 열면 친구 자기 컴퓨터의 3000번을 찾는다. 거기엔 아무것도 없으니 연결이 안 된다.
내 방 주소를 알려줬는데 친구가 자기 집에서 그 방을 찾는 셈이다.
클라이언트와 서버는 손님과 24시간 주방이다.
- 클라이언트(내 브라우저) = 주문하러 온 손님. 왔다가 간다
- 서버 = 늘 켜져 있는 주방 겸 창고. 주문을 받고, 음식을 내주고, 재료를 보관한다
지금까지 만든 화면은 홀 인테리어만 있는 가게였다. 예쁘지만 주방이 없으니 진짜 주문은 못 받는다.
판단의 실마리도 하나 얻었다. 여러 사람이 나눠 쓰거나, 껐다 켜도 남아야 하거나, 비밀이 오가는 것은 대개 서버가 필요하다. 반대로 버튼 색 바꾸기나 애니메이션처럼 내 화면 안에서 잠깐 보이고 마는 것은 서버 없이도 된다.
1-2. 주소가 화면이 되기까지 — 네 조각
① 도메인과 DNS. 모든 컴퓨터에는 IP 주소라는 번호가 있다. 사람이 그걸 외울 수 없으니 mysite.com 같은 이름을 쓰고, 이름을 번호로 바꿔 주는 전화번호부가 DNS다.
도메인은 단순한 별명이 아니었다. 이름과 실제 서버 사이에 한 겹을 두는 것이 핵심이다. 앞(이름)은 그대로 두고 뒤(서버)를 얼마든지 바꿀 수 있다. 서버를 옮겨도 세상에 뿌린 주소는 손대지 않아도 되고, 하나의 이름에 여러 서버를 물릴 수도 있다.
왜 바꿨는데 바로 안 바뀌지? DNS는 답을 한동안 기억(캐시)해 둔다. 그래서 누구는 새 주소를, 누구는 옛 주소를 본다. 몇 분에서 몇 시간이면 퍼진다.
② 서버와 호스팅, 그리고 내부망·외부망. 서버는 내 파일과 데이터를 올려 두고 24시간 켜져 있는 컴퓨터다. 요즘은 직접 사지 않고 클라우드에서 빌려 쓴다(호스팅).
네트워크는 두 세계로 나뉜다.
- 내부망(LAN) — 공유기로 묶인 우리 집 안.
192.168.0.5같은 집 전용 주소를 쓴다 - 외부망(WAN) — 그 바깥의 전 세계. 밖에서 볼 때 우리 집은 공유기 하나로만 보인다
둘 사이의 문지기가 공유기(NAT)다. 안에서 밖으로는 자유롭지만, 밖에서 안으로는 기본적으로 못 들어온다.
localhost:3000이 나한테만 보이는 이유가 이것이다. 그건 내부망 안에만 있으니까.
③ 포트와 방화벽. IP가 건물 주소라면 포트는 호실 번호다. 컴퓨터 한 대에 문이 수만 개 있고, 프로그램마다 자기 문에서 손님을 받는다. 웹은 보통 80번(HTTP)과 443번(HTTPS)을 쓴다. localhost:3000의 3000이 바로 그 문 번호다. 방화벽은 그 문을 지키는 경비다.
④ HTTP와 HTTPS. 브라우저와 서버의 공통 언어다. “이 페이지 줘”(요청) → “여기 있어”(응답). HTTPS는 그 대화를 암호로 감싼 버전이다. HTTP는 엽서, HTTPS는 밀봉된 봉투에 가깝다.
자물쇠에 대한 흔한 오해 하나. 자물쇠는 “믿을 만한 회사”라는 증명이 아니다. “이 도메인의 주인과 암호를 걸어 통신 중”이라는 뜻일 뿐이라, 피싱 사이트도 자기 가짜 도메인에 자물쇠를 붙일 수 있다. 자물쇠 유무보다 도메인 철자 자체를 확인하는 게 더 중요하다.
이 넷이 함께 움직이면 한 번의 여정이 된다.
1-3. 요청과 응답 — 상태 코드는 신호등
서버는 응답할 때 세 자리 숫자를 함께 보낸다. 수백 개를 외울 필요 없이 맨 앞 숫자만 보면 된다.
| 앞자리 | 뜻 | 신호등으로 |
|---|---|---|
| 2xx | 성공 | 초록불, 통과 |
| 3xx | 다른 데로 | 노란불, 우회 |
| 4xx | 요청(나)의 잘못 | 주소를 틀렸거나 권한이 없음 |
| 5xx | 서버의 잘못 | 서버가 터짐 |
자주 만나는 것들.
| 코드 | 한마디로 |
|---|---|
| 200 OK | “다 잘됐어” |
| 401 Unauthorized | “누구세요?” — 아직 로그인 안 함 |
| 403 Forbidden | “누군지 알지만, 넌 안 돼” |
| 404 Not Found | “그런 페이지 없어” |
| 429 Too Many Requests | “천천히 좀” |
| 500 Internal Server Error | “네 잘못 아냐, 내가 터졌어” |
실전 감각은 이거였다. 4xx면 내 요청을 고치고, 5xx면 만든 사람(서버) 문제다. 그리고 이 정보를 AI에게 그대로 붙여 주면 원인을 훨씬 잘 찾는다.
직접 보는 법도 배웠다. 화면에서 우클릭 → 검사 → 네트워크 탭 → 새로고침. Status 칸에 숫자가 뜬다. 화면이 안 뜰 때 빨간 줄(4xx·5xx)을 찾으면 어디가 문제인지 단서가 나온다.
1-4. 백엔드가 실제로 하는 일
한 문장이면 충분했다. 요청을 받아서, 처리하고, 응답을 돌려준다.
주방 비유를 이어 보면 — 홀에서 주문서(요청)를 받고, 레시피(비즈니스 로직)대로 조리하고, 창고(데이터베이스)에서 재료를 꺼내고, 완성한 요리를 내보낸다(응답).
엔드포인트와 API. 백엔드는 통째로 열려 있지 않고 정해진 몇 개의 창구만 연다. 창구 하나는 “주소 + 동작”의 조합이고, 그게 곧 하나의 기능이다.
GET /products → "상품 목록 주세요"
POST /login → "이 정보로 로그인 처리해 주세요"
주소는 무엇(명사), 동작은 메서드로 정한다. 그래서 /getProducts가 아니라 /products다.
CRUD. 대부분의 서비스가 데이터에 하는 일은 사실 넷뿐이다 — 만들기(Create) · 읽기(Read) · 고치기(Update) · 지우기(Delete). 그래서 “어떤 창구가 필요할까?”는 대개 “이 데이터에 CRUD 중 무엇을 허용할까?” 로 바꿔 생각할 수 있다.
JSON. 프론트와 백엔드가 서로 다른 환경에서 도는데도 알아듣는 이유는 공통 형식을 쓰기 때문이다. 지난 회차의 목업 데이터가 바로 이 모양이었다.
그리고 오늘 가장 중요했던 한 문장.
프론트는 못 믿는다. 프론트엔드는 사용자의 컴퓨터에서 돌아가므로 사용자가 얼마든지 조작할 수 있다. 화면에 “가격 10,000원”이라고 떠 있어도, 브라우저에서 그 값을 100원으로 바꿔 주문을 보낼 수 있다. 그래서 가격·수량·권한 검증은 반드시 백엔드에서 다시 해야 한다. 프론트의 검증은 사용자 편의를 위한 것이고, 진짜 방어선은 언제나 백엔드다.
1-5. 안전하게 만들기 — AI 코드의 약점 세 가지
바이브 코딩에는 흔한 실패 패턴이 있다. 첫날엔 “하루 만에 화면이 다 만들어졌어!”, 다음 주엔 “로그인이 자꾸 풀려… 저장한 게 사라져… 요금이 왜 이렇게 나왔지?”
AI는 “돌아가는 코드”는 잘 만들지만 “안전하고 튼튼한 코드”는 놓친다. 명시적으로 요구하지 않으면 보안을 알아서 챙기지 않는다. 보안 회사 Veracode의 2025년 실험에서 AI가 생성한 코드의 약 45%에 보안 결함이 있었고, 모델이 똑똑해져도 이 비율은 거의 줄지 않았다고 한다.
① 입력을 그대로 믿지 않기. 검색창의 이름 칸에 명령어처럼 생긴 문자열을 넣었을 때 서버가 그대로 실행하면 데이터베이스가 통째로 털릴 수 있다. 규칙은 하나 — 사용자가 보낸 값은 일단 의심하고 검사한다.
② 비밀은 코드에 적지 않는다 — 환경변수. API 키를 코드에 적어 GitHub에 올리면 몇 분 만에 자동 프로그램이 찾아간다. 그 키로 남이 비싼 작업을 돌리면 요금은 내 몫이다.
그래서 비밀값은 코드 바깥의 .env 파일에 두고, 그 파일은 .gitignore로 깃에 안 올라가게 막는다.
한 줄 요약: 코드 = 공개해도 되는 것 / 환경변수 = 절대 공개하면 안 되는 것.
③ 인증과 인가는 다른 것이다. 콘서트장으로 보면 딱 맞는다.
- 인증(Authentication) = 입구의 표 검사원. “당신, 표 주인 맞아요?” → 실패하면 401
- 인가(Authorization) = VIP 구역의 바운서. “들어는 왔는데, 이 방에 들어갈 권한은 있어요?” → 없으면 403
로그인에 성공하면 서버가 토큰을 준다. 클럽 입구에서 받는 손목 밴드 같은 것이다. 한 번 확인받고 밴드를 차면 이후엔 매번 신분증을 안 보여줘도 된다.
비밀번호는 그대로 저장하면 안 된다. 대신 해시로, 한쪽으로만 계산되는 방식으로 바꿔 저장한다. 달걀을 프라이로 만들 수는 있어도 프라이를 달걀로 되돌릴 수 없는 것과 같다.
여기에 사람마다 다른 무작위 값인 솔트를 섞는다. 같은 123456이라도 사람마다 결과가 달라지기 때문이다.
쿠키와 세션 — 웹은 원래 건망증이 심하다. 초창기 HTTP는 요청 하나하나를 매번 처음 보는 손님처럼 취급했다. 그대로라면 로그인하고 다음 페이지로 가는 순간 로그아웃된다.
- 세션 = 사이트 금고 속 내 서류철. 진짜 정보(장바구니, 권한)가 여기 있다
- 쿠키 = 그 서류철을 찾는 번호표. 내 브라우저가 갖고 다니며 매번 보여준다
그래서 진짜 정보는 브라우저에 두면 안 된다. 쿠키는 사용자가 조작할 수 있어서 이름=일반사용자를 이름=관리자로 바꾸는 장난이 가능하다. 브라우저엔 의미 없는 번호만, 진짜 정보는 서버 금고에 둔다.
1-6. 세상에 내보내기 — 배포
배포는 내 컴퓨터에만 있던 서비스를 늘 켜진 서버에 올려 누구나 주소로 들어올 수 있게 하는 것이다.
보통 세 군데에 나눠 올린다. 각자 잘하는 게 다르기 때문이다.
| 무엇을 | 어디에 (예) | 왜 |
|---|---|---|
| 화면(프론트) | Vercel · Netlify · Cloudflare Pages | 손님 가까운 곳에서 빠르게 |
| 서버(백엔드) | Render · Railway | 요청을 계속 처리해야 |
| 데이터(DB) | Supabase 등 | 안전하게 보관해야 |
요즘 배포는 GitHub에 올리면 자동으로 된다. 코드를 GitHub에 올리고 → 배포 플랫폼에서 저장소를 연결하면 → 코드가 바뀔 때마다 자동으로 다시 배포된다. 그리고 주소를 하나 만들어 준다.
초보가 가장 많이 막히는 세 곳도 미리 배웠다.
- 환경변수 빼먹기 —
.env는 일부러 GitHub에 안 올리므로, 배포 플랫폼 설정 화면에 따로 넣어 줘야 한다. “로컬에선 됐는데 배포하니 안 돼”의 1등 원인 - CORS 막힘 — 화면과 서버가 다른 주소에 살면, 서버가 “이 화면에서 오는 요청은 허용”이라고 열어 줘야 한다
- 무료의 함정 — 무료 티어는 한동안 안 쓰면 잠들고, 기한이 있는 것도 있다
요금 폭탄을 피하는 세 가지도 있다. 요금 알림을 켜 두기, 내 플랫폼의 무료 한도를 알아 두기, 비밀 키를 지키기.
집 컴퓨터를 서버로 쓰는 길도 있다. 다만 내부망 구조 때문에 그냥은 안 되고, 원래는 포트 포워딩과 동적 DNS가 필요했다. 요즘은 터널이 이걸 대신한다. 밖에서 들어오게 문을 여는 대신, 내 컴퓨터가 바깥 서비스로 먼저 연결을 맺어 그 통로로 손님을 받는 방식이다. 그래서 포트를 열 필요가 없다.
다만 결론은 분명했다. 집 서버는 원리를 익히기 좋은 공부이고, 세상에 보여줄 때는 클라우드가 거의 항상 더 안전하고 손이 덜 간다.
왜 필요한가
오늘 배운 것 중에 내가 직접 코드로 쓸 것은 거의 없다. 코드는 AI가 쓴다. 그런데 무엇이 필요하고 무엇이 위험한지 판단하는 것은 내 몫이다.
“안 열려요”가 DNS 문제인지, 포트 문제인지, CORS 문제인지 구분하려면 이 지도가 있어야 한다. 그리고 AI에게 보안을 요구하려면, 요구할 항목이 무엇인지부터 알아야 한다.
2. 실습 / 코드 — 프론트와 백엔드를 붙인 서비스 만들기
과제 요건
프론트엔드와 백엔드를 둘 다 사용해 서비스를 만들고, GitHub에 올려 주소를 제출하는 것. 간단한 서비스로 잡고, LLM과 PRD를 먼저 작성한 뒤 진행하라는 조건이 붙었다.
수업에서 보여준 예시 프롬프트는 이런 모양이었다.
C:\aiffel_work\오늘의운세 에서 작업할 거야.
오늘의 운세 사이트를 만들어서 Tailscale Funnel로 외부에서 접속되게 해줘.
프론트엔드는 front 폴더에, 백엔드는 back 폴더에 구현해줘.
프론트엔드에서 이름, 생년월일을 입력받아 백엔드에 전송하고,
백엔드는 받은 정보로 오늘의 운세를 만들어서 프론트엔드로 전송한다.
API는 네가 판단해서 만들고, 어떻게 구성되었는지 md 파일로 정리해줘.
이 프롬프트에서 배울 점은 작업 위치·폴더 구조·데이터 흐름·산출물 문서까지 미리 정해 준다는 것이다. “오늘의 운세 만들어 줘” 한 줄과 결과가 완전히 달라진다.
내가 만든 것 — 그루뽑기
조별 수업에서 이끄미와 시간지키미를 뽑고, 그대로 학습 타이머로 이어지는 웹 서비스를 만들었다.
주소 → rolepicker-pink.vercel.app 코드 → github.com/songyee-ai/rolepicker
설치도 로그인도 없다. 링크 하나를 조 채팅방에 공유하면 그날부터 쓴다.
구조 — 오늘 배운 것이 그대로 들어갔다
app/ 주소록. "이 주소는 저 함수"만 가리킨다
front/ 화면
back/ 서버 — DB 접근, 배정, API 구현. 비밀값은 이 폴더 밖으로 안 나간다
shared/ 양쪽이 같이 쓰는 것 — 날짜, 이름 규칙, 주고받는 값의 모양
supabase/ DB 설계와 연결 방법
docs/ API 구성, 보안 정리, 배포 순서
요청이 지나가는 길은 이렇다.
브라우저 → Vercel의 API 라우트 → Supabase(Postgres)
브라우저는 데이터베이스에 직접 연결하지 않는다. 모든 읽기·쓰기가 서버를 거친다.
비밀값을 막는 세 겹
수업에서 “민감 정보는 오픈된 공간에 올리지 말고 따로 보관하도록 요청하라”고 배운 것을 그대로 적용했다.
- 이름 규칙 — Next.js는
NEXT_PUBLIC_으로 시작하는 환경변수를 브라우저까지 딸려 보낸다. 그래서 이 프로젝트엔 그 접두사가 하나도 없다. 실제 키는.env.local에만 있고.gitignore로 막았다 - 도구가 막는다 — 서버 전용 파일 첫 줄에
import 'server-only'를 붙였다. 화면 쪽에서 실수로 부르면 빌드가 그 자리에서 실패한다. 사람의 주의력이 아니라 도구가 막는 방식이다 - DB가 막는다 — 모든 테이블에 RLS를 켜고 브라우저 역할의 권한을 회수했다. 키가 새더라도 브라우저에서는 아무것도 읽지 못한다
배운 것 하나 — 링크가 곧 열쇠일 때
이 서비스에는 로그인이 없다. 링크를 아는 사람은 누구나 들어올 수 있다. 그래서 주소를 추측할 수 없게 만드는 것이 사실상 유일한 방어선이 되고, 요청 횟수 제한을 함께 걸었다.
로그인을 안 붙이는 선택도 보안 설계의 일부라는 걸 알게 됐다. 편의를 위해 뺐다면 그만큼 다른 곳을 조여야 한다.
3. 그림 / 스크린샷
4. 막혔던 점과 해결
| 막힌 지점 | 궁금했던 이유 / 원인 | 정리한 답 |
|---|---|---|
| 도메인을 파는 사이트가 여러 곳인데 뭐가 다른가? | 화면상으로는 다 비슷해 보여서 고르는 기준을 몰랐다 | 층이 다르다. ① 등록기관은 .com 같은 최상위 도메인의 원부를 관리하는 곳, ② 등록대행업체는 인가를 받아 실제 등록을 처리하는 곳, ③ 리셀러는 그 권한을 빌려 파는 중계상, ④ DNS 호스팅은 도메인이 어느 서버를 가리킬지 관리하는 곳이다(등록한 곳과 달라도 된다). 어디서 사든 도메인 자체는 같다. 다른 건 갱신 가격, 이전의 자유로움, 관리 화면, 개인정보 보호 제공 여부다. 특히 첫해 할인가가 아니라 갱신가를 봐야 한다 |
| 프론트와 백엔드 연결에서 막힘 (주소·CORS) | 화면과 서버가 다른 주소에 살면 브라우저가 요청을 차단한다 | 그루뽑기는 한 프로젝트 안에 화면과 API를 같이 두어 같은 주소에서 돌게 했다. 그러니 CORS 문제가 애초에 생기지 않았다. 나눠 올릴 때만 생기는 문제였다는 걸 알게 됐다 |
| Tailscale Funnel로 외부에 공개하는 단계에서 막힘 | 내부망·NAT 구조를 몰라서 “왜 그냥은 안 되는지”부터 이해가 안 됐다 | 밖에서 안으로 들어오는 길이 기본적으로 막혀 있다는 것이 출발점이었다. 터널은 그 방향을 뒤집어, 내 컴퓨터가 먼저 바깥으로 연결을 맺는다 (그림 3). 다만 내 컴퓨터가 꺼지면 서비스도 멈춘다. 그래서 최종 제출물은 Vercel에 배포했다 |
| 백엔드를 만들었는데 “어디서 도는지” 감이 안 왔다 | 서버를 따로 빌린 기억이 없어서 헷갈렸다 | Vercel이 화면과 API 라우트를 함께 올려 주는 구조였다. 별도 서버를 빌리지 않아도 백엔드가 도는 셈이고, 데이터만 Supabase에 따로 있다 (그림 4) |
5. 스스로 확인하기
- 내 화면이 세상과 끊겨 있던 이유를 손님·주방 비유로 한 문장으로 설명할 수 있는가?
localhost를 친구가 열면 왜 안 되는지 설명할 수 있는가?- 어떤 기능이 서버를 필요로 하는지 판단 기준을 말할 수 있는가?
- 401과 403의 차이를 말할 수 있는가?
- 4xx와 5xx 중 내가 고쳐야 하는 쪽은 어느 쪽인가?
- 공개키·개인키·세션키가 각각 무슨 일을 하는지 구분할 수 있는가?
- “프론트는 못 믿는다”가 무슨 뜻인지 예를 들어 설명할 수 있는가?
- 비밀 키를 어디에 두어야 하고, 왜 코드에 두면 안 되는지 말할 수 있는가?
- 배포 후 “로컬에선 됐는데 안 돼”일 때 가장 먼저 확인할 두 가지는?
6. 더 알아볼 것
- 개발자 도구 네트워크 탭을 열어 내 서비스의 요청을 하나씩 읽어 보기
- CORS가 실제로 막히는 상황을 한 번 만들어 보고, 어떻게 푸는지 확인하기
- Supabase RLS 정책을 직접 읽고, 무엇을 허용하고 무엇을 막고 있는지 설명해 보기
- 무료 티어의 한도와 요금 알림을 실제로 설정해 두기
- 로그인이 있는 서비스를 만들 때 인증·인가를 어떻게 붙이는지
- 지난 회차부터 밀린 숙제: PowerShell 기초, 비동기를 코드로 확인하기