네트워크와 HTTP: 패킷이 되어 여행을 떠나자

localhost의 평화, 그리고 깨진 환상

학창 시절 내 세상의 전부는 localhost였다. XAMPP 같은 걸 깔아서 PHP로 게시판을 짜도, 그건 어디까지나 내 컴퓨터 안에서만 도는 세상이었다.

입사 초기에는 운 좋게 프론트엔드 유지보수 업무를 주로 맡았다. Quasar(Vue.js) 기반의 어드민 페이지였는데, 업무 난이도는 높지 않았다. 오타를 수정하거나, 선임들이 이미 잘 만들어둔 API를 axios.get()으로 호출해서 화면에 뿌려주기만 하면 됐다.

그때 나에게 axios는 마법의 지팡이 같았다. 복잡한 원리는 몰라도 URL만 넣으면 데이터를 뚝딱 가져와 줬으니까. “프론트엔드랑 백엔드 통신하는 거 별거 없네?” 하는 건방진 생각도 했었다.

하지만 그 평화는 오래가지 못했다. 나는 인력이 부족한 중소기업의 ‘생계형 풀스택’ 개발자였기 때문이다.

얼마 안 가 화면 수정이 아닌, 새로운 기능을 위한 API를 직접 만들어야 하는 순간이 왔다. 유튜브로 급하게 배운 스프링 부트(Spring Boot)로 서버를 띄우고, 호기롭게 프론트엔드에서 요청을 보냈다. 하지만 내 화면에 뜬 것은 데이터가 아니라 빨간색 CORS 에러와 Timeout 에러뿐이었다.

“분명 로컬 변수로 넘길 땐 잘 됐는데, 왜 네트워크만 타면 안 되지?”

내 컴퓨터(Localhost) 안에서 변수 A를 변수 B로 옮기는 건 100% 보장된 안전한 길이었다. 하지만 랜선 밖의 세상은 달랐다. 거기는 신호등이 고장 난 사거리와 같았고, 내 데이터는 그 위험한 도로를 달리는 트럭이었다. 나는 그동안 트럭에 짐을 실어 보내기만 했지, 그 트럭이 어떤 경로로, 어떻게 쪼개져서 가는지 전혀 모르고 있었던 것이다.

내 컴퓨터를 벗어나는 순간, 데이터는 거친 야생의 도로를 달려야 한다.

디지털 물류 센터의 배송 시스템

우리의 ‘디지털 물류 센터’ 세계관을 확장해보자. 이제 우리 공장(컴퓨터)에서 만든 물건(데이터)을 저 멀리 있는 고객(서버)에게 배송해야 한다.

1. 패킷(Packet): 표준 배송 박스

우리가 보내려는 데이터가 아무리 커도(예: 1GB 동영상), 한 번에 보낼 수는 없다. 도로는 좁고 트럭은 작기 때문이다. 그래서 우리는 데이터를 아주 작은 조각으로 잘라서, 규격화된 박스에 담는다. 이것이 바로 ‘패킷(Packet)’이다.

2. IP (Internet Protocol): 주소와 내비게이션

박스만 있으면 뭐하나? 어디로 갈지 알아야 한다.

  • IP 주소: 배송지 주소 (예: 192.168.0.1)
  • 라우터(Router): 갈림길마다 서 있는 교통경찰. “이 주소로 가려면 저쪽 길로 가세요”라고 안내한다.

3. TCP/UDP: 배송 방식의 차이

이제 박스를 어떤 트럭에 실어 보낼지 결정해야 한다.

  • TCP (Transmission Control Protocol): 믿음직한 우체국 택배.
    • “잘 받았나요?” 하고 꼼꼼히 확인한다(3-way Handshake).
    • 박스 순서가 뒤섞이면 다시 정렬해주고, 하나라도 분실되면 재배송해준다.
    • 웹(HTTP), 이메일, 파일 전송에 쓰인다. (느리지만 확실함)
  • UDP (User Datagram Protocol): 쿨한 드론 배송.
    • 그냥 던져놓고 간다. 받았는지 확인 안 한다.
    • 박스 하나쯤 없어져도 상관없다. 대신 엄청 빠르다.
    • 스트리밍, 온라인 게임에 쓰인다. (빠르지만 불안함)
확실하게 줄 것인가(TCP), 일단 빨리 던질 것인가(UDP).

[Code Verification] HTTP의 민낯은 그냥 텍스트다

우리가 흔히 쓰는 HTTP는 사실 TCP라는 택배 트럭 위에 실린 ‘편지 내용’일 뿐이다. 브라우저나 라이브러리 없이, 가장 원시적인 방법(Socket)으로 네이버 서버에 말을 걸어보자. HTTP가 얼마나 단순한 텍스트 쪼가리인지 알게 될 것이다.

import socket

# 1. TCP 소켓 생성 (전화기 들기)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 네이버 서버(223.130.195.200)의 80번 포트로 연결 (전화 걸기)
# 실제 네이버 IP는 바뀔 수 있음 (nslookup [www.naver.com](https://www.naver.com) 로 확인 가능)
target_host = "[www.naver.com](https://www.naver.com)"
target_port = 80
sock.connect((target_host, target_port))

# 3. HTTP 요청 메시지 작성 (할 말 적기)
# 가장 날것의 HTTP 요청 형태
request = "GET / HTTP/1.1\r\nHost: [www.naver.com](https://www.naver.com)\r\n\r\n"

# 4. 전송 (말하기)
sock.send(request.encode())

# 5. 응답 받기 (듣기)
response = sock.recv(4096)
print(response.decode())

# 6. 연결 종료 (끊기)
sock.close()

실행 결과 (일부):

HTTP/1.1 200 OK
Server: NWS
Date: Mon, 01 Jan 2024 00:00:00 GMT
Content-Type: text/html; charset=UTF-8
...
<html>...</html>

분석: 우리가 axios.get()을 호출할 때 내부적으로 벌어지는 일은 이게 전부다.

  1. 상대방 IP와 포트로 TCP 연결(Connect)을 맺는다.
  2. GET / HTTP/1.1이라는 텍스트를 보낸다.
  3. 서버가 보내주는 텍스트를 받는다.

HTTP는 특별한 기술이 아니라, 그저 “우리 대화할 때 이런 양식(Protocol)으로 텍스트를 주고받자”라고 정한 약속일 뿐이다.

실무에서의 Trade-off: 신뢰성이냐 속도냐

실무, 특히 백엔드 개발을 하다 보면 TCP와 UDP 사이에서 고민할 때가 온다. (물론 대부분은 TCP 기반의 HTTP를 쓰지만)

최근 면접 질문으로도 자주 나오는 HTTP/3가 바로 이 Trade-off의 산물이다. 우리가 쓰는 인터넷(HTTP/1.1, HTTP/2)은 그동안 TCP 위에서 돌아갔다. 데이터가 절대 깨지면 안 되니까.

하지만 시대가 변했다. 사람들은 4K 동영상을 끊김 없이 보고 싶어 한다. TCP의 그 꼼꼼한 확인 절차(Handshake)와 “순서대로 줄 서세요(Head of Line Blocking)”라는 특징이 이제는 답답해진 것이다.

그래서 구글(Google)은 생각했다. “야, 그냥 UDP 쓰자. 불안한 건 우리가 애플리케이션 레벨에서 대충 때우면 되잖아. 일단 빨리 보내는 게 중요해!”

이것이 QUIC 프로토콜이고, HTTP/3의 기반이다. 무조건적인 안정성(TCP)을 버리고, 약간의 위험을 감수하더라도 압도적인 속도(UDP)를 선택한 현대 기술의 흐름이다.

더 빠른 속도를 위해, 우리는 가장 밑바닥의 운송 수단(TCP -> UDP)까지 바꾸고 있다.

마치며: 보이지 않는 도로를 상상하라

이제 우리는 데이터가 어떻게 쪼개져서(패킷), 어떤 트럭(TCP/UDP)을 타고 여행하는지 알게 되었다.

프론트엔드에서 API 요청이 실패했을 때, 예전처럼 단순히 “서버가 죽었나?”라고만 생각하지 말자. “아, 3-way handshake가 실패했나? 중간에 라우터에서 패킷이 드랍됐나? 아니면 DNS가 주소를 못 찾았나?” 이렇게 네트워크 레이어를 상상할 수 있다면, 당신은 이미 ‘그냥 코더’가 아니다.

자, 이제 우리 공장의 물건이 고객(클라이언트)에게 무사히 도착했다. 그런데 고객은 이 물건을 받아서 어떻게 포장을 뜯고 화면에 보여줄까? 우리가 보낸 HTML 코드는 브라우저에서 어떻게 그림이 될까?

다음 시간에는 웹 개발의 종착지, 브라우저 렌더링 원리(Browser Rendering)에 대해 이야기해 보겠다

댓글 남기기