서버와 클라이언트 관계 이해
브라우저 클라이언트와 백엔드 서버의 책임을 구분하고 HTTP 요청·처리·응답이 오가는 전체 흐름을 이해합니다.
지금까지는 사용자가 보는 화면을 다루는 프런트엔드(frontend) 개발에 집중했습니다. HTML, CSS, 자바스크립트와 React로 복잡한 화면을 만드는 방법도 익혔습니다. 하지만 실제 서비스는 브라우저 안에서만 완성되지 않습니다. 계정 관리, 데이터 저장과 조회, 권한 확인, 보안, 외부 서비스와의 연동 같은 핵심 기능은 대부분 백엔드(backend)에서 이루어집니다. 프런트엔드가 사용자와 맞닿은 화면이라면, 백엔드는 그 화면이 제대로 동작하게 만드는 시스템의 중심입니다.
12장 백엔드 기초와 API에서는 프런트엔드와 백엔드가 어떻게 협력하는지, 둘을 잇는 API(application programming interface)가 왜 중요한지를 다룹니다. 그 첫걸음으로, 웹이 동작하는 기본 구조인 클라이언트-서버 모델(client-server model)을 정리합니다. 이 모델은 정보를 요청하는 쪽인 클라이언트와 정보를 제공하는 쪽인 서버가 나뉘어 서로 통신하는 방식입니다.
클라이언트
클라이언트는 서비스를 요청하는 쪽입니다. 웹에서는 주로 사용자가 웹 애플리케이션에 접근할 때 쓰는 소프트웨어를 말합니다.
가장 대표적인 클라이언트는 Chrome, Firefox, Safari, Edge 같은 웹 브라우저입니다. 브라우저는 HTML, CSS, 자바스크립트를 해석해 화면을 그리고, 사용자의 클릭이나 입력을 받아 서버에 요청을 보냅니다. 스마트폰과 태블릿의 네이티브 앱, PC에 설치한 데스크톱 프로그램도 서버에 데이터를 요청하고 응답을 받는 클라이언트가 될 수 있습니다. Postman이나 Insomnia 같은 API 클라이언트 도구는 개발자가 HTTP 요청을 직접 만들어 서버의 API를 시험할 때 씁니다.
클라이언트의 주된 역할은 사용자의 요청을 서버에 전달하고, 서버에서 받은 응답을 사용자에게 알맞은 형태로 보여 주는 것입니다.
서버
서버는 클라이언트의 요청을 받아 처리하고, 그 결과를 클라이언트에게 응답으로 돌려주는 프로그램이나 컴퓨터입니다. 이름 그대로 서비스를 제공하는 쪽입니다.
웹 서버는 HTML, CSS, 자바스크립트, 이미지 같은 파일을 갖고 있다가 브라우저가 요청하면 그 파일을 보내며, Nginx나 Apache가 대표적입니다. 애플리케이션 서버는 요청을 받아 업무 로직을 실행하고 데이터베이스와 함께 데이터를 처리하는데, Java의 Tomcat, Node.js의 Express, Python의 Django와 Flask 같은 환경에서 만듭니다. 데이터베이스 서버는 많은 데이터를 체계적으로 저장하고 관리하며, 애플리케이션 서버의 요청에 따라 데이터를 조회하거나 저장합니다. MySQL, PostgreSQL, MongoDB 등이 있습니다.
서버의 주된 역할은 요청을 기다리다가, 요청이 오면 그 내용을 해석해 데이터 조회나 저장, 계산 같은 처리를 하고, 결과를 클라이언트에게 응답하는 것입니다.
HTTP로 오가는 요청과 응답
웹에서 클라이언트와 서버는 주로 HTTP(Hypertext Transfer Protocol)라는 통신 규약으로 데이터를 주고받습니다.
HTTP 클라이언트·서버 책임 경계 — 상태 추적
HTTP 요청에서 route·입력·권한·업무 처리·응답 해석·UI로 담당자와 보유 정보가 바뀐다. 한 요청의 성공/실패 시점과 아직 실행하지 않은 처리를 구분할 가치가 있다.
DATA-FLOW · B99 WEB FUNDAMENTALS
HTTP 클라이언트·서버 책임 경계
요청은 method·URL·headers·body를 담고 서버는 routing·validation·authentication·authorization·business logic·data access를 거쳐 응답한다. 클라이언트는 loading·success·error 상태를 렌더링하지만 권한과 데이터 무결성은 서버가 강제한다.
네트워크는 호출 상자가 아니라 책임 경계다. 요청과 응답의 메타데이터가 양쪽 실패 처리까지 연결한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 클라이언트 의도 | 클라이언트: 입력·진행 표시 / 서버: 요청 전 | 화면이 요청을 시작한다 | UI 의도가 HTTP 계약으로 전달된다 |
| 서버 판정 | 서버: 검증과 처리 / UI: 응답 대기 | 서버가 입력·권한·업무 조건을 확인한다 | 클라이언트 검사만으로 서버 판단을 대신할 수 없다 |
| 응답 해석 | 서버: 응답 전송 / UI: 성공 또는 오류 표시 | 클라이언트가 응답을 읽어 결과 화면을 고른다 | HTTP 결과와 UI 표현의 책임을 연결한다 |
HTTP 클라이언트·서버 책임 경계 — 상태 추적
HTTP 요청에서 route·입력·권한·업무 처리·응답 해석·UI로 담당자와 보유 정보가 바뀐다. 한 요청의 성공/실패 시점과 아직 실행하지 않은 처리를 구분할 가치가 있다.
DATA-FLOW · B99 WEB FUNDAMENTALS
HTTP 클라이언트·서버 책임 경계
요청은 method·URL·headers·body를 담고 서버는 routing·validation·authentication·authorization·business logic·data access를 거쳐 응답한다. 클라이언트는 loading·success·error 상태를 렌더링하지만 권한과 데이터 무결성은 서버가 강제한다.
네트워크는 호출 상자가 아니라 책임 경계다. 요청과 응답의 메타데이터가 양쪽 실패 처리까지 연결한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 클라이언트 의도 | 클라이언트: 입력·진행 표시 / 서버: 요청 전 | 화면이 요청을 시작한다 | UI 의도가 HTTP 계약으로 전달된다 |
| 서버 판정 | 서버: 검증과 처리 / UI: 응답 대기 | 서버가 입력·권한·업무 조건을 확인한다 | 클라이언트 검사만으로 서버 판단을 대신할 수 없다 |
| 응답 해석 | 서버: 응답 전송 / UI: 성공 또는 오류 표시 | 클라이언트가 응답을 읽어 결과 화면을 고른다 | HTTP 결과와 UI 표현의 책임을 연결한다 |
위 그림은 요청 하나가 클라이언트에서 서버로 갔다가 돌아오는 흐름을 보여 줍니다. 사용자가 주소를 입력하거나 링크를 누르거나 버튼을 누르면, 브라우저는 지금의 URL과 쿠키, 캐시 같은 상태를 바탕으로 HTTP 요청을 만듭니다. 요청에는 어떤 자원을 원하는지 나타내는 URL, 어떤 동작을 원하는지 나타내는 메서드(GET, POST, PUT, DELETE 등), 부가 정보인 헤더, 보낼 데이터인 본문이 들어갑니다.
서버는 요청을 받으면 메서드와 경로를 보고 처리할 라우트(route)를 고릅니다. 그다음 입력이 올바른지와 요청한 사용자가 권한이 있는지 검사하는데, 입력이 잘못되면 400, 로그인이 필요하면 401, 권한이 없으면 403처럼 그 자리에서 바로 응답합니다. 검사를 통과하면 업무 규칙을 적용하고, 데이터베이스에서 데이터를 조회하거나 한 번에 일관되게 바꿉니다.
처리가 끝나면 서버는 상태 코드, 헤더, 본문을 담은 HTTP 응답을 돌려줍니다. 상태 코드는 처리 결과를 나타내며 200 OK, 404 Not Found, 500 Internal Server Error 같은 값이 있고, 본문에는 HTML이나 JSON, 이미지 같은 데이터가 들어갑니다. 브라우저는 응답의 헤더를 보고 캐시에 저장하거나 다른 주소로 이동하고, 본문을 해석합니다. 본문이 HTML이면 화면을 그리고, JSON이면 자바스크립트가 데이터를 가공해 화면을 갱신합니다. 오류 응답이라면 그에 맞는 오류 화면으로 나뉘고, 마지막으로 화면과 URL, 포커스를 반영합니다.
그림 아래의 설명처럼 네트워크는 단순히 함수를 호출하는 상자가 아니라, 클라이언트와 서버가 할 일이 나뉘는 지점입니다. 클라이언트는 로딩, 성공, 오류 상태를 화면에 보여 주지만, 권한을 확인하고 데이터가 올바른지 지키는 일은 반드시 서버가 해야 합니다. 요청과 응답에 담긴 메서드, 상태 코드, 헤더 같은 정보가 양쪽의 실패 처리까지 연결해 줍니다. 이 요청과 응답의 왕복은 웹을 쓰는 동안 끊임없이 반복되며, 웹이 동작하는 기본 원리입니다.
더 깊이: HTTP 버전이 달라도 요청의 모양이 같은 이유
HTTP에는 HTTP/1.1, HTTP/2, HTTP/3 같은 여러 버전이 있지만, 이 글에서 본 메서드와 상태 코드, 헤더의 뜻은 어느 버전에서나 같습니다. 2022년에 개정된 HTTP 명세가 둘을 나눠 정했기 때문입니다. RFC 9110은 모든 버전이 함께 쓰는 의미(semantics)를 정하고, 그 메시지를 실제로 어떤 바이트로 주고받는지는 버전마다 따로 정합니다. HTTP/1.1은 RFC 9112, HTTP/2는 RFC 9113, HTTP/3는 RFC 9114이며, 캐시는 RFC 9111이 따로 다룹니다.
버전에 따라 달라지는 것은 전송 방식입니다. HTTP/1.1은 사람이 읽을 수 있는 글자로 된 메시지를 한 연결에서 차례로 주고받습니다. HTTP/2는 TCP 위에서 메시지를 이진 단위로 나눠, 한 연결 안에서 여러 요청과 응답을 섞어 보내는 다중화(multiplexing)를 하며, 웹에서는 보통 TLS를 함께 씁니다. HTTP/3는 UDP 기반의 QUIC 위에서 동작해, 동시에 오가는 메시지끼리 서로를 덜 기다리게 합니다. 두 버전이 메시지를 나눠 보내는 방식은 네트워크 교재의 HTTP/2와 HTTP/3와 QUIC에서 자세히 다룹니다.
그래서 fetch()로 요청을 보내는 코드나 Express에서 상태 코드를 정하는 코드는 버전이 바뀌어도 그대로입니다. 어느 버전으로 통신할지는 브라우저와 서버, 그 사이의 프록시가 정하며, 같은 페이지라도 자원마다 다른 버전이 쓰일 수 있습니다. 의미와 버전의 관계는 RFC 9110의 머리말에 정리되어 있습니다.
역할 나누기와 협업
클라이언트와 서버의 역할이 나뉘면서, 프런트엔드 개발자와 백엔드 개발자가 각자의 분야에 집중해 협업하는 방식이 일반적이 되었습니다. 프런트엔드 개발자는 주로 화면(UI)과 사용 경험(UX)을 맡아, 서버에서 받은 데이터를 화면에 보여 주고 사용자의 동작을 처리해 서버에 요청을 보냅니다. 지금까지 배운 내용이 여기에 해당합니다. 백엔드 개발자는 서버와 데이터베이스, 업무 로직, 보안, 인증을 맡아, 클라이언트가 필요로 하는 데이터를 효율적으로 제공하고 서비스의 핵심 기능을 만듭니다. 이 장부터 배울 내용입니다.
프런트엔드와 백엔드는 자바스크립트, Python, Java, PHP처럼 서로 다른 언어와 프레임워크를 쓸 수 있지만, HTTP라는 공통의 규약으로 데이터를 주고받으며 하나의 애플리케이션을 완성합니다. 그래서 어느 쪽에 문제가 있는지도 같은 순서로 따져 보면 원인을 빨리 좁힐 수 있습니다.
DEBUGGING FLOW
첫 실패 경계부터 증거로 좁히기
요청 → 응답 접근 → HTTP 상태 → 응답 계약 → UI 순서로 확인합니다. “아니요”가 나온 지점의 증거를 조사한 뒤 같은 요청으로 다시 검증합니다.
요청이 실제로 발생했나요?
예 · 다음 검사로
Network에 요청 항목이 있다
아니요 · 이벤트 연결부터 확인
증거: event handler, Console, loading state
응답을 읽을 수 있나요?
예 · 다음 검사로
fetch가 Response를 돌려준다
아니요 · 연결·브라우저 정책 확인
증거: Network, DNS/TLS, CORS, timeout·abort
HTTP 상태가 기대한 성공인가요?
예 · 다음 검사로
이 API에서 기대한 2xx 상태다
아니요 · 상태 코드와 서버 증거 확인
증거: 401/403 인증·권한, 400 입력, 409 충돌, 5xx 서버 로그
응답 내용이 계약에 맞나요?
예 · 다음 검사로
Content-Type·파싱·schema가 맞다
아니요 · 원본 응답과 파서 확인
증거: raw body, JSON 파싱 오류, schema 검증 결과
UI에 결과가 반영됐나요?
예 · 흐름 확인 완료
state와 화면이 기대대로 바뀐다
아니요 · 렌더링 상태 확인
증거: component state, render exception, 오류 UI
경계 하나를 수정하고 같은 요청으로 재검증
실패 증거를 찾은 지점부터 수정하고, 같은 입력·URL·메서드로 요청을 반복해 이전 증거와 비교합니다.
첫 검사부터 다시 따라가며 응답 내용과 UI 반영까지 확인합니다.
HTTP 응답을 받는 것과 성공 상태는 다릅니다. 또한 2xx라도 응답 파싱·schema·UI 반영은 따로 확인해야 합니다. 응답 본문이 없는 API는 해당 계약에 맞게 4번을 검사합니다.
위 그림은 화면이 기대대로 동작하지 않을 때 첫 실패 지점을 찾는 순서입니다. 다섯 가지 질문을 차례로 묻고, “아니요”가 나온 곳에서 그에 맞는 정보를 조사합니다. 먼저 요청이 실제로 일어났는지 봅니다. 개발자 도구의 Network 패널에 요청이 없다면 이벤트 핸들러가 연결되었는지, 콘솔에 오류가 있는지, 로딩 상태가 바뀌었는지를 확인합니다.
요청이 있었다면 응답을 읽을 수 있었는지, 즉 fetch()가 Response를 돌려줬는지 봅니다. 그렇지 않다면 네트워크 연결이나 DNS, TLS, CORS 같은 브라우저 정책, 시간 초과나 요청 취소를 확인합니다. 응답을 받았다면 상태 코드가 이 API에서 기대한 2xx 성공인지 봅니다. 아니라면 401과 403은 인증과 권한, 400은 입력, 409는 충돌, 5xx는 서버의 로그를 확인합니다.
상태가 성공이라면 응답의 내용이 약속한 형식에 맞는지, 즉 Content-Type과 JSON 해석, 데이터 구조가 맞는지 봅니다. 맞지 않다면 원래 응답 본문과 해석하는 코드를 비교합니다. 마지막으로 결과가 화면에 반영되었는지 보고, 반영되지 않았다면 컴포넌트의 상태와 렌더링 중의 예외, 오류 화면을 확인합니다.
원인을 찾은 지점 하나를 고친 뒤에는 같은 입력과 URL, 메서드로 요청을 다시 보내 이전 결과와 비교하고, 처음 질문부터 다시 따라가며 화면 반영까지 확인합니다. 그림 아래의 설명처럼 HTTP 응답을 받은 것과 성공 상태는 다르고, 2xx라도 응답 해석과 데이터 구조, 화면 반영은 따로 확인해야 합니다. 본문이 없는 응답을 돌려주는 API라면 네 번째 질문을 그 API의 형식에 맞게 검사합니다.
정리
- 웹은 정보를 요청하는 클라이언트와 정보를 제공하는 서버가 HTTP로 통신하는 클라이언트-서버 모델로 동작합니다.
- 클라이언트는 브라우저, 앱, 개발 도구처럼 요청을 보내고 응답을 보여 주는 쪽이며, 서버는 웹 서버, 애플리케이션 서버, 데이터베이스 서버로 나눠 볼 수 있습니다.
- 요청에는 메서드, URL, 헤더, 본문이, 응답에는 상태 코드, 헤더, 본문이 들어갑니다.
- 서버는 라우트를 고르고, 입력과 권한을 검사하고, 업무 규칙과 데이터베이스 처리를 거쳐 응답하며, 권한과 데이터의 정확성은 반드시 서버가 지킵니다.
- 문제가 생기면 요청 발생, 응답 수신, 상태 코드, 응답 내용, 화면 반영의 순서로 첫 실패 지점을 찾아 고칩니다.
연습 문제
1. 어떤 상태 코드일까
게시글 삭제 API에 다음 세 요청이 들어왔습니다. 그림의 입력과 권한 검사 단계를 기준으로 각각 어떤 상태 코드가 알맞을까요? ① 로그인하지 않은 사용자의 삭제 요청, ② 로그인했지만 다른 사람의 글을 지우려는 요청, ③ 로그인한 작성자가 보냈지만 글 ID가 숫자가 아닌 abc인 요청.
해설 보기
①은 401(Unauthorized)입니다. 누가 보냈는지 알 수 없으므로 먼저 인증이 필요하다고 알립니다. ②는 403(Forbidden)으로, 누구인지는 알지만 그 글을 지울 권한이 없습니다. ③은 400(Bad Request)으로, 요청의 형식 자체가 잘못되었습니다.
세 경우 모두 업무 규칙이나 데이터베이스에 이르기 전에 서버가 바로 응답합니다. 화면에서 삭제 버튼을 숨겼더라도, 서버는 요청마다 이 검사를 해야 합니다.
2. 버튼을 눌러도 아무 일이 없을 때
저장 버튼을 눌렀는데 아무 변화가 없고, Network 패널에도 새 요청이 보이지 않습니다. 서버의 로그부터 확인해야 할까요? 어디서부터 살펴봐야 할까요?
해설 보기
서버의 로그부터 볼 필요는 없습니다. 요청이 아예 일어나지 않았으므로 첫 번째 질문에서 “아니요”가 나온 것이고, 서버는 요청을 받은 적이 없습니다.
버튼에 이벤트 핸들러가 제대로 연결되었는지, 핸들러가 실행되다 콘솔에 오류가 났는지, 폼의 검사 때문에 제출이 막혔는지, 로딩 상태가 바뀌었는지를 클라이언트에서 먼저 확인합니다.
3. 200 OK인데 빈 화면
목록 API의 응답이 200 OK인데 화면의 목록이 비어 있습니다. 응답 본문을 보니 { "items": [...] } 형태였고, 화면 코드는 응답 전체를 배열로 생각하고 map()을 호출하고 있었습니다. 그림의 어느 질문에서 실패한 것이고, 어떻게 고쳐야 할까요?
해설 보기
네 번째 질문, 응답 내용이 약속한 형식에 맞는지에서 실패한 것입니다. 상태 코드는 성공이지만, 화면 코드가 기대한 배열과 실제 응답의 구조가 다릅니다. 객체에는 map()이 없으므로 렌더링 중에 오류가 나거나 빈 목록이 보입니다.
API가 약속한 형식이 { items: [...] }라면 화면 코드에서 data.items를 읽도록 고치고, 배열인지 검사하는 단계를 둡니다. 반대로 배열을 돌려주기로 약속했다면 서버를 고쳐야 하므로, 먼저 어느 쪽이 약속과 다른지 확인합니다.
다음 글 Node.js와 Express.js 입문에서는 브라우저 밖에서 자바스크립트를 실행하는 Node.js와, 그 위에서 웹 서버를 간단하게 만드는 Express로 첫 백엔드 서버를 만들어 봅니다.