콘텐츠로 이동

curl verbose와 timing은 어디부터 읽어야 할까요?

브라우저에서는 그냥 "느려요"로 보이는데, 터미널에서는 요청이 지나간 체크포인트가 꽤 많이 보여요.

End-to-End Request Debugging에서는 느린 요청 하나를 DNS, 연결, TLS, 프록시, 캐시, 오리진 같은 체크포인트로 나눠 읽는 큰 그림을 봤어요. 그리고 앞의 HTTP 글들에서는 HTTP/1.1, HTTP/2, HTTP/3가 요청과 응답을 어떤 모양으로 실어 나르는지도 열어봤죠.

근데요, 실제로 문제를 만나면 이런 순간이 와요.

"브라우저 waterfall을 열기 전에, 터미널에서 이 요청이 어디서 느린지 빠르게 볼 수 없을까요?"

이럴 때 가장 자주 꺼내는 도구 중 하나가 curl이에요. curl -v는 요청이 지나가는 장면을 글로 보여주고, --write-out은 전송이 끝난 뒤 시간 값을 숫자로 뽑아줘요. curl 공식 문서에서도 --verbose는 내부 동작을 보기 위한 디버깅 정보, --write-out은 전송 완료 뒤 변수 값을 출력하는 기능으로 설명해요. 자세한 변수 이름은 curl man pageeverything curl의 write-out 문서를 기준으로 볼게요.

이 글의 범위

여기서는 curl을 부하 테스트 도구처럼 쓰는 법이 아니라, 요청 하나를 어디서 읽을지에 집중해요. DNS, TCP, TLS, HTTP 버전, 응답 코드, 첫 바이트, 전체 시간을 나눠 보는 감각이 목표예요.


먼저 몸통은 버리고 신호만 보게 만들어요

처음 curl https://example.com/을 치면 HTML 본문이 주르륵 나와요. 그런데 디버깅에서는 본문보다 연결과 시간 신호가 먼저 필요할 때가 많아요.

그래서 기본 모양은 이렇게 잡으면 좋아요.

curl -sS -o /dev/null \
  -w 'http_version=%{http_version}
response_code=%{response_code}
remote_ip=%{remote_ip}
time_namelookup=%{time_namelookup}
time_connect=%{time_connect}
time_appconnect=%{time_appconnect}
time_pretransfer=%{time_pretransfer}
time_starttransfer=%{time_starttransfer}
time_total=%{time_total}
' \
  https://example.com/

각 옵션은 이런 역할이에요.

옵션 처음엔 이렇게 읽으면 돼요
-sS 진행 막대는 숨기되, 에러 메시지는 보여줘요
-o /dev/null 응답 본문은 버려요
-w / --write-out 전송이 끝난 뒤 보고 싶은 값을 출력해요
%{response_code} 마지막 응답 코드
%{http_version} 실제로 사용된 HTTP 버전
%{remote_ip} 실제 연결한 원격 IP
%{time_*} 요청 흐름의 누적 시간 값

중요한 건 time_* 값이 대부분 구간별 시간이 아니라 시작부터 그 지점까지의 누적 시간이라는 점이에요. 그래서 숫자를 그대로 더하면 안 되고, 필요하면 서로 빼서 구간을 읽어야 해요.

실제 출력은 이렇게 생겨요

아래는 이 작업 환경에서 2026년 6월 17일에 https://example.com/으로 한 번 실행해 본 예시예요. 인터넷 경로와 서버 상태에 따라 값은 매번 달라져요.

http_version=2
response_code=200
remote_ip=172.66.147.243
time_namelookup=0.084679
time_connect=0.117370
time_appconnect=0.163551
time_pretransfer=0.163677
time_starttransfer=0.201843
time_total=0.201888

이 출력만 보면 일단 이런 말을 할 수 있어요.

  • 이름을 주소로 바꾸는 데 약 0.085s까지 걸렸어요.
  • TCP 연결 완료는 약 0.117s 시점이에요.
  • TLS handshake 완료는 약 0.164s 시점이에요.
  • 첫 바이트는 약 0.202s 시점에 왔어요.
  • 전체 다운로드도 거의 같은 시점에 끝났으니, 본문 다운로드는 작았어요.
flowchart LR
    A[요청 시작] --> B[DNS 완료<br/><small>time_namelookup</small>]
    B --> C[TCP 연결 완료<br/><small>time_connect</small>]
    C --> D[TLS 완료<br/><small>time_appconnect</small>]
    D --> E[전송 직전 준비 완료<br/><small>time_pretransfer</small>]
    E --> F[첫 바이트 도착<br/><small>time_starttransfer</small>]
    F --> G[전체 완료<br/><small>time_total</small>]

이 그림처럼 curl timing은 요청이 지나간 체크포인트를 숫자로 찍어준다고 보면 돼요. 한 줄짜리 평균 시간이 아니라, 어느 지점까지 도달하는 데 얼마나 걸렸는지를 보여주는 표식이에요.


비유로 보면, 택배 추적 시간표에 가까워요

택배가 늦을 때 "배송이 느려요"라고만 하면 원인을 알기 어렵죠.

  • 주문 접수까지 늦었는지
  • 물류센터 입고가 늦었는지
  • 간선 이동이 늦었는지
  • 집 근처 도착 뒤 배송이 늦었는지

이걸 시간표로 보면 훨씬 빨리 좁힐 수 있어요. curl --write-out도 비슷해요.

택배 추적에서는 curl timing에서는
주문 번호로 배송지를 찾음 time_namelookup
물류센터와 연결됨 time_connect
신원 확인과 인수 조건 확인 time_appconnect
실제 배송 준비 완료 time_pretransfer
첫 물건이 도착함 time_starttransfer
모든 물건이 도착함 time_total

그래서 time_total 하나만 보면 "전체가 2초 걸렸어요"밖에 모르지만, 중간 표식을 같이 보면 DNS가 늦은 건지, 연결이 늦은 건지, 서버가 첫 응답을 늦게 준 건지를 나눠 볼 수 있어요.

누적 시간은 빼서 구간으로 바꿔 읽어요

time_starttransfer0.800이고 time_total0.900이면 첫 바이트까지 0.8초, 그 뒤 전체 다운로드까지 0.1초에 가까워요. time_total - time_starttransfer가 다운로드 쪽 감각을 주는 거죠.

자주 보는 계산은 이렇게예요.

보고 싶은 구간 대략 이렇게 계산해요 의미
DNS time_namelookup 이름 해석 완료까지
TCP 연결 time_connect - time_namelookup 주소를 안 뒤 연결이 열릴 때까지
TLS time_appconnect - time_connect HTTPS 보호 통로 준비
서버 처리 + 첫 응답 대기 time_starttransfer - time_pretransfer 요청을 보낸 뒤 첫 바이트가 오기까지
다운로드 time_total - time_starttransfer 첫 바이트 이후 전체 완료까지

숫자를 전부 더하면 안 돼요

time_namelookup, time_connect, time_appconnect, time_starttransfer, time_total은 대체로 같은 시작점을 기준으로 한 누적 시각이에요. 0.1 + 0.2 + 0.3처럼 더하면 실제 전체 시간보다 커져요.

curl -v는 숫자 대신 장면을 보여줘요

타이밍 값이 "어느 단계가 길었는지"를 숫자로 보여준다면, curl -v는 그 단계에서 무슨 일이 있었는지를 글로 보여줘요.

curl -sS -o /dev/null -v https://example.com/

실제로는 훨씬 길지만, 중요한 줄만 줄이면 이런 모양이에요.

*   Trying 104.20.23.154:443...
* Host example.com:443 was resolved.
* IPv6: 2606:4700:10::ac42:93f3, 2606:4700:10::6814:179a
* IPv4: 104.20.23.154, 172.66.147.243
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / ...
* ALPN: server accepted h2
* Server certificate:
*   subject: CN=example.com
*   subjectAltName: "example.com" matches cert's "example.com"
* OpenSSL verify result: 0
* using HTTP/2
> GET / HTTP/2
> Host: example.com
> User-Agent: curl/8.20.0
< HTTP/2 200
< content-type: text/html
< cf-cache-status: HIT

curl 공식 문서 기준으로 -v 출력에서 >는 curl이 보낸 헤더, <는 받은 헤더, *는 curl이 덧붙인 설명이에요. 이 구분만 알아도 로그가 훨씬 덜 무서워져요.

표시
* curl이 알려주는 진행 설명
> 클라이언트가 보낸 HTTP 헤더
< 서버가 보낸 HTTP 헤더
} curl이 보낸 데이터
{ curl이 받은 데이터

verbose 로그는 공유 전에 꼭 지워야 해요

curl -v에는 Authorization, Cookie, 토큰, 내부 호스트명 같은 민감한 값이 섞일 수 있어요. 공개 이슈나 채팅에 붙이기 전에는 요청 헤더와 URL query를 반드시 확인해야 해요.


curl -v는 위에서 아래로 체크포인트를 지워가며 읽어요

처음부터 모든 줄을 외우려고 하면 힘들어요. 대신 아래 순서로 보면 좋아요.

flowchart TD
    A[curl -v 출력] --> B[1. 어느 IP로 연결했나요?]
    B --> C[2. DNS 결과가 기대와 맞나요?]
    C --> D[3. TLS 인증서 검증은 통과했나요?]
    D --> E[4. ALPN으로 어떤 HTTP 버전이 골라졌나요?]
    E --> F[5. 요청 헤더는 기대와 맞나요?]
    F --> G[6. 응답 코드와 응답 헤더는 무엇인가요?]

1. 어느 IP로 연결했나요?

Trying 104.20.23.154:443... 같은 줄은 curl이 실제 연결을 시도한 주소예요. DNS가 여러 주소를 돌려줬더라도, 이번 요청에서는 그중 하나를 골라 붙을 수 있어요.

여기서 remote_ip와 같이 보면 좋아요.

remote_ip=172.66.147.243

같은 도메인인데 어떤 요청은 다른 IP로 가고, 특정 IP에서만 느리다면 서버 묶음이나 엣지 위치 차이를 의심할 수 있어요.

2. DNS 결과가 기대와 맞나요?

Host example.com:443 was resolved. 뒤에 IPv4, IPv6 후보가 보여요. 여기서 엉뚱한 IP가 보이면 앱 서버보다 DNS나 hosts, 프록시, 사내 네트워크 정책을 먼저 봐야 할 수 있어요.

3. TLS 인증서 검증은 통과했나요?

subjectAltName ... matchesOpenSSL verify result: 0 같은 줄은 이름 검증과 인증서 검증이 통과했다는 단서예요. 반대로 여기서 멈추면 HTTP 요청 자체가 서버 애플리케이션까지 가지 못했을 가능성이 커요.

4. 어떤 HTTP 버전이 골라졌나요?

ALPN: server accepted h2using HTTP/2 같은 줄은 협상된 HTTP 버전을 알려줘요. 앞에서 본 HTTP/2 프레임과 멀티플렉싱, HTTP/3와 QUIC 프레임 감각이 여기서 연결돼요.

5. 내가 보낸 요청이 맞나요?

> 줄을 보면 실제로 어떤 Host, User-Agent, Accept, 추가 헤더가 나갔는지 볼 수 있어요. 인증 헤더, 캐시 우회 헤더, 테스트용 Host 헤더가 빠졌다면 서버가 다른 응답을 주는 게 당연할 수 있어요.

6. 응답은 누가 어떤 힌트를 줬나요?

< HTTP/2 200, < server: cloudflare, < cf-cache-status: HIT 같은 줄은 응답의 표면 힌트예요. 특정 CDN이나 프록시 헤더가 보이면, "오리진이 직접 대답했나?"보다 "중간 계층이 어떤 상태였나?"도 같이 봐야 해요.

증상별로 먼저 볼 값을 나눠볼게요

curl 값을 읽는 목적은 모든 숫자를 예쁘게 모으는 게 아니에요. 증상별로 의심 지점을 좁히는 거예요.

증상 먼저 볼 값 / 줄 해석 방향
첫 접속이 유난히 느림 time_namelookup, time_connect, time_appconnect DNS, TCP, TLS 준비 단계 확인
첫 바이트가 늦음 time_starttransfer - time_pretransfer 서버 처리, 프록시 대기, 캐시 미스 의심
다운로드가 길게 늘어짐 time_total - time_starttransfer, size_download, speed_download 본문 크기, 대역폭, 압축, 스트리밍 확인
가끔 다른 결과가 옴 remote_ip, 응답 헤더, 캐시 상태 헤더 엣지 위치, 서버 묶음, 캐시 상태 차이 확인
브라우저와 curl 결과가 다름 User-Agent, 쿠키, 압축, HTTP 버전 클라이언트 조건 차이 확인
인증서 오류 TLS 관련 -v 줄, ssl_verify_result 이름 불일치, 체인, 신뢰 저장소 확인

여기서 time_starttransfer는 흔히 TTFB 감각과 연결해서 봐요. 다만 curl의 값은 curl이 본 전송 기준이고, 브라우저의 waterfall과는 캐시, 쿠키, 프록시, 연결 재사용 조건이 다를 수 있어요.

redirects를 따라가면 시간이 섞여 보여요

-L을 붙이면 curl이 Location 리다이렉트를 따라가요.

curl -sS -L -o /dev/null -w 'redirects=%{num_redirects}
time_redirect=%{time_redirect}
time_starttransfer=%{time_starttransfer}
time_total=%{time_total}
' https://example.com/

이때 time_redirect는 마지막 요청이 시작되기 전까지의 리다이렉트 단계 시간을 보여줘요. 그래서 -L을 쓴 결과와 쓰지 않은 결과를 섞어 비교하면 판단이 흐려질 수 있어요.

sequenceDiagram
    participant C as curl
    participant A as 첫 URL
    participant B as 최종 URL

    C->>A: GET /old
    A-->>C: 301 Location: /new
    C->>B: GET /new
    B-->>C: 200 OK

리다이렉트가 많으면 사용자는 그냥 "첫 화면이 늦다"고 느끼지만, 실제로는 최종 서버 처리 전부터 시간이 쓰였을 수 있어요.

연결 재사용은 curl 한 번 실행 안에서만 생각해야 해요

curl man page는 여러 URL을 한 번의 curl invocation에 넣으면 연결 재사용을 시도할 수 있지만, 별도 curl 실행 사이에서는 재사용할 수 없다고 설명해요. 이 차이가 꽤 중요해요.

curl -sS -o /dev/null -w 'connect=%{time_connect} total=%{time_total}\n' \
  https://example.com/ \
  https://example.com/

같은 명령 안에서 여러 URL을 요청하면 뒤쪽 요청은 이미 열린 연결을 재사용할 수 있어요. 반대로 쉘에서 curl ...을 두 번 따로 실행하면 매번 새 프로세스와 새 연결 조건에서 시작한다고 봐야 해요.

브라우저와 curl은 같은 클라이언트가 아니에요

브라우저는 쿠키, 캐시, connection pool, HTTP/⅔ 정책, service worker, 보안 정책을 갖고 있어요. curl은 훨씬 단순한 조건으로 요청할 수 있어요. 그래서 curl 결과가 빠르다고 브라우저 문제가 확정되는 것도 아니고, curl 결과가 느리다고 사용자 전체가 느리다는 뜻도 아니에요.

잘못 읽기 쉬운 함정

time_total 하나로 원인을 단정하기

time_total은 전체 시간이에요. 전체가 길다는 사실만으로 DNS, TLS, 서버 처리, 다운로드 중 무엇이 문제인지는 알 수 없어요. 최소한 time_namelookup, time_connect, time_appconnect, time_starttransfer를 같이 봐야 해요.

time_starttransfer를 순수 서버 처리 시간으로 보기

time_starttransfer는 시작부터 첫 바이트까지예요. 서버 처리 시간뿐 아니라 DNS, TCP, TLS, 요청 전송 준비 시간이 이미 포함돼요. 서버 처리 감각을 보려면 time_starttransfer - time_pretransfer처럼 빼서 봐야 해요.

-k로 성공했으니 문제가 없다고 보기

-k 또는 --insecure는 인증서 검증 실패를 무시하게 만들 수 있어요. 테스트에는 쓸 수 있지만, 실제 문제를 해결한 게 아니에요. 인증서 이름, 체인, 신뢰 저장소 문제는 따로 확인해야 해요.

verbose 로그를 그대로 공유하기

curl -v는 디버깅에 좋지만, 요청 헤더와 URL에 민감한 값이 들어갈 수 있어요. 특히 Authorization, Cookie, query token, 내부 도메인은 공유 전에 지워야 해요.

한 번의 결과만 보고 결론 내기

네트워크 시간은 흔들려요. 같은 URL도 DNS 캐시, 연결 대상 IP, 엣지 위치, 서버 부하에 따라 달라질 수 있어요. 최소 몇 번 반복하고, 값이 튀는지 안정적인지 봐야 해요.

자, 정리해볼까요?

오늘 우리가 배운 것

  • curl -v는 요청이 지나간 장면을 텍스트 로그로 보여줘요.
  • --write-out은 전송이 끝난 뒤 HTTP 버전, 응답 코드, IP, timing 값을 뽑아줘요.
  • time_* 값은 대체로 누적 시간이므로, 구간을 보려면 서로 빼서 읽어야 해요.
  • time_namelookup, time_connect, time_appconnect, time_starttransfer, time_total을 같이 보면 DNS, TCP, TLS, 첫 바이트, 다운로드 구간을 나눠 볼 수 있어요.
  • curl 결과와 브라우저 결과는 조건이 다를 수 있으니, 결론보다 의심 지점을 좁히는 데 쓰는 편이 좋아요.

이어서 보면 좋은 글

댓글