"curl은 macOS와 Linux 운영 환경에서 널리 쓸 수 있고, 프레임워크가 가린 HTTP 교환을 드러내 줘. 최소 컨테이너에는 빠져 있을 수 있지만, API 호출이 이상할 때 가장 먼저 꺼낼 만한 진단 도구야."
자주 쓰는 옵션
다음 옵션만 익혀도 대부분의 HTTP 진단을 시작할 수 있어:
-v— 상세 모드야. 요청 헤더는 (>), 응답 헤더는 (<), DNS·연결·TLS 상태 같은 curl 진단 메시지는 (*)로 구분해 stderr에 보여 줘. 본문 전체를 같은 표식으로 보여 주는 옵션은 아니야.-i— 응답 본문을 출력하면서 그 앞에 응답 헤더도 포함해. 헤더만 받는 옵션이 아니라는 점에 주의해.-I(대문자) — HEAD 요청을 보내. 서버가 HEAD를 올바르게 지원한다면 본문을 내려받지 않고 메타데이터를 확인할 수 있어.-X METHOD— 사용할 메서드 문자열을 직접 지정해.-X POST,-X DELETE처럼 쓸 수 있지만, 전송 동작까지 메서드에 맞춰 주지는 않아. 다른 옵션이 이미 올바른 메서드를 선택한다면 불필요하게 붙이지 않는 편이 안전해.-H 'Name: value'— 요청 헤더를 추가해. 헤더가 여러 개라면 옵션을 반복하면 돼.-d 'body'— 요청 본문을 보내며, 별도 메서드를 지정하지 않으면 POST를 선택해.-d @file.json는 파일에서 데이터를 읽고 기본 Content-Type은 form 데이터 형식이므로, JSON이라면 해당 헤더를 명시해야 해.--data-binary @file— 파일 내용을 줄바꿈 변환 없이 그대로 보내. 원본 바이트 보존이 필요한 업로드에 알맞아.-u user:pass— HTTP Basic 인증을 설정하는-H 'Authorization: Basic ...'의 편의 기능이야. 명령줄 기록과 프로세스 목록에 자격 증명이 노출되지 않도록 주의해야 해.-N— curl 출력 버퍼링을 끄는 옵션이야. SSE처럼 도착하는 즉시 내용을 확인해야 하는 스트림에서 유용해.--compressed— curl 빌드가 지원하는 압축 인코딩을 요청하고 응답을 자동으로 풀어 줘. 지원 형식은 curl 빌드마다 다를 수 있어.-L— 리디렉션을 따라가. 사용하지 않으면 보통 첫 3xx 응답에서 멈춰.-o file/-O— 응답 본문을 지정한 파일명 또는 URL에서 가져온 원격 파일명으로 저장해.-w '%{http_code}\n'— 전송이 끝난 뒤 상태 코드, 크기, 단계별 소요 시간 같은 변수를 원하는 형식으로 출력해.
HTTP 교환을 들여다보는 법
curl -v는 요청이 예상과 다르게 동작할 때 가장 먼저 비교하기 좋은 도구야:
# 보낸 모든 byte 와 받은 모든 byte 봐
curl -v -X POST https://api.example.com/users \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer abc.def.ghi' \
-d '{"name":"Pippa"}'
# 출력 shape:
# * Trying 1.2.3.4:443... (TCP connect)
# * TLSv1.3 (OUT), TLS handshake, ... (TLS handshake)
# > POST /users HTTP/1.1 (request 줄)
# > Host: api.example.com (request header)
# > Authorization: Bearer abc.def.ghi (request header)
# > Content-Type: application/json (request header)
# > Content-Length: 17 (request header)
# >
# > {"name":"Pippa"} (request body)
# < HTTP/1.1 201 Created (status 줄)
# < Location: /users/u_abc (response header)
# < Content-Type: application/json (response header)
# < {"id":"u_abc","name":"Pippa"} (response body)
위 동결 블록의 출력 모양은 HTTP 교환을 개념적으로 한데 그린 그림이야. 실제 -v 출력에서 >와 <는 주로 프로토콜 헤더에 붙고, 응답 본문은 일반 출력으로 분리되며 요청 본문도 저 모양 그대로 표시되지 않을 수 있어. 원시 데이터를 더 깊게 조사해야 한다면 --trace나 --trace-ascii를 검토해. httpx 호출은 401인데 curl은 200이라면 메서드, URL, 헤더, 쿠키, 본문 바이트가 정말 같은지 비교해야 해. 상세 출력에는 토큰과 쿠키가 노출될 수 있으므로 저장하거나 공유하기 전에 반드시 가려.
라이브러리 호출이 이상하면 같은 요청을 curl로 최소 재현해. curl만 성공한다고 곧바로 라이브러리 버그라고 단정할 수는 없어. 두 요청의 인증 정보, 프록시, 인증서 저장소, HTTP 버전, 리디렉션, DNS 경로가 다를 수 있기 때문이야. 반대로 둘 다 실패해도 서버 결함만이 아니라 요청이나 환경 설정이 원인일 수 있어. 핵심은 조건을 하나씩 맞춰 원인 범위를 좁히는 데 있어.
유용한 조합
단계별 시간 측정 — DNS, TCP, TLS, 첫 바이트, 전체 전송에 걸린 시간을 나눠 봐:
curl -w 'dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' -o /dev/null -s https://api.example.com/
출력 버퍼링을 끈 SSE 스트림:
curl -N -H 'Accept: text/event-stream' https://api.example.com/stream
요청률 제한 관찰 — 테스트가 허용된 엔드포인트에 요청을 100번 보내 상태 코드 분포를 세어 봐:
for i in {1..100}; do
curl -s -o /dev/null -w '%{http_code}\n' https://api.example.com/
done | sort | uniq -c
브라우저 요청 재현 — Chrome DevTools의 Network 패널에서 "Copy as cURL"을 선택하면 브라우저가 보낸 요청을 터미널에서 재현할 수 있어. 복사된 명령에는 쿠키와 토큰이 들어갈 수 있으니 비밀 정보처럼 다뤄야 해.
cwkPippa에서 curl을 쓰는 방식
cwkPippa WebUI가 멈추거나 500 응답을 돌려줄 때는 백엔드 엔드포인트에
curl -v http://localhost:8000/api/conversations/{id}를 보내 연결 거부, 401 인증 실패, 500 서버 오류, 200이지만 잘못된 본문 같은 경우를 먼저 구분할 수 있어. curl은 브라우저처럼 CORS 정책을 집행하지 않으므로 curl 성공만으로 CORS 문제가 없다고 결론 내리면 안 돼. CORS는 응답 헤더와 브라우저 콘솔을 함께 확인해야 해. 브라우저에서만 나타나는 문제는 Copy as cURL로 동일한 요청을 옮긴 뒤, 쿠키와 헤더를 하나씩 줄여 가며 원인을 찾을 수 있어.