본문으로 건너뛰기

curl으로 HTTP 요청 이해하기

· 약 12분
MinjiKim
MinjiKim
FrontEnd Engineer

들어가며

E2E 테스트 자동화를 도입하면서 테스트가 완료된 후 Slack 알림을 전송하는 작업을 진행했습니다. 이 과정에서 curl을 활용한 HTTP 요청이 필요했고, 익숙하지 않은 curl 요청 방식을 학습할 수 있는 좋은 기회가 되었습니다.

특히, curl의 verbose 모드(-v)를 사용하면 HTTP 요청의 상세한 흐름을 실시간으로 확인할 수 있다는 점이 흥미로웠습니다. 평소 머릿속으로만 이해하고 있던 네트워크 요청 과정을 직접 눈으로 볼 수 있어, HTTP 요청 디버깅에 큰 도움이 되었습니다.

이번 글에서는 curl을 활용하여 HTTP 요청을 이해하는 과정을 정리하고, 이를 통해 실제 문제를 해결했던 경험을 공유하고자 합니다.

curl이란

curl은 URL을 통해 데이터를 전송하거나 다운로드하는 명령어입니다. HTTP, HTTPS, FTP 등의 다양한 프로토콜을 지원하며, API 요청 테스트 및 자동화, 파일 업로드 및 다운로드, HTTP 요청 디버깅에 사용됩니다.

curl 기본 사용법

curl -X METHOD URL [OPTIONS]
  • -X METHOD: HTTP 메서드(GET, POST, PUT, DELETE)
  • URL: 요청 보낼 URL
  • [OPTIONS]: 헤더, 데이터, 인증 등 추가 설정

✅  주요 활용 사례

GET 요청(데이터 가져오기)

 # 기본 GET 요청
PRODUCTS_URL=https://fakestoreapi.com/products/1
curl -X GET ${PRODUCTS_URL}

# 쿼리 파라미터로 요청
SO_LIMITED_URL=https://fakestoreapi.com/products?limit=5
curl -X GET ${SO_LIMITED_URL}

# 커스텀 헤더 넣기
SLACK_API=https://slack.com/api/files.getUploadURLExternal?filename=test.webm&length=594980
curl -X GET -H "Authorization: YOUR_TOKEN" ${SLACK_API}
  • HTTP 헤더 추가: -H "필드명: 값”

POST 요청(데이터 전송)

 # 기본 POST 요청
curl -X POST ${EXAMPLE_URL} \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alswl99710@gmail.com"}'

# URL 인코딩된 폼 데이터 전송
curl -X POST ${LOGIN_URL} \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=alswl99710&password=1234"

# 폼 데이터 형식으로 파일 업로드
curl -X POST ${UPLOAD_URL} \
-F "file=@image.jpg"

# 예) 파일 업로드
curl -X POST ${UPLOAD_URL} \
-H "Content-Type: video/webm" \
--data-binary "@test.webm" -v

# 예) 슬랙에 파일 업로드 완료 알려주기
curl -X POST -H "Authorization: MY_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"files": [{
"id": "FILE_ID"
}],
"channel_id": "CHANNEL_ID",
"initial_comment": "first_thing_to_say"
}' https://slack.com/api/files.completeUploadExternal
  • -d 또는 (—data) : 데이터 전송
  • -F : multiplart/form-data 파일 업로드

PUT 요청(데이터 업데이트)

# 기본 PUT 요청: 이미 존재하는 리소스 업데이트
curl -X PUT https://api.example.com/users/123 \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alswl99710@gmail.com"}'

# 파일 업로드
curl -X PUT https://example.com/upload/image.jpg \
-H "Content-Type: image/jpeg" \
--data-binary "@image.jpg"
  • —data-binary : raw 파일 업로드

DELETE 요청(데이터 삭제)

# 기본 DELETE 요청
curl -X DELETE https://api.example.com/users/123

# 인증하여 삭제(보통 삭제를 위해서는 authentication 필요)
curl -X DELETE https://api.example.com/users/123 \
-H "Authorization: Bearer YOUR_TOKEN"

인증 메서드

  • username:password : 자동으로 기본 Basic Auth를 전송

    curl -u username:password https://api.com
  • Bearer 토큰 인증(OAuth): 현대 API에서 자주 사용

    curl -H "AUthorization: Bearer YOUR_ACCESS_TOKEN" https://api.example.come/data

응답 처리 및 디버깅

  • 응답 헤더 표시(-i 또는 —include)

    curl -i https://api.example.com/data
  • URl이 리다이렉트(301, 302)하는 경우 따라가도록

    curl -L https://example.com
  • 응답을 파일에 저장: 응답 body를 output.json에 쓰기

    curl -o output.json https://api.example.com/data

Curl -v로 이해하는 HTTP 요청 순서

curl -X GET ${PRODUCTS_URL} -v

curl의 verbose 모드(-v)를 사용하면, HTTP 요청의 모든 과정을 상세히 확인할 수 있습니다. 각 단계에서 무엇을 확인해야하는지 알아보겠습니다.


DNS 조회 및 연결

* Host fakestoreapi.com:443 was resolved.
* IPv6: (none)
* IPv4: 104.21.32.1, 104.21.80.1, 104.21.48.1, 104.21.112.1, 104.21.96.1, 104.21.64.1, 104.21.16.1
* Trying 104.21.32.1:443...
* Connected to fakestoreapi.com (104.21.32.1) port 443
  • DNS 조회 및 서버 연결 과정을 확인

🔈  이해하기

  • DNS lookup이 fakestoreapi.com을 IP 주소로 변환함
  • IPv4: 여러 IP는 로드 밸런싱을 의미
  • Trying… : 포트 443의 IP에 연결을 시도
  • Connected to ..: TCP 연결

✅  확인사항

  • DNS resolution이 실패하면, Could not resolve host 에러 발생
  • 연결이 지속되는 경우, Connection timed out
  • 여러 IPv4 주소가 보이는 경우, CDN(Content Delivery Network)일 가능성이 높음

TLS/SSL 핸드쉐이크

* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256
  • HTTPS 요청 시 SSL 인증서 검증 과정을 확인

🔈  이해하기

  • ALPN: curl offers h2,http/1.1: 어플리케이션 계층 프로토콜 협의(Application-Layer Protocol Negotiation)가 HTTP/2 또는 HTTP/1.1을 선택했음
  • TLS handshake, Client Hello: 클라이언트가 안전한 TLS 연결을 시작
  • SSL connection using TLSv1.3: 암호화 메서드(TLS 1.3)과 cipher suite 가 사용됨

✅  확인

  • SSL 핸드쉐이크가 실패한 경우, SSL certificate problem: unable to get local issuer certificate
  • TLS 버전이 오래된 경우, TLS 1.2를 사용하려면 —tlsv1.2를 사용할 것

SSL 인증서

💎 SSL(=TLS) 인증서

  • SSL은 인터넷 트래픽을 암호화하고 서버 신원을 확인하기 위한 프로토콜로, HTTPS 웹 주소가 있는 모든 웹 사이트에서는 SSL/TLS를 사용한다.
  • SSL 인증서는 웹사이트의 신원을 확인하고, 브라우저-서버 간 데이터를 암호화하는 디지털 보안 인증서
  • 웹 사이트를 방문할 경우(https://), 브라우저는 SSL 인증서를 확인하여 다음을 보장
    1. 웹사이트가 진짜 웹사이트인지(피싱 사이트가 아닌지)
    2. 데이터가 암호화되어 있는지(해커들로부터 보호)

💎 curl에서 SSL 인증서란?

  • 기본적으로, curl은 진짜 서버와 대화하고 있음을 보장하기 위해 SSL 인증서를 확인함.
  • 만약 curl이 유효하지 않은 SSL 인증서를 확인하면, 아래와 같은 에러 발생
    curl: (60) SSL certificate problem: unable to get local issuer certificate

서버 SSL 인증서 상세

* Server certificate:
* subject: CN=fakestoreapi.com
* start date: Dec 30 13:34:58 2024 GMT
* expire date: Mar 30 14:33:14 2025 GMT
* SSL certificate verify ok.

🔈 이해하기

  • subject: CN=fakestoreapi.com: 도메인의 SSL 인증서가 유효함
  • start date / expire date: 유효기간이 지났을 경우, SSL 에러 발생
  • SSL certificate verify ok : SSL 인증서는 유효함

✅ 확인

  • 인증서가 유효기간이 지났을 경우: SSL certificate has expired.
  • 도메인이 일치하지 않은 경우: Hostname mismactch

HTTP 요청 전송

* [HTTP/2] [1] OPENED stream for https://fakestoreapi.com/products/1
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:path: /products/1]
* [HTTP/2] [1] [:authority: fakestoreapi.com]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET /products/1 HTTP/2
> Host: fakestoreapi.com
> User-Agent: curl/8.7.1
> Accept: */*

🔈 이해하기

  • > GET /products/1 HTTP/2: HTTP GET 요청
  • Host: fakestoreapi.com: 요청 받은 서버
  • User-Agent: curl/8.7.1: 클라이언트
  • Accept: */* 클라이언트는 모든 응답 형식을 허용

✅  확인

  • API가 인증을 필요로 하는 경우(401 Unauthorized), 헤더에 Authorization을 추가해야함

HTTP 응답 상태 및 헤더 확인

< HTTP/2 200
< date: Sun, 02 Feb 2025 11:38:57 GMT
< content-type: application/json; charset=utf-8
< content-length: 364
< access-control-allow-origin: *
< etag: W/"16c-MMdrqY6N0sTiefLdsgtBej9eunY"
< x-powered-by: Express
< cf-cache-status: DYNAMIC
< server: cloudflare

🔈 이해하기

  • HTTP/2 200: 성공(200 OK)
  • content-type: application/json: JSON 응답임
  • content-length: 364 응답 크기
  • < access-control-allow-origin: * CORS가 허용됨
  • < cf-cache-status: DYNAMIC Cloudflare에 의해 캐싱되지 않음
  • etag: 캐싱에 사용

✅ 확인

  • 만약 403 Forbidden 또는 401 Unauthorized를 받는 경우,아래의 수정이 필요할 수 있음.
    • Authentication 헤더(Authorization: Bearer YOUR_TOKEN)
    • 쿠키(-H “Cookie: sessionid=12345”)
    • User-Agent 변경(일부 API는 curl agent를 차단하기도 함)

6️⃣ HTTP 응답 바디

{
"id": 1,
"title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
"price": 109.95,
"description": "Your perfect pack for everyday use...",
"category": "men's clothing",
"image": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
"rating": { "rate": 3.9, "count": 120 }
}

🔈 이해하기

  • 응답 데 이터는 유효한 JSON 객체로, id, title, price.. 등의 키를 가짐.

✅ 확인

  • 응답 바디가 비어있는 경우({}), 파라미터가 누락된 것일 수 있음.

  • JSON이 아닌 경우, Content-Type 헤더를 확인할 것.

  • 301/302 Redirect but no response 에러 발생하는 경우, -L(—location) 옵션으로 실행

    # -L 없이 요청하는 경우
    curl https://short.url/example

    # 요청이 리다이렉트에서 멈춘 경우
    HTTP/1.1 301 Moved Permanently
    Location: https://example.com/real-url

    # 리다이렉트할 수 있도록 요청
    curl -L https://short.url/example
  • JSON 형식이 잘못된 경우, 포맷팅하기

    curl -s ${URL} | jq

나가며

이전까지는 fetch API를 사용해 HTTP 요청을 보내는 것이 익숙했지만, 이번 경험을 통해 curl을 활용한 CLI 요청을 보다 깊이 이해할 수 있었습니다. 특히, -v 모드를 사용하면 네트워크 요청 과정을 시각적으로 확인하며 디버깅할 수 있다는 점이 매우 유용했습니다. 앞으로 API 테스트, 디버깅, 파일 업로드 등 다양한 워크플로우에서 이를 활용할 수 있을 것입니다.

새로운 한 해가 시작되었습니다. 이러한 작은 배움이 쌓여 큰 성장을 이룬다는 마음으로, 더 나은 개발자로 한 걸음씩 나아가려 합니다. 올해도 꾸준히 배우고 성장하며, 더 좋은 코드와 웹 전반을 이해해나가도록 하겠습니다. 화이팅입니다. 😄