Ping에서 HTTP까지
ping 명령에서 출발해 HTTP가 어떤 계층 위에서 동작하는지 살펴봅니다. printf와 netcat으로 HTTP 요청을 직접 보내며 계층 구조를 확인합니다.
프론트엔드 개발을 하다 보면 “요청이 안 간다”, “연결이 느리다”, “CORS 에러가 난다” 같은 문제를 만납니다. 이런 문제를 진단하려면 브라우저의 fetch 한 줄 아래에서 어떤 계층들이 동작하는지 알아두는 편이 좋습니다. 이 레슨은 ping 명령에서 출발해, HTTP가 어떤 계층 위에 올라타 있는지 정리합니다. printf와 netcat으로 HTTP 요청을 직접 보내보며 계층 구조를 확인합니다.
모든 트래픽은 패킷이다
인터넷의 모든 통신은 패킷(packet) 이라는 짧은 메시지로 쪼개져 이동합니다. 각 패킷에는 보낸 사람과 받는 사람의 주소가 적혀 있습니다. 우편엽서 여러 장이 와이파이·이더넷·케이블 모뎀 위를 오가는 모습으로 생각하면 됩니다. 큰 데이터(이미지, JS 번들 등)도 여러 패킷으로 나뉘어 전송된 뒤 받는 쪽에서 다시 합쳐집니다.
ping은 패킷이 출발지 → 목적지 → 출발지로 왕복할 수 있는지 확인하는 도구입니다. -c3은 “3번만 보내고 결과를 출력한 뒤 종료”라는 뜻입니다.
$ ping -c3 8.8.8.8
# 3 packets transmitted, 3 received, 0% packet loss8.8.8.8은 Google의 한 서비스 주소입니다. 여기서 중요한 점은, ping이 성공했다고 해서 그 주소에 웹사이트가 있다는 뜻은 아니라는 것입니다. ping은 HTTP와 무관하게 OS 차원에서 동작합니다(ICMP라는 별도 프로토콜의 echo request/reply). 즉 “서버까지 네트워크 경로는 살아 있지만 웹 서비스는 죽어 있는” 상황도 가능합니다. 그래서 사이트 장애를 진단할 때 ping이 되는지(=네트워크 경로 문제인지)와 HTTP 응답이 오는지(=서비스 문제인지)를 나눠서 보면 원인을 좁힐 수 있습니다.
ping -c3 8.8.8.8이 성공(0% loss)했을 때 합리적으로 내릴 수 있는 결론은?
netcat과 printf로 HTTP 요청 직접 보내기
HTTP 요청은 결국 정해진 형식의 텍스트입니다. 브라우저 없이도 그 텍스트를 직접 만들어 보낼 수 있습니다. printf는 포맷 문자열을 출력하고, nc(netcat)는 지정한 주소·포트로 TCP 연결을 맺어 받은 입력을 그대로 전송합니다. 둘을 파이프(|)로 연결합니다.
$ printf "HEAD / HTTP/1.1\r\nHost: en.wikipedia.org\r\n\r\n" | nc en.wikipedia.org 80여기서 \r\n은 줄바꿈이고, 마지막의 빈 줄(\r\n\r\n)이 “헤더 끝”을 알립니다. 이 형식은 HTTP의 약속이며, 2편에서 자세히 다룹니다.
주목할 점은 printf는 네트워크를 모르고, netcat은 HTTP를 모른다는 것입니다. netcat은 “포트에 연결해 바이트를 보낸다”만 할 뿐인데, 그 바이트가 마침 HTTP 형식이라서 서버가 알아듣고 응답합니다. 이로써 HTTP라는 텍스트 약속과 그 텍스트를 실어 나르는 연결·전송 계층(TCP) 이 서로 분리되어 있음을 알 수 있습니다. 브라우저의 fetch도 본질적으로는 이 텍스트를 만들어 TCP로 보내는 일을 대신해 주는 것입니다.
HTTP/1.1에서
Host:헤더는 필수입니다. 한 서버(IP 하나)가 여러 도메인을 호스팅할 수 있기 때문에(Apache의 virtual host, Nginx의 server block), 어느 사이트와 통신하려는지 알려줘야 합니다. 이 구조 덕분에 한 IP에 여러 사이트를 올릴 수 있습니다.
4계층 모델
네트워크는 여러 계층(layer)으로 나뉘어 있습니다. 각 계층은 바로 아래 계층에 의존하면서, 동시에 아래의 복잡함을 숨겨 위 계층에 더 단순한 인터페이스를 제공합니다. 덕분에 프론트엔드 개발자는 HTTP만 다루면 되고, 그 아래에서 패킷이 어떻게 라우팅되는지는 신경 쓰지 않아도 됩니다.
| 계층 | 이름 | 대표 프로토콜 | 다루는 것 | 누가 구현하나 |
|---|---|---|---|---|
| L4 | Application | HTTP · SSH · SMTP · DNS | URL, 쿠키, HTTP 메서드, 헤더 | 브라우저 · 웹 서버 |
| L3 | Transport | TCP · UDP | 포트 번호, “연결(connection)“이라는 개념 | OS(운영체제) |
| L2 | Internet | IP (v4 / v6) | IP 주소와 라우팅 | OS · 라우터 |
| L1 | Hardware | Wi-Fi · Ethernet · DSL | 실제 비트를 같은 물리 네트워크의 장치 사이로 운반 | 네트워크 카드 · 드라이버 |
인터넷 계층(L2)은 사실상 IP 하나뿐입니다. 위쪽의 모든 프로토콜과 아래쪽의 모든 하드웨어가 IP를 거치기 때문에, 이 구조를 좁은 허리(narrow waist) 라고 부릅니다. 모래시계의 잘록한 가운데처럼, IP 하나만 공통으로 맞추면 위아래로 다양한 기술을 자유롭게 조합할 수 있습니다.
프론트엔드 관점에서 이 표가 유용한 이유는, 문제가 어느 계층에서 났는지 가늠할 수 있기 때문입니다. DNS 조회 실패는 L4, 연결 자체가 안 맺히면 L3, 와이파이가 끊기면 L1 식으로 원인을 좁힐 수 있습니다.
서버가 된다는 것 = 포트를 리스닝한다는 것
포트 번호는 같은 컴퓨터 위의 여러 프로그램을 구분합니다. 전화기 한 대에 여러 내선번호가 있는 것과 같습니다. IP 주소가 컴퓨터를 찾아간다면, 포트 번호는 그 컴퓨터 안의 어느 프로그램과 통신할지 정합니다(HTTP는 80번, HTTPS는 443번, SSH는 22번).
nc -l 3456은 3456번 포트에서 연결을 기다리는(listen) 가장 단순한 형태의 서버입니다. 로컬 개발 서버(localhost:3000, localhost:5173 등)도 이렇게 특정 포트를 리스닝하는 프로그램입니다.
아무도 듣고 있지 않은 포트로 연결을 시도하면 “없는 번호입니다”에 해당하는 에러 패킷, 즉 RST(reset) 가 돌아옵니다. localhost:3000에 접속했는데 “연결이 거부됨(connection refused)“이 뜬다면, 보통 그 포트에서 리스닝하는 서버가 없다는 뜻입니다(개발 서버를 안 켰거나 다른 포트로 켠 경우).
- 같은 포트는 보통 한 번에 한 프로그램만 리스닝할 수 있습니다. 개발 서버가 “port already in use” 에러를 내는 이유입니다.
- 0~1023번은 예약 포트 라서 root(관리자) 권한이 있어야 리스닝할 수 있습니다. 그래서 80·443번 웹 서버를 직접 띄울 땐
sudo가 필요하고, 개발용으로는 3000·5173·8000 같은 높은 포트를 씁니다. sudo lsof -i로 현재 어떤 프로그램이 어느 포트를 리스닝 중(LISTEN)인지 확인할 수 있습니다. 포트 충돌을 디버깅할 때 유용합니다.
netcat으로 리스닝할 수 있는 가장 높은 포트 번호는?
동시 연결을 다루는 세 가지 모델
실제 브라우저와 서버는 여러 연결을 동시에 처리합니다. 한 페이지를 그리려면 HTML·이미지·스크립트·CSS를 병렬로 받아야 빠르기 때문입니다. 서버가 동시 연결을 처리하는 방식은 크게 세 가지입니다.
- Fork — 연결이 올 때마다 프로세스를 하나씩 복제해 담당시킵니다. 구현이 단순하지만 프로세스 생성 비용이 큽니다. 오래된 웹 서버나 SSH 데몬이 씁니다.
- 프로세스/스레드 풀 — 미리 만들어 둔 일꾼(worker)들이 연결을 하나씩 맡습니다. 매번 새로 만드는 것보다 빠르지만, 일꾼 수만큼만 동시에 처리할 수 있습니다.
- 이벤트 루프(단일 프로세스) — 하나의 프로세스가 여러 연결을 오가며, 준비된 작업만 골라 빠르게 처리합니다. 기다리는 시간(예: DB 응답 대기) 동안 다른 요청을 처리하므로 적은 자원으로 많은 연결을 다룰 수 있습니다.
세 번째 이벤트 루프 모델이 바로 Node.js의 동작 방식입니다. Node가 단일 스레드인데도 많은 요청을 처리할 수 있는 이유이고, 동시에 무거운 동기 작업(CPU를 오래 점유하는 계산)을 메인 스레드에서 돌리면 안 되는 이유이기도 합니다. 그런 작업은 루프 전체를 멈춰 다른 요청까지 지연시키기 때문입니다.
이 레슨을 다 읽으셨나요?
완료 표시는 이 브라우저에 저장됩니다.