웹 성능 최적화의 끝은 결국 네트워크에 달려 있다. 초창기 웹은 단순히 정적인 HTML 문서를 화면에 전달하는 단방향 구조(HyperText Transfer)에 불과했지만, 현대 웹은 수많은 자바스크립트 모듈과 고해상도 미디어 자원을 실시간으로 교환하는 거대한 애플리케이션으로 진화했다. 이러한 폭발적인 데이터 증가는 기존 HTTP 아키텍처의 한계를 드러냈고, 대역폭이 아무리 넓어져도 해결할 수 없는 지연 시간(Latency)이라는 병목 현상을 만들어냈다.
이 글에서는 HTTP/1.0부터 TCP를 버린 최신 HTTP/3(QUIC)까지의 진화 과정을 “왜(Why) 등장했는가”를 중심으로 살펴본다. 또한 HTTP의 메서드와 상태코드를 함께 정리하여, 웹 통신의 흐름을 한 번에 이해할 수 있도록 정리해본다.
1. 지연 시간과의 전쟁: HTTP 1.0 ~ 3.0(QUIC)까지의 진화
현대 웹 페이지를 렌더링하려면 수십에서 수백 개의 리소스(CSS, JavaScript, 이미지 등)가 필요하며, 이 과정에서 프로토콜의 전송 효율성은 곧 웹 서비스의 성능과 생존에 직결된다. HTTP의 발전 역사는 바로 이러한 수많은 리소스를 얼마나 빠르고 손실 없이, 적은 오버헤드로 전달할 수 있는가에 대한 끊임없는 고민과 기술적 진화를 볼 수 있게 해준다.
앞서 글의 도입부에서도 언급했듯, 새로운 기술이 등장하면 우리는 먼저 “왜(Why) 등장했는가?”를 이해해야 그 기술의 본질을 파악할 수 있다. 네트워크, 그중에서도 OSI 7계층의 애플리케이션 계층에서 동작하는 HTTP 프로토콜은 바로 지연 시간(Latency)과 전송 효율이라는 문제를 해결하기 위해 진화해왔다.
1.1 HTTP/1.1의 등장배경과 맹점
초기 HTTP/1.0 아키텍처는 '연결 없는(Connectionless)' 단발성 통신 모델을 채택했다. 클라이언트가 특정 리소스를 요청하고 서버가 응답을 반환하면, 그 즉시 기저의 연결을 끊어버리는 방식이다. 이는 웹 페이지 하나를 로드하기 위해 수십 개의 에셋을 받아야 하는 환경에서, 리소스 하나를 요청할 때마다 매번 무거운 연결 수립 과정을 거쳐야 함을 의미했으며 치명적인 네트워크 지연을 불러왔다. TCP/IP에 대해 자세히 보고싶다면? https://ezilog.dev/post/osi-7-layer-tcp-ip-4-layer

이러한 성능적 한계를 극복하기 위해 HTTP/1.1에서는 지속 연결 메커니즘이 기본값으로 도입되었다. 브라우저와 서버는 한 번 맺은 연결을 곧바로 끊지 않고 재사용함으로써, 후속 요청 시 발생하는 핸드셰이크 소모 시간을 비약적으로 단축시켰다. 나아가, 클라이언트가 이전 요청에 대한 응답을 온전히 기다리지 않고 여러 요청을 연속적으로 전송할 수 있는 파이프라이닝 기술이 고안되었다. 이론적으로 파이프라이닝은 유휴 대기 시간을 없애 네트워크 대역폭을 극대화할 수 있는 기술이었다.

그러나 HTTP/1.1의 파이프라이닝은 결정적인 구조적 결함을 안고 있었다. HTTP/1.1 명세는 클라이언트가 요청을 보낸 순서대로 서버가 반드시 응답해야 한다는 엄격한 동기화 규칙을 강제한다. 만약 첫 번째 요청이 데이터베이스 조회가 오래 걸리는 무거운 API 응답이거나 처리 과정에서 병목이 발생하면, 그 뒤에 파이프라이닝된 가벼운 정적 리소스(CSS, JS 파일 등)들은 앞선 응답이 완전히 전송될 때까지 서버의 출력 버퍼에서 계속 대기해야만 했다.
이를 HTTP 수준의 헤드 오브 라인 블로킹(Head-Of-Line Blocking, HOLB)이라 부른다. 결국 이 문제로 인해 파이프라이닝은 현대 브라우저에서 사실상 비활성화되었고, 브라우저들은 도메인당 최대 6개의 연결을 열어 병렬 처리를 물리적으로 흉내 내는 대안에 의존해야만 했다.

HTTP/1.0과 달리 HTTP/1.1에서는 위의 다이어그램처럼 하나의 TCP 연결에서 여러 요청을 처리할 수 있다. 그러나 요청을 순서대로 처리하는 구조 때문에, 첫 번째 요청이 매우 무거운 파일이라면 이후 요청들이 아무리 빠르게 처리될 수 있어도 첫 번째 요청이 끝날 때까지 기다려야 하는 문제가 발생한다.
그러면 비교적 가벼운 리소스를 먼저 받아 렌더링하고, 무거운 리소스는 나중에 받아 처리하면 이 문제를 해결할 수 있지 않을까? 이러한 필요에서 등장한 것이 바로 HTTP/2이고, 이를 가능하게한 기술이 멀티플렉싱이다.
1.2 HTTP/2의 구조적 혁신
2015년에 표준화된 HTTP/2는 기존의 텍스트 기반 HTTP/1.x와는 결이 완전히 다른 접근을 취한다. 단순히 성능을 개선한 수준이 아니라, 메시지를 주고받는 방식 자체를 다시 설계한 프로토콜이다. 핵심은 사람이 읽을 수 있는 텍스트 대신, 컴퓨터가 처리하기에 최적화된 바이너리 프레이밍(Binary Framing) 계층을 도입했다는 점이다.
HTTP/2에서는 모든 HTTP 메시지가 ‘프레임(Frame)’이라는 더 작은 단위로 쪼개진다. 헤더를 담는 HEADERS 프레임, 실제 데이터를 담는 DATA 프레임처럼 역할에 따라 나뉘고, 각 프레임에는 고유한 스트림 ID가 붙는다. 이 프레임들은 모두 바이너리로 인코딩되어 전송되기 때문에 파싱 속도가 빠르고, 전송 효율도 자연스럽게 좋아진다. 여기에 더해 HPACK 압축(헤더 압축)을 통해 매 요청마다 반복되던 Cookie나 User-Agent 같은 헤더를 크게 줄이면서 불필요한 네트워크 비용까지 함께 줄였다.
이 구조가 만들어낸 가장 큰 변화는 바로 멀티플렉싱(Multiplexing)이다. 하나의 TCP 연결 위에서 여러 요청과 응답을 동시에 처리할 수 있게 된 것이다. 예전처럼 요청 하나가 끝나기를 기다릴 필요 없이, 여러 스트림이 서로 뒤섞인 채 전송되고, 브라우저는 스트림 ID를 기준으로 이를 다시 조립한다. 덕분에 특정 요청이 지연되더라도 다른 요청이 함께 막히는 문제, 즉 HTTP 계층에서의 HOLB는 사실상 해결되었다.

HTTP/2는 애플리케이션 계층의 병목을 없애는 데는 성공했지만, 그 아래에 있는 TCP라는 전송 계층의 한계를 그대로 갖고있었다. 멀티플렉싱으로 수십, 수백 개의 스트림이 하나의 연결로 몰리게 되자, 오히려 TCP의 특성이 더 크게 작용했다. TCP는 데이터의 순서와 무결성을 보장하기 위해, 중간에 패킷 하나라도 유실되면 해당 패킷이 복구될 때까지 뒤에 오는 모든 데이터를 멈춰 세운다.
문제는 이 ‘멈춤’이 스트림 단위가 아니라 연결 전체에 영향을 준다는 점이다. 예를 들어, 단순한 이미지 하나의 패킷이 유실되었을 뿐인데, 그 뒤에 도착한 자바스크립트 데이터까지 전부 버퍼에 묶여버리는 상황이 발생한다. HTTP/2가 논리적으로는 완전히 병렬화된 것처럼 보이지만, 실제 네트워크에서는 여전히 하나의 줄에 서 있는 셈이다.
결국 HTTP/2는 “HTTP 레벨의 HOLB”는 해결했지만, 대신 “TCP 레벨의 HOLB”라는 더 근본적인 문제를 마주하게 된다. 특히 패킷 손실이 잦은 모바일 환경에서는 이 문제가 더 크게 체감되면서, 다음 단계의 진화를 필요로 하게 된다.
1.3 HTTP/3의 패러다임 전환(QUIC)
TCP 기반 구조의 한계를 끝까지 밀어붙인 결과, 엔지니어들은 결국 하나의 결론에 도달한다.
“문제를 해결하려면, TCP의 한계를 우회하는 새로운 접근이 필요하다.”
이 발상에서 출발한 것이 구글이 주도하고 IETF가 표준화한 QUIC(Quick UDP Internet Connections)이며, 이를 기반으로 동작하는 프로토콜이 바로 HTTP/3이다.
하지만 인터넷 위에 완전히 새로운 전송 프로토콜을 배포하는 일은 생각보다 훨씬 어렵다. 이미 네트워크 곳곳에는 방화벽, NAT 같은 수많은 미들박스가 존재하고, 이들은 일반적으로 TCP와 UDP만을 정상적인 트래픽으로 인식하도록 굳어져 있다.
새로운 전송 프로토콜을 배포할 수 없다면, TCP자체를 수정하면 되지 않을까? 라는 생각을 할 수 도 있다. 하지만 만약 운영체제 커널에 깊숙이 구현되어 있는 TCP의 로직을 수정하면, 이미 전세계에 배포되어있는 모든 OS를 전부 뜯어 고쳐야한다. 사실상 불가능에 가까울것이다.
그래서 HTTP/3(QUIC)은 전혀 다른 길을 택한다. 최소한의 기능만 제공하는 UDP를 기반으로 깔고, 그 위에서 TCP가 해오던 역할 (재전송, 혼잡 제어, 흐름 제어, 그리고 TLS 기반 암호화까지)을 애플리케이션 레벨에서 다시 구현한다. → 전송계층은 바꿀 수 없으니(OS레벨은 어렵고) App(User 레벨)에서 처리하자.

이 구조가 만들어낸 가장 중요한 변화는, 드디어 전송 계층에서의 HOLB를 끊어냈다는 점이다. QUIC에서는 연결 전체가 아니라 스트림 단위로 독립적인 흐름 제어와 재전송이 이루어진다. 특정 스트림에서 패킷이 유실되더라도, 그 스트림만 잠시 멈출 뿐 다른 스트림들은 영향을 받지 않고 그대로 전달된다. HTTP/2에서 남아 있던 마지막 병목이 여기서 사라진다.
지연 시간 측면에서도 큰 성능개선을 만들어냈다. QUIC은 TLS 1.3을 내부에 통합해, 연결 수립과 암호화 과정을 하나로 합쳤다. 그 결과 최초 연결에서도 1-RTT 만에 안전한 통신을 시작할 수 있고, 이미 한 번 연결했던 서버라면 0-RTT로 첫 요청을 곧바로 전송할 수도 있다.
다만 0-RTT에는 명확한 한계가 있다. 이 방식은 동일한 요청이 재전송되는 Replay Attack에 취약하기 때문에, 서버 상태를 변경하는 요청에는 사용할 수 없다. 실제에선 보통 멱등성이 보장된 GET 요청처럼, 안전한 읽기 작업에만 제한적으로 사용된다.
또 하나 큰 변화는 모바일 환경에서의 경험이다. 기존 TCP는 IP 주소와 포트 조합(4-Tuple)으로 연결을 식별하기 때문에, 사용자의 네트워크가 바뀌는 순간 연결이 끊어질 수밖에 없었다. 와이파이에서 LTE로 전환되는 순간 다운로드가 끊기는 이유가 바로 이것이다. 반면 QUIC은 Connection ID라는 별도의 식별자를 사용한다. IP가 바뀌어도 같은 연결로 인식하기 때문에, 네트워크가 변경되어도 연결을 다시 맺을 필요가 없다. 이동 중에도 스트리밍이나 다운로드가 끊기지 않는 이유가 여기에 있다.

위 이미지에서, HTTP/2는 S1 패킷이 손실되자 S1의 재전송을 기다리느라 아무 문제 없는 S2(CSS), S3(JS) 스트림까지 모두 멈춰버리는 'TCP HOLB' 문제가 발생했다.
반면 HTTP/3는 S1 패킷이 손실되어도 S1만 재전송할 뿐, S2와 S3는 중단 없이 데이터를 전송하여 불안정한 네트워크 환경에서도 데이터 전송 효율을 극대화한다. HTTP/3는 “TCP 위에서 HTTP를 어떻게 더 잘 굴릴 것인가”라는 생각을 버리고 “HTTP에 맞는 전송 계층을 새로 만들자”라는 방향으로의 전환이다.

2. HTTP 메서드와 상태 코드의 의미
프로토콜의 아키텍처가 아무리 빠르고 안정적으로 진화했다 하더라도, 그 위에서 오가는 메시지의 의도(Semantics)가 명확하지 않다면 백엔드와 프론트엔드 간의 분산 시스템은 예측할 수 없는 오류와 유지보수 불능의 상태에 빠지게 된다. HTTP는 시스템 간의 정교한 통신을 위한 언어이며, 개발자는 HTTP 메서드와 상태 코드를 통해 명확한 규약과 의도를 맺어야 한다.
프로토콜이 아무리 빨라져도, 그 위에서 오가는 메시지의 ‘의도’가 불명확하다면 시스템은 쉽게 무너진다. 프론트엔드와 백엔드가 분리된 분산 환경에서는 특히 더 그렇다. 같은 요청을 보내더라도, 서로 다르게 해석하는 순간 수많은 버그와 마주하게 될 것이다.
HTTP는 단순한 데이터 전달 수단이 아니라, 시스템 간 약속된 통신 언어다. 그리고 그 언어의 핵심이 바로 메서드와 상태 코드의 시맨틱스(Semntics)다.
2.1 HTTP 메서드
RESTful API 생태계에서 HTTP 메서드를 단순한 데이터베이스의 CRUD 매핑 도구라고 생각할 수도 있다. 하지만 실제로는 이를 넘어선 깊은 의미론적 특성을 지닌다. 이 시맨틱스를 이해하고 시스템에 적용하는 핵심 개념은 안전성과 멱등성이다.
먼저 안전성은 해당 메서드를 호출해도 서버의 상태나 데이터가 절대 변경되지 않음을 의미하는 읽기 전용 작업의 특성이다. 예를 들어 브라우저의 프리 패치기능이나 구글봇과 같은 웹 크롤러가 서버를 망가뜨릴수 있다는 두려움 없이 수백만 개의 URL을 자유롭게 호출할 수 있는 근거가 바로 GET이나 HEAD 메서드의 안전성 덕분이다.
둘째, 멱등성은 조금 더 실전적인 개념이다. 같은 요청을 한 번 보내든, 네트워크 문제로 여러 번 반복해서 보내든 서버의 최종 상태가 동일하게 유지되는가를 의미한다. 이게 중요한 이유는 분산 시스템의 현실 때문이다. 분산 시스템 환경에서는 네트워크 타임아웃이 빈번하게 발생하며, 클라이언트는 자신이 보낸 요청이 서버에 도달하지 않은 것인지, 아니면 서버가 처리 후 보낸 응답이 유실된 것인지 확신할 수 없다. 이때 멱등성이 보장된 메서드는 중복 실행의 부작용이 없으므로 프론트엔드에서 마음 놓고 재시도 로직을 구동할 수 있다.
이 기준으로 HTTP 메서드를 다시 보면 성격이 명확해진다.
GET,HEAD: 안전하고 멱등적이다. 조회용이며, 캐싱과 자동 재시도에 매우 적합하다.PUT,DELETE: 멱등적이지만 안전하지는 않다. 상태는 바꾸지만, 여러 번 호출해도 결과는 동일하다.POST,PATCH: 일반적으로 멱등성이 보장되지 않는다. 같은 요청이 반복되면 중복 생성이나 예기치 않은 상태 변경이 발생할 수 있다.
이 차이는 단순한 이론이 아니라, 재시도 전략, 캐싱, 그리고 장애 대응 방식까지 직접적으로 영향을 준다.
예를 들어 결제 API를 POST로 설계했다면, 네트워크 재시도 로직 하나만 잘못 들어가도 중복 결제라는 치명적인 문제가 발생할 수 있다. 결국 HTTP 메서드는 단순한 CRUD 매핑이 아니라, “이 요청은 얼마나 안전한가, 얼마나 다시 보내도 괜찮은가”에 대한 시스템 간 약속이다.

2.2 POST, PUT, PATCH는 언제, 어떻게 구분해서 써야 하는가?
이 세 가지 메서드는 겉보기에는 모두 “데이터를 바꾼다”는 공통점을 가지지만, 시맨틱스는 완전히 다르다.
먼저 PUT은 리소스의 ‘전체 교체’를 의미한다.
클라이언트는 대상 리소스의 완전한 스키마를 포함한 상태를 보내야 하며, 서버는 이를 그대로 덮어쓴다. 예를 들어 사용자 정보에 이름, 나이, 연락처가 존재할 때, 클라이언트가 이름과 나이만 담아 PUT 요청을 보낸다면 연락처는 유지되는 것이 아니라 삭제되거나 null로 초기화되는 것이 명세에 더 가깝다.
이러한 특성 덕분에 PUT은 반드시 멱등하게 동작해야 하며, 동일한 요청을 여러 번 보내더라도 서버의 최종 상태는 항상 동일하다. 그래서 네트워크 장애 상황에서 안전한 재시도가 가능한 메서드다.
반면 PATCH는 리소스의 ‘부분 수정’을 위한 메서드다.
전체가 아닌 변경할 필드만 전송하기 때문에 네트워크 효율 측면에서는 유리하다. 다만 중요한 차이는, PATCH는 명세상 멱등성을 보장하지 않는다는 점이다.
단순히 값을 덮어쓰는 형태라면(예: {"name": "Jaeho"}) 결과적으로 멱등하게 동작할 수 있지만, 연산이나 명령을 포함하는 경우 상황은 완전히 달라진다. 배열에 값을 추가하거나, 카운터를 증가시키는 요청이 반복되면 동일 요청임에도 결과는 계속 누적된다. 이 경우 재시도는 곧 데이터 오염으로 이어진다.
POST는 가장 범용적인 메서드다.
주로 서버가 식별자를 생성하는 리소스 생성, 또는 다른 메서드로 명확하게 표현하기 어려운 작업에 사용된다. 하지만 POST는 기본적으로 멱등하지 않기 때문에, 같은 요청이 반복되면 중복 생성이나 중복 처리가 발생한다. 대표적으로 주문 생성이나 결제 요청이 여기에 해당한다.
그래서 POST를 사용할 때 특히 주의가 필요하다. 사용자의 중복 클릭이나 네트워크 재시도로 동일 요청이 여러 번 전송될 수 있기 때문에, 프론트엔드에서는 디바운싱, 스로틀링, 버튼 비활성화 같은 방어 로직을 반드시 고려해야 한다.
2.2 상태 코드의 체계적 이해
HTTP 상태 코드는 서버가 요청을 어떻게 처리했는지를 가장 빠르고 명확하게 전달하는 신호다. 상태코드를 신경쓰지 않는다면, 개발자는 에러를 디버깅함에 있어 큰 어려움이 생길것이다. 팀플을 하다보면 뜻하지 않게자주 보이는 대표적인 안티 패턴이 있다.
에러가 발생했음에도 불구하고 항상 200 OK를 반환하고, 응답 본문에 {"error": true} 같은 자체 규약을 담는 방식이다. 브라우저나 CDN이 해당 응답을 정상으로 인식해 캐싱해버리는 캐시 포이즈닝을 유발할 수 있고, fetch나 axios 인터셉터 역시 이를 성공 응답으로 처리하게 된다. 결국 에러는 숨겨지고, 디버깅은 훨씬 어려워진다.
상태코드에 대한 모든 정리를 하기보단 대표적인 특징들과 자주 만나게 되는 코드들을 위주로 정리했고, 모든 상태코드와 각각의 특성이 궁금하다면 글을 작성할 때 많이 참고한 링크를 가져와봤다. https://inpa.tistory.com/entry/HTTP-🌐-상태-코드-1XX-5XX-총정리판-📖

성공과 우회의 대응 (2xx, 3xx)
2xx는 “성공”이라는 공통점을 가지지만, 그 결과의 의미는 꽤 다르다. 일반적인 조회나 수정 성공은 200 OK로 충분하다. 하지만 POST로 새로운 리소스를 생성했다면, 단순 성공이 아니라 생성이 완료되었다는 사실 자체가 중요하다. 이때는 201 Created를 사용하고, 보통 생성된 리소스의 URI를 Location 헤더로 함께 전달한다. 204 No Content는 종종 간과되지만 꽤 유용하다. 삭제 요청이 정상적으로 처리되었거나, UI를 바꿀 필요 없는 백그라운드 작업이 성공했을 때 “보낼 데이터는 없지만 작업은 끝났다”는 의도를 명확하게 표현할 수 있다.
3xx는 “성공했지만, 다른 곳을 보라”는 신호다. 과거 301, 302는 리다이렉션 과정에서 HTTP 메서드를 GET으로 바꿔버리는 문제가 있었다. 이 때문에 POST 요청의 바디가 유실되는 일이 발생했다. 이를 해결하기 위해 등장한 것이 307 Temporary Redirect와 308 Permanent Redirect다. 이들은 원래의 메서드와 바디를 그대로 유지한 채 이동한다. 그리고 304 Not Modified는 캐시 최적화의 핵심이다. 서버가 “이미 가지고 있는 데이터 그대로 써도 된다”고 확인해주는 순간, 클라이언트는 네트워크 전송 없이 즉시 응답을 완료한다.
에러의 책임은? Client vs Server (4xx, 5xx)
4xx는 문제의 원인이 클라이언트에 있음을 의미한다. 문법 오류나 필수 값 누락 같은 기본적인 문제는 400 Bad Request로 표현할 수 있다. 실제로 가장 자주 마주치는 상태 코드는 아래 세 가지이다.
401 Unauthorized는 인증이 필요하지만, 아직 인증되지 않은 상태를 의미한다. 보통 로그인이 되어 있지 않거나, 토큰이 없거나 만료된 경우에 발생한다.403 Forbidden은 인증은 되었지만 권한이 없는 경우다. 로그인은 했지만 접근할 수 없는 리소스에 접근하려 할 때 반환된다.404 Not Found는 요청한 리소스 자체가 존재하지 않는 경우다. 잘못된 URL이거나, 이미 삭제된 리소스를 조회할 때 발생한다.
이 세 코드는 사용자 경험과도 직결되기 때문에, 프론트엔드에서 명확하게 구분해 처리하는 것이 중요하다. 단순히 “에러 발생”으로 묶어버리면 UX가 크게 저하된다. 한편, 평소에는 잘 드러나지 않지만 반드시 이해하고 있어야 하는 상태 코드도 있다. API Gateway, CDN, WAF, 혹은 프론트 단의 사전 검증 로직에 의해 실제로는 자주 노출되지 않을 뿐이다.
409 Conflict는 동시성 문제가 발생했을 때 사용된다. 이미 존재하는 데이터로 생성 요청을 하거나, 동일 자원을 동시에 수정하려 할 때 대표적으로 등장한다. 이 경우 단순 에러 처리로 끝내는 것이 아니라, 사용자에게 충돌 상황을 알리고 재시도를 유도하는 UX가 필요하다. 또한 429 Too Many Requests는 서버가 보내는 명확한 속도 제한 신호다. “지금은 요청이 너무 많으니 잠시 멈춰라”는 의미이며, 클라이언트는 이에 맞춰 요청 간격을 조절하거나 재시도를 지연해야 한다.
5xx는 반대로, 클라이언트의 요청은 정상이지만 서버가 이를 처리하지 못한 상황이다. 500 Internal Server Error는 가장 일반적인 서버 측 예외다. 반면 502 Bad Gateway나 504 Gateway Timeout은 MSA 환경에서 특히 자주 보인다. 앞단의 리버스 프록시(Nginx 등)나 로드 밸런서가 뒷단의 애플리케이션 서버(WAS)로부터 유효한 응답을 제때 받지 못했거나 지연되었을 때 발생한다. 프론트엔드는 이 코드를 통해 백엔드 인프라의 일시적 병목 현상을 인지하고, 즉각적인 재요청보다는 점진적으로 요청 간격을 늘려가는 지수 백오프 기반의 지연 재시도 로직을 설계해야 시스템의 연쇄적인 붕괴를 막을 수 있다.
아래 이미지는 Mohith Gupta님의 글을 읽다가 HTTP 상태 코드를 화장실로 재밌게 비유한 짤이 있어서 넣어보았다.

<reference>
image made by claude, gemini
https://calendar.perfplanet.com/2020/head-of-line-blocking-in-quic-and-http-3-the-details/
https://velog.io/@yesbb/HTTP3까지-버전별-변천사
https://code-lab1.tistory.com/34
https://bbo-blog.tistory.com/87