HTTP 성능: keep-alive·gzip·HTTP/2·CORS 실측

연결 재사용과 매번 연결, gzip 레벨, HTTP/1.1 과 HTTP/2 를 curl·Python·php -S 로 실측. curl 반복문 함정과 CORS 프리플라이트에서 서버가 실제로 받는 것까지 확인.

시리즈: HTTP 강좌 6/6 (고급). 요청, 캐시, TLS 까지 익힌 분이 사이트와 API 를 빠르게 만들고 다른 오리진에서도 부를 수 있게 하는 마지막 회차입니다. 다 읽으면 연결 재사용을 직접 재고, 언제 얼마나 압축할지 정하고, HTTP/2 로 바뀌는 것과 바뀌지 않는 것을 설명하고, 무엇을 막아 주는지 정확히 알고 CORS 프리플라이트 응답을 설정할 수 있습니다.

이 글은 "회사 와이파이에서는 빠른데 LTE·5G 에서는 느리다"는 말을 들은 분, 배치 스크립트가 API 를 몇백 번 부르는 데만 몇 분씩 걸리는 분, 그리고 프론트엔드 동료에게서 "콘솔에 blocked by CORS policy 가 떠요"라는 캡처를 메신저로 받은 분을 위한 글입니다. 세 가지 모두 여기서 재 볼 몇 가지 전송 계층 선택으로 결정됩니다. 모두 Apple Silicon Mac(M1 Pro)에서 curl 8.7.1(nghttp2 로 HTTP/2 지원, HTTP/3 미지원), Python 3.9.6, PHP 8.5.6(php -S), zlib 1.2.12 로 127.0.0.1 에서 실행했고, example.com 과 이 사이트에 몇 번 읽기 전용 요청을 보냈습니다. 브라우저 동작은 설명만 하고 실행하지 않았습니다.

keep-alive: 연결 하나로 여러 요청

HTTP/1.1 에서는 어느 한쪽이 Connection: close 라고 하지 않는 한 응답 뒤에도 연결이 열려 있습니다. 이것이 keep-alive(지속 연결)이고, 다음 요청은 TCP 연결과 TLS 핸드셰이크를 건너뛸 수 있습니다. curl -v 로 볼 수 있습니다. 강좌 5의 keep-alive 서버(tls_server.py 를 인증서 없이, 포트 18171)에 보냈습니다.

$ curl -sv -o /dev/null -o /dev/null http://127.0.0.1:18171/a http://127.0.0.1:18171/b
*   Trying 127.0.0.1:18171...
* Connected to 127.0.0.1 (127.0.0.1) port 18171
> GET /a HTTP/1.1
< HTTP/1.1 200 OK
* Connection #0 to host 127.0.0.1 left intact
* Re-using existing connection with host 127.0.0.1
> GET /b HTTP/1.1
< HTTP/1.1 200 OK

-H 'Connection: close' 를 붙이면 두 번째 요청은 새 연결을 엽니다.

* Connection 0 seems to be dead
* Closing connection
*   Trying 127.0.0.1:18171...
* Connected to 127.0.0.1 (127.0.0.1) port 18171
> GET /b HTTP/1.1

모든 서버가 연결을 유지하지는 않습니다. PHP 내장 서버는 강좌 4에서도 이번에도 매 요청에 Connection: close 로 답하고 요청마다 Accepted / Closing 을 로그에 남겼습니다. 이 효과를 보여 주는 데는 쓸 수 없습니다.

실측: 재사용 대 매번 연결

루프백, curl 프로세스 하나로 200건(URL 글롭 r[1-200]). 3세트 중 1세트, 모두 같은 파이썬 서버, curl 을 한 번 띄우는 시간을 포함한 합계입니다.

방식 새 연결 수 합계
평문 HTTP, 재사용 1 75 ms
평문 HTTP, Connection: close 200 87 ms
HTTPS, 재사용 1 85 ms
HTTPS, Connection: close 200 366 ms
평문 HTTP, curl 프로세스 200번(셸 반복문) 200 1,976 ms

나머지 세트도 같은 경향이었습니다(HTTP 83/68 대 86/80 ms, HTTPS 81/77 대 361/356 ms, 프로세스 200번 1,980·2,060 ms 대 프로세스 하나 79·74 ms). 루프백에서 TCP 연결은 거의 공짜이고, TLS 핸드셰이크는 CPU 로 약 1.4 ms, 프로세스 시작은 약 10 ms 입니다.

인터넷 너머로 curl 프로세스 하나로 example.com 에 다섯 번 보냈습니다.

방식 1번째 2~5번째 합계
재사용(HTTP/2) 104 ms 22, 23, 21, 18 ms 0.19초
Connection: close(HTTP/1.1) 95 ms 120, 81, 76, 79 ms 0.45초

새 연결은 매번 DNS·TCP·TLS 를 다시 치릅니다. 강좌 5의 중앙값(2.6 + 13.5 + 20.7 ms)으로 약 37 ms 이고, 휴대폰 회선이라면 더 걸립니다.

루프백에서 200건을 보낸 합계 시간 막대그래프. 평문 HTTP 연결 하나 재사용 75 ms, HTTPS 재사용 85 ms, 평문 HTTP 매번 새 연결 87 ms, HTTPS 매번 새 연결 366 ms, 평문 HTTP 매번 curl 프로세스 새로 실행 1,976 ms.
로컬 파이썬 서버에 보낸 200건의 합계 시간(3회 중 1회차, Apple M1 Pro, curl 8.7.1). 연결 재사용이 가장 싸고, 요청마다 curl 을 새로 띄우는 것이 압도적으로 비싸다.

흔한 실수: curl 셸 반복문, 그리고 URL 하나에만 듣는 -o

흔히 하는 일: for i in …; do curl …; done 으로 API 를 200번 부른다. 실제로 일어나는 일: 반복마다 새 프로세스가 떠서 아무것도 재사용하지 못합니다. 루프백에서 1,976 ms 대 75 ms 였고, 인터넷 너머라면 매번 TLS 핸드셰이크까지 통째로 붙습니다. 해결: curl 하나에 URL 을 여러 개 넘기거나(r[1-200] 같은 글롭, URL 목록을 담은 --config, --parallel), 프로그램에서는 HTTP 클라이언트 객체를 하나만 씁니다.

측정하다가 함정을 하나 더 밟았습니다. curl -s -o /dev/null -w … URL URL URL URL URL 이라고 썼더니 터미널이 HTML 로 가득 찼습니다. -o 는 URL 하나에만 적용되어 나머지 네 개의 본문은 표준 출력으로 나간 것입니다. URL 마다 -o 를 쓰거나 --output-dir 과 -O 를 쓰세요(글롭 URL 은 인자 하나라서 -o 하나로 전체에 적용됩니다).

압축: gzip 실측

압축은 텍스트를 보내기 전에 줄이는 것입니다. 클라이언트는 Accept-Encoding: gzip, br, … 로 풀 수 있는 형식을 알리고, 서버는 Content-Encoding: gzip 으로 답하면서 캐시가 두 버전을 섞지 않도록 Vary: Accept-Encoding 을 붙입니다. 파이썬 gzip 모듈로 입력 세 가지를 각각 50번 압축해 중앙값을 냈습니다.

import gzip, random, time, statistics
random.seed(7)
data = {
    "products.json (179,500 B)": open("products.json", "rb").read(),
    "tiny JSON": b'{"ok":true,"id":42}',
    "random bytes (like JPEG)": random.randbytes(100_000),
}
REP = 50
for name, raw in data.items():
    print(f"{name}: raw={len(raw)}")
    for lvl in (1, 6, 9):
        ts = []
        for _ in range(REP):
            t = time.perf_counter(); out = gzip.compress(raw, compresslevel=lvl, mtime=0); ts.append(time.perf_counter() - t)
        td = []
        for _ in range(REP):
            t = time.perf_counter(); gzip.decompress(out); td.append(time.perf_counter() - t)
        print(f"  level {lvl}: {len(out):>7} B ({len(out)/len(raw)*100:5.1f}%)  "
              f"compress {statistics.median(ts)*1000:.3f} ms  decompress {statistics.median(td)*1000:.3f} ms")
products.json (179,500 B): raw=179500
  level 1:   26052 B ( 14.5%)  compress 0.505 ms  decompress 0.176 ms
  level 6:   19711 B ( 11.0%)  compress 1.401 ms  decompress 0.139 ms
  level 9:   18758 B ( 10.5%)  compress 7.087 ms  decompress 0.142 ms
tiny JSON: raw=19
  level 1:      39 B (205.3%)  compress 0.008 ms  decompress 0.011 ms
  …
random bytes (like JPEG): raw=100000
  level 1:  100053 B (100.1%)  compress 1.624 ms  decompress 0.040 ms
  …

배울 점은 세 가지입니다. 레벨 9는 레벨 6보다 다섯 배 오래 걸리고 바이트는 5% 만 줄었습니다. 압축 풀기는 어느 레벨이든 쌉니다. 그리고 압축은 작은 응답(19 → 39)과 이미 압축된 데이터(무작위 바이트는 JPEG·PNG·MP4·ZIP 대역)를 키웁니다. 그래서 서버는 보통 약 1 KB 미만 응답과 바이너리 미디어는 압축하지 않습니다. 두 번째 실행에서도 크기는 같았고 시간 차이는 0.07 ms 이내였습니다.

크기 막대그래프. 원본 JSON 은 179,500바이트. gzip 레벨 1은 26,052바이트, 레벨 6은 19,711바이트, 레벨 9는 18,758바이트.
179,500바이트 products.json 을 gzip 레벨 1·6·9 로 압축한 뒤의 크기(Python gzip, zlib 1.2.12). 압축 시간 중앙값은 0.5·1.4·7.1 ms.

이 사이트의 메인 페이지는 --compressed 없이 8,912바이트, 붙이면 3,450바이트였습니다(요청 두 번).

php -S 로 gzip 보내기, 그리고 날 바이트 함정

요청받을 때만 압축하고 CORS 에도 답하는(다음 절) 작은 API 입니다. products.json 옆에 api.php 로 저장하고 php -S 127.0.0.1:18170 api.php 로 실행합니다.

<?php
// php -S 127.0.0.1:18170 api.php
$path   = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? null;
$allowed = ['https://app.example'];

if ($path !== '/api/products') { http_response_code(404); exit; }

// CORS: only for allowed origins, and tell caches the answer depends on Origin
header('Vary: Origin, Accept-Encoding');
if ($origin !== null && in_array($origin, $allowed, true)) {
    header("Access-Control-Allow-Origin: $origin");
}
if ($method === 'OPTIONS') {                      // preflight
    header('Access-Control-Allow-Methods: GET, POST, PUT');
    header('Access-Control-Allow-Headers: Content-Type, Authorization');
    header('Access-Control-Max-Age: 600');
    http_response_code(204);
    exit;
}
error_log("$method $path origin=" . ($origin ?? '-'));   // the server DID run

$body = file_get_contents(__DIR__ . '/products.json');
header('Content-Type: application/json');
if (str_contains($_SERVER['HTTP_ACCEPT_ENCODING'] ?? '', 'gzip')) {
    $body = gzencode($body, 6);
    header('Content-Encoding: gzip');
}
header('Content-Length: ' . strlen($body));
echo $body;
$ curl -s -o /dev/null -w 'plain:      %{http_code} %{size_download} B\n' http://127.0.0.1:18170/api/products
plain:      200 179500 B
$ curl -s --compressed -o /dev/null -w 'compressed: %{http_code} %{size_download} B on the wire\n' http://127.0.0.1:18170/api/products
compressed: 200 19730 B on the wire
$ curl -s --compressed http://127.0.0.1:18170/api/products | wc -c
  179500
$ curl -s -H 'Accept-Encoding: gzip' http://127.0.0.1:18170/api/products | head -c 20 | xxd
00000000: 1f8b 0800 0000 0000 0013 addd ddae 75bd  ..............u.
00000010: 719e e97d                                q..}

마지막 명령이 함정입니다. 헤더를 손으로 붙이면 서버에는 "gzip 을 풀 수 있다"고 전해지지만 curl 이 풀어 주지는 않아서, 날 gzip(매직 바이트 1f 8b 로 시작)이 그대로 나옵니다. 헤더 전송과 압축 해제를 둘 다 하는 --compressed 를 쓰세요. 앱의 HTTP 라이브러리 중에도 "gzip 요청"과 "gzip 해제"가 따로인 것이 있습니다.

정말 빨라졌을까요? 각 20번의 중앙값입니다.

회선 비압축(179,500 B) gzip(19,730 B)
루프백 0.71 ms 2.24 ms
curl --limit-rate 1M 124.88 ms(83~164) 2.30 ms

루프백에서는 압축 때문에 오히려 느려졌습니다. 옮기는 데 비용이 안 드는 바이트를 위해 CPU 를 1.5 ms 썼기 때문입니다. 느린 회선에서는 압도적으로 이깁니다. 다만 솔직히 말하면 curl 의 속도 제한이 19.7 KB 응답을 한 번에 통과시켜서, 1 MB/s 의 gzip 줄은 너무 낙관적입니다. 정말 1 MB/s 라면 약 19 ms 가 걸려야 합니다. 비압축 쪽도 제한이 부정확해서, 같은 179,500바이트가 강좌 4의 구성에서는 중앙값 173 ms 였습니다. 정적 파일은 요청마다가 아니라 배포할 때 한 번 압축해 두는 것이 가장 좋습니다.

HTTP/1.1, HTTP/2, HTTP/3

HTTP/2 는 HTTP 의 의미(메서드, 상태 코드, 헤더)는 그대로 두고 운반 방식을 바꿨습니다. 바이너리 프레임, 연결 하나 위에서 여러 요청을 동시에 흘리는 멀티플렉싱, 헤더 압축(HPACK). 강좌 5에서 본 것처럼 TLS 핸드셰이크 안의 ALPN 으로 정해집니다(ALPN: server accepted h2). example.com 에 한 번씩 보냈습니다.

--http1.1 -> HTTP/1.1 header_bytes=347 total=0.097737
--http2 -> HTTP/2 header_bytes=291 total=0.069453

같은 응답 헤더가 HTTP/2 에서는 56바이트 적었습니다. 한 번씩 잰 것이라 속도에 대해서는 아무 말도 할 수 없습니다. 서버가 HTTP/1.1 만 말할 때 curl 의 동작입니다.

$ curl -sv --http2 http://127.0.0.1:18162/      # 평문 HTTP: curl 이 업그레이드를 요청
> GET / HTTP/1.1
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAAQAoAAAAAIAAAAA
< HTTP/1.1 200 OK
$ curl -sS --http2-prior-knowledge http://127.0.0.1:18162/
curl: (16) Remote peer returned unexpected data while we expected SETTINGS frame.  Perhaps, peer does not support HTTP/2 properly.
$ curl -sS --http3 https://example.com/ -o /dev/null
curl: option --http3: the installed libcurl version doesn't support this

파이썬 서버는 업그레이드를 무시하고 HTTP/1.1 로 답했고, 이는 명세상 허용됩니다. --http2-prior-knowledge 는 협상을 건너뛰므로 종료 코드 16 으로 실패합니다. HTTP/3 은 UDP 위의 QUIC 으로 동작합니다. example.com 과 이 사이트 모두 alt-svc: h3=":443" 으로 지원을 알리지만, 이 curl 빌드로는 쓸 수 없어서 시험하지 않았습니다.

실무에서 HTTP/2 가 바꾸는 것(브라우저 동작이며 여기서 실행하지 않음): 브라우저는 HTTP/1.1 에서 호스트당 최대 6개 연결을 열고 나머지는 줄을 세웠습니다. 예전 사이트들이 파일을 여러 도메인으로 나누고 뭐든 하나로 묶었던 이유입니다. HTTP/2 에서는 연결 하나로 다 나르므로 도메인 분산은 오히려 손해이고 묶을 필요도 줄었습니다. 다만 TCP 의 헤드 오브 라인 블로킹은 남습니다. 패킷 하나가 사라지면 그 연결의 모든 스트림이 멈춥니다. 이를 해결하는 것이 HTTP/3 입니다.

php -S 로 CORS 프리플라이트 보기

https://app.example 페이지가 https://api.example 에 PUT 이나 JSON Content-Type 으로 보내려 하면, 브라우저는 먼저 프리플라이트, 즉 허락을 구하는 OPTIONS 요청을 보냅니다. 같은 요청을 curl 로 손수 보낼 수 있습니다.

$ curl -si -X OPTIONS -H 'Origin: https://app.example' \
    -H 'Access-Control-Request-Method: PUT' \
    -H 'Access-Control-Request-Headers: content-type' http://127.0.0.1:18170/api/products
HTTP/1.1 204 No Content
Host: 127.0.0.1:18170
Date: Thu, 12 Nov 2026 14:31:48 GMT
Connection: close
X-Powered-By: PHP/8.5.6
Vary: Origin, Accept-Encoding
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
Content-type: text/html; charset=UTF-8

브라우저는 이 헤더들을 보내려는 내용과 비교하고, 답을 최대 Max-Age 초 동안 캐시한 뒤에야 진짜 PUT 을 보냅니다. 이번에는 허용되지 않은 오리진에서 온 요청입니다.

$ curl -si -H 'Origin: https://evil.example' http://127.0.0.1:18170/api/products | head -8
HTTP/1.1 200 OK
…
Vary: Origin, Accept-Encoding
Content-Type: application/json
Content-Length: 179500

서버 로그는 이렇습니다.

[Thu Nov 12 14:31:48 2026] GET /api/products origin=https://evil.example

요청은 실행되었고 본문도 다 나갔습니다. 차이는 Access-Control-Allow-Origin 이 없다는 것 하나뿐이고, 그 때문에 브라우저가 페이지의 자바스크립트에게서 응답을 숨깁니다. curl, 스크립트, 앱은 신경 쓰지 않습니다. CORS 에 관해 가장 중요한 사실이 이것입니다.

흐름도. https://app.example 페이지가 API 에 JSON 본문이 담긴 PUT 을 보내려 한다. 브라우저는 먼저 Origin 과 Access-Control-Request-Method 를 붙인 OPTIONS 를 보낸다. 서버는 Access-Control-Allow-Origin, Allow-Methods, Allow-Headers, Max-Age 가 담긴 204 로 답한다. 브라우저는 자기 오리진과 메서드가 허용되었는지 확인하고, 허용되면 진짜 PUT 을 보낸다. 허용되지 않으면 PUT 은 전송되지 않는다. 단순 GET 에는 프리플라이트가 없어서 허용되지 않은 오리진의 GET 도 서버에 도착해 실행되었음을 로그로 확인했고, 브라우저는 페이지에서 응답을 숨길 뿐이다.
CORS 프리플라이트. 서버 쪽은 php -S 와 curl 로 실행했고, 브라우저 쪽 판정은 Fetch Standard 를 바탕으로 한 설명이며 실행하지 않았다. 허용되지 않은 오리진의 요청도 서버에는 도착한다.

CORS 의 흔한 실수:

  • Access-Control-Allow-Origin: * 와 쿠키나 Authorization 을 함께 쓰기. 브라우저는 와일드카드에서는 자격 증명이 담긴 요청을 거부합니다. 허용할 오리진 하나를 돌려줍니다.
  • 들어온 Origin 을 무엇이든 그대로 돌려주기. 모든 사이트가 로그인된 응답을 읽을 수 있게 됩니다. 위처럼 허용 목록을 씁니다.
  • Origin 에 따라 응답을 바꾸면서 Vary: Origin 을 빼기. CDN 이 한 오리진용 헤더를 다른 오리진에 내줄 수 있습니다.
  • 프리플라이트로 폼의 POST 를 막을 수 있다고 믿기. 단순 요청(폼 전송)에는 프리플라이트가 없습니다. CSRF 토큰을 쓰세요. PHP 보안: XSS·CSRF·세션 을 참고하세요.

판단표

상황 할 일 이유(이번 숫자)
스크립트가 같은 API 를 여러 번 호출 클라이언트 하나 / curl 한 번에 URL 여러 개 프로세스 200개 1,976 ms 대 75 ms
작은 호출이 많은 HTTPS API 연결 유지, HTTP/2 사용 새 TLS 연결 76~120 ms 대 재사용 18~23 ms
1 KB 넘는 JSON/HTML/CSS/JS gzip 레벨 6(또는 배포 때 미리 압축) 179,500 → 19,711 B, 1.4 ms
이미지·동영상·ZIP·작은 응답 압축 안 함 100,000 → 100,053 B, 19 → 39 B
다른 오리진의 페이지가 API 호출 허용 목록 기반 프리플라이트 응답 + Vary: Origin CORS 는 페이지가 읽을 수 있는 것만 정함
원치 않는 호출자를 막아야 함 인증, CSRF 토큰, 속도 제한 허용 안 된 오리진의 GET 도 서버에서 실행됨

정직한 한계와 버전 메모

  • 실행한 것: curl 8.7.1(macOS 기본, LibreSSL 3.3.6, nghttp2 1.68.1, zlib 1.2.12), Python 3.9.6, PHP 8.5.6 내장 서버, Apple M1 Pro. 루프백 시간에는 curl 을 한 번 띄우는 시간이 들어 있습니다. --limit-rate 는 대역폭만 흉내 냅니다.
  • 실행하지 않은 것: 브라우저 전부(CORS 판정, 프리플라이트 캐시, 연결 6개 제한, HTTP/2 우선순위), HTTP/3, Brotli(br, RFC 7932)와 zstd 압축, 서버 쪽 압축 모듈(Apache/LiteSpeed/nginx).
  • 명세: HTTP/1.1 메시지 형식은 RFC 9112, HTTP 의미론은 RFC 9110(둘 다 2022년 6월). HTTP/2 는 RFC 9113(2022년, 2015년의 RFC 7540 대체), HTTP/3 는 RFC 9114(2022년), gzip 은 RFC 1952 입니다. CORS 는 RFC 가 아니라 WHATWG 의 Fetch Standard 에 정의되어 있습니다.
  • 원격 측정은 한 네트워크에서 하루에 다섯 번씩 보낸 것뿐입니다. 경향을 보여 줄 뿐 벤치마크가 아닙니다.

연습문제

1. 야간 스크립트가 ID 3,000개에 대해 for id in $(cat ids.txt); do curl -s "https://api.example/items/$id" -o "out/$id.json"; done 을 실행합니다. curl 이 연결을 재사용하도록 고치고, 이번 측정 중 어느 것이 효과를 예측해 주는지 답하세요.

2. API 가 Access-Control-Allow-Origin: * 를 보내는데, 프론트엔드가 이제 세션 쿠키를 보내야 합니다. 브라우저 콘솔에 CORS 오류가 납니다. 응답 헤더를 어떻게 바꿔야 할까요?

정답

1. 줄마다 URL 과 출력 파일을 적은 설정 파일을 만들고 curl 을 한 번만 실행합니다.

while read id; do
  printf 'url = "https://api.example/items/%s"\noutput = "out/%s.json"\n' "$id" "$id"
done < ids.txt > urls.cfg
curl -s --config urls.cfg

로컬 서버와 ID 다섯 개로 이 형태를 확인했더니 -w '%{num_connects} ' 가 1 0 0 0 0 을 출력했습니다. 다섯 건 모두 연결 하나였습니다. 프로세스가 하나면 연결을 재사용합니다(--parallel 을 붙이면 HTTP/2 로 여러 건을 동시에 보낼 수도 있습니다). 프로세스 몫의 효과는 루프백 측정(200건에 1,976 ms 대 75 ms)이, 연결 몫의 효과는 example.com 측정(건당 76~120 ms 대 18~23 ms)이 예측해 줍니다.

2. 허용할 특정 오리진을 돌려주고 자격 증명을 허용합니다. Access-Control-Allow-Origin: https://app.example, Access-Control-Allow-Credentials: true, Vary: Origin 입니다. 자격 증명이 있으면 와일드카드는 받아들여지지 않습니다. 쿠키 자체도 Secure 여야 하고, 크로스 사이트 요청이라면 SameSite=None 이 필요합니다(강좌 3 참고).

다음 강좌

자주 묻는 질문

keep-alive 는 요청마다 새로 연결하는 것보다 얼마나 빠른가요?

새 연결이 얼마나 비싼지에 달려 있습니다. 루프백에서 평문 HTTP 200건은 재사용 75~83 ms, 매번 연결 80~87 ms 였고, HTTPS 는 77~85 ms 대 356~366 ms 였습니다. 인터넷 너머 example.com 에서는 재사용한 두 번째 이후 요청이 18~23 ms, 새 연결은 건마다 76~120 ms 였습니다. 가장 느린 것은 요청마다 curl 을 새로 띄우는 방식으로, 200건에 약 2초가 걸렸습니다.

gzip 압축 레벨은 몇으로 해야 하나요?

흔한 기본값인 레벨 6이 균형이 좋습니다. 179,500바이트 JSON 에서 레벨 1은 26,052바이트(0.5 ms), 레벨 6은 19,711바이트(1.4 ms), 레벨 9는 18,758바이트(7.1 ms)였습니다. 작은 응답과 이미 압축된 파일은 압축하지 마세요. 19바이트 JSON 은 39바이트로, 무작위 10만 바이트는 100,053바이트로 늘었습니다.

CORS 가 다른 사이트로부터 API 를 지켜 주나요?

아닙니다. CORS 는 다른 오리진의 페이지가 응답을 읽어도 되는지를 브라우저에 알려 주는 장치입니다. 요청 자체는 서버에 도착해 실행됩니다. php -S 로그에 https://evil.example 의 GET 이 찍혔고, Access-Control-Allow-Origin 만 빠진 채 200 본문이 통째로 나갔습니다. API 보호는 인증과 CSRF 대책으로 합니다.

  • #HTTP
  • #고급
  • #성능
  • #keep-alive
  • #gzip
  • #HTTP/2
  • #CORS
  • #curl

t.laboratory

직접 만들고 운영하는 앱·서버·서비스에서 겪은 문제와, 그 해결에 쓴 코드를 기록하는 기술 블로그입니다.

운영자 소개 · 앱

↑