브라우저에서는 그냥 “느려요”로 보이는데, 터미널에서는 요청이 지나간 체크포인트가 꽤 많이 보여요.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 page와 everything curl의 write-out 문서를 기준으로 볼게요.
여기서는
curl을 부하 테스트 도구처럼 쓰는 법이 아니라, 요청 하나를 어디서 읽을지에 집중해요. DNS, TCP, TLS, HTTP 버전, 응답 코드, 첫 바이트, 전체 시간을 나눠 보는 감각이 목표예요.먼저 몸통은 버리고 신호만 보게 만들어요
처음curl https://example.com/을 치면 HTML 본문이 주르륵 나와요. 그런데 디버깅에서는 본문보다 연결과 시간 신호가 먼저 필요할 때가 많아요.
그래서 기본 모양은 이렇게 잡으면 좋아요.
중요한 건
time_* 값이 대부분 구간별 시간이 아니라 시작부터 그 지점까지의 누적 시간이라는 점이에요. 그래서 숫자를 그대로 더하면 안 되고, 필요하면 서로 빼서 구간을 읽어야 해요.
실제 출력은 이렇게 생겨요
아래는 이 작업 환경에서https://example.com/으로 한 번 실행해 본 예시예요. 인터넷 경로와 서버 상태에 따라 값은 매번 달라져요.
- 이름을 주소로 바꾸는 데 약
0.085s까지 걸렸어요. - TCP 연결 완료는 약
0.117s시점이에요. - TLS handshake 완료는 약
0.164s시점이에요. - 첫 바이트는 약
0.202s시점에 왔어요. - 전체 다운로드도 거의 같은 시점에 끝났으니, 본문 다운로드는 작았어요.
curl timing은 요청이 지나간 체크포인트를 숫자로 찍어준다고 보면 돼요. 한 줄짜리 평균 시간이 아니라, 어느 지점까지 도달하는 데 얼마나 걸렸는지를 보여주는 표식이에요.
비유로 보면, 택배 추적 시간표에 가까워요
택배가 늦을 때 “배송이 느려요”라고만 하면 원인을 알기 어렵죠.- 주문 접수까지 늦었는지
- 물류센터 입고가 늦었는지
- 간선 이동이 늦었는지
- 집 근처 도착 뒤 배송이 늦었는지
curl --write-out도 비슷해요.
그래서
time_total 하나만 보면 “전체가 2초 걸렸어요”밖에 모르지만, 중간 표식을 같이 보면 DNS가 늦은 건지, 연결이 늦은 건지, 서버가 첫 응답을 늦게 준 건지를 나눠 볼 수 있어요.
누적 시간은 빼서 구간으로 바꿔 읽어요
time_starttransfer가 0.800이고 time_total이 0.900이면 첫 바이트까지 0.8초, 그 뒤 전체 다운로드까지 0.1초에 가까워요. time_total - time_starttransfer가 다운로드 쪽 감각을 주는 거죠.
자주 보는 계산은 이렇게예요.
curl -v는 숫자 대신 장면을 보여줘요
타이밍 값이 “어느 단계가 길었는지”를 숫자로 보여준다면, curl -v는 그 단계에서 무슨 일이 있었는지를 글로 보여줘요.
-v 출력에서 >는 curl이 보낸 헤더, <는 받은 헤더, *는 curl이 덧붙인 설명이에요. 이 구분만 알아도 로그가 훨씬 덜 무서워져요.
curl -v는 위에서 아래로 체크포인트를 지워가며 읽어요
처음부터 모든 줄을 외우려고 하면 힘들어요. 대신 아래 순서로 보면 좋아요.
1
curl -v 원본 출력을 남겨요
필요한 경우 token과 cookie를 가린 뒤 연결 시작부터 응답 헤더까지 보존해요. 일부 줄만 떼어 보면 DNS, TLS, HTTP 경계가 사라질 수 있어요.
2
실제로 연결한 IP를 확인해요
Connected to 줄에서 hostname이 어느 IP와 port로 이어졌는지 봐요. 예상과 다른 edge나 사설 주소로 갔다면 routing과 proxy 설정을 먼저 의심해요.3
DNS 결과를 비교해요
curl이 선택한 주소가
dig 결과와 기대한 레코드에 맞는지 확인해요. 여러 주소가 있다면 한 번의 결과만으로 전체 경로를 단정하지 않아요.4
TLS 인증서 검증을 확인해요
HTTPS라면 hostname, 유효 기간, issuer와 verification 결과를 읽어요. 이 단계가 실패하면 HTTP 요청은 서버 애플리케이션까지 도달하지 않아요.
5
ALPN으로 선택된 HTTP 버전을 확인해요
client와 server가
h2, http/1.1 중 무엇에 합의했는지 봐요. 기대한 버전과 다르면 proxy와 TLS 종료 지점의 설정도 함께 확인해요.6
보낸 요청 헤더를 확인해요
>로 시작하는 줄에서 method, path, Host, 인증 헤더가 기대와 같은지 읽어요. shell quoting이나 redirect 때문에 요청이 달라지지 않았는지도 봐요.7
응답 코드와 헤더를 확인해요
<로 시작하는 줄에서 status, cache, redirect, server 식별 헤더를 읽어요. 앞선 연결 정보와 합쳐 어느 계층이 결과를 만들었는지 판단해요.1. 어느 IP로 연결했나요?
Trying 104.20.23.154:443... 같은 줄은 curl이 실제 연결을 시도한 주소예요. DNS가 여러 주소를 돌려줬더라도, 이번 요청에서는 그중 하나를 골라 붙을 수 있어요.
여기서 remote_ip와 같이 보면 좋아요.
2. DNS 결과가 기대와 맞나요?
Host example.com:443 was resolved. 뒤에 IPv4, IPv6 후보가 보여요. 여기서 엉뚱한 IP가 보이면 앱 서버보다 DNS나 hosts, 프록시, 사내 네트워크 정책을 먼저 봐야 할 수 있어요.
3. TLS 인증서 검증은 통과했나요?
subjectAltName ... matches와 OpenSSL verify result: 0 같은 줄은 이름 검증과 인증서 검증이 통과했다는 단서예요. 반대로 여기서 멈추면 HTTP 요청 자체가 서버 애플리케이션까지 가지 못했을 가능성이 커요.
4. 어떤 HTTP 버전이 골라졌나요?
ALPN: server accepted h2와 using 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_starttransfer는 흔히 TTFB 감각과 연결해서 봐요. 다만 curl의 값은 curl이 본 전송 기준이고, 브라우저의 waterfall과는 캐시, 쿠키, 프록시, 연결 재사용 조건이 다를 수 있어요.
redirects를 따라가면 시간이 섞여 보여요
-L을 붙이면 curl이 Location 리다이렉트를 따라가요.
time_redirect는 마지막 요청이 시작되기 전까지의 리다이렉트 단계 시간을 보여줘요. 그래서 -L을 쓴 결과와 쓰지 않은 결과를 섞어 비교하면 판단이 흐려질 수 있어요.
리다이렉트가 많으면 사용자는 그냥 “첫 화면이 늦다”고 느끼지만, 실제로는 최종 서버 처리 전부터 시간이 쓰였을 수 있어요.
연결 재사용은 curl 한 번 실행 안에서만 생각해야 해요
curl man page는 여러 URL을 한 번의 curl invocation에 넣으면 연결 재사용을 시도할 수 있지만, 별도 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결과와 브라우저 결과는 조건이 다를 수 있으니, 결론보다 의심 지점을 좁히는 데 쓰는 편이 좋아요.
이어서 보면 좋은 글
- End-to-End Request Debugging - 느린 요청은 어디서 막히고 있을까요? — curl timing 값을 전체 요청 체크포인트 지도 위에 올려볼 수 있어요.
- HTTP/2는 어떻게 여러 요청을 한 연결에 섞어 보낼까요? —
curl -v에서h2가 보일 때 실제로 어떤 구조인지 이어서 볼 수 있어요. - HTTP/3는 QUIC 위에서 프레임을 어떻게 나눌까요? —
h3가 보일 때 HTTP/3와 QUIC 스트림 감각을 같이 연결할 수 있어요.