Logo

curl 커맨드로 터미널에서 HTTP 호출하기

이번 포스팅에서는 터미널에서 간단한 명령어를 입력하여 웹 페이지나 API 데이터를 요청하고 받을 수 있는 HTTP 클라이언트 도구인 curl 커맨드에 대해서 알아보겠습니다.

curl 커맨드

1998년에 만들어진 curl 커맨드는 Postman이나 Insomnia와 같이 다양한 기능과 화려한 UI를 제공하는 GUI 기반 HTTP 클라이언트가 계속해서 출시되는 와중에도 아직까지 꾸준히 사랑받고 있는 CLI 도구입니다.

curl 커맨드는 리눅스나 macOS에 대부분의 경우 기본으로 탑재되어 있으며, 몇 가지 주요 옵션만 숙지하면 매우 간편하게 사용할 수 있습니다. 특히, 어떤 서버에 SSH를 통해서 원격 접속을 했을 때, 별도의 HTTP 클라이언트를 설치하기는 귀찮고, 빠르게 간단한 HTTP 호출을 해보고 싶을 때 매우 유용합니다.

기본 HTTP 호출

웹 브라우저에서 어떤 사이트에 접속하듯이, GET 방식으로 HTTP 호출을 할 때는 아무런 옵션없이 curl 커맨드를 사용할 수 있습니다.

예를 들어, 인테넷에 공개된 REST API 서비스인 JSONPlaceholder의 글 상세 조회 endpoint를 호출해보겠습니다.

$ curl https://jsonplaceholder.typicode.com/posts/1
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}%

다른 방식으로 호출

GET 방식이 아닌 다른 방식(verb, method)으로 HTTP 호출을 할 때는 -X 또는 --request 옵션을 사용하는데요.

예를 들어, 위에서 호출한 endpoint를 DELETE 방식으로 호출해보겠습니다.

$ curl https://jsonplaceholder.typicode.com/posts/1 -X DELETE
{}%

요청 바디/헤더 설정

POST나 PUT, PATCH 방식으로 HTTP 호출을 할 때는 일반적으로 생성하거나 변경할 데이터를 서버에 보냅니다. -d 또는 --data 옵션을 사용하여 송신할 데이터를 설정해줄 수 있으며, -H 또는 --header 옵션을 통해 데이터 포맷을 명시해줄 수 있습니다.

$ curl https://jsonplaceholder.typicode.com/posts \
  -d '{"title": "foo", "body": "bar", "userId": 1}' \
  -H 'Content-type: application/json; charset=UTF-8'
{
  "title": "foo",
  "body": "bar",
  "userId": 1,
  "id": 101
}%

위 예제에서는 json 포맷의 데이터를 JSONPlaceholder 서비스의 글 생성 endpoint에 보내고 있습니다.

쿼리 문자열 설정

GET 방식으로 데이터를 송신할 때는 URL의 쿼리 문자열(Query String)에 담아야하는데요. 이 때는 -G 또는 --get 옵션을 주고, 쿼리 문자열을 -d 옵션을 통해 설정해줍니다.

예를 들어, 글 목록을 조회할 때 쿼리 문자열로 userId=1을 넘기면, 해당 사용자의 글만 필터링되어서 응답이 됩니다.

$ curl https://jsonplaceholder.typicode.com/posts -G -d "userId=1"
[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
  },
  {
    "userId": 1,
    "id": 3,
    "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
    "body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
  },
  {
    "userId": 1,
    "id": 4,
    "title": "eum et est occaecati",
    "body": "ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit"
  },
  {
    "userId": 1,
    "id": 5,
    "title": "nesciunt quas odio",
    "body": "repudiandae veniam quaerat sunt sed\nalias aut fugiat sit autem sed est\nvoluptatem omnis possimus esse voluptatibus quis\nest aut tenetur dolor neque"
  },
  {
    "userId": 1,
    "id": 6,
    "title": "dolorem eum magni eos aperiam quia",
    "body": "ut aspernatur corporis harum nihil quis provident sequi\nmollitia nobis aliquid molestiae\nperspiciatis et ea nemo ab reprehenderit accusantium quas\nvoluptate dolores velit et doloremque molestiae"
  },
  {
    "userId": 1,
    "id": 7,
    "title": "magnam facilis autem",
    "body": "dolore placeat quibusdam ea quo vitae\nmagni quis enim qui quis quo nemo aut saepe\nquidem repellat excepturi ut quia\nsunt ut sequi eos ea sed quas"
  },
  {
    "userId": 1,
    "id": 8,
    "title": "dolorem dolore est ipsam",
    "body": "dignissimos aperiam dolorem qui eum\nfacilis quibusdam animi sint suscipit qui sint possimus cum\nquaerat magni maiores excepturi\nipsam ut commodi dolor voluptatum modi aut vitae"
  },
  {
    "userId": 1,
    "id": 9,
    "title": "nesciunt iure omnis dolorem tempora et accusantium",
    "body": "consectetur animi nesciunt iure dolore\nenim quia ad\nveniam autem ut quam aut nobis\net est aut quod aut provident voluptas autem voluptas"
  },
  {
    "userId": 1,
    "id": 10,
    "title": "optio molestias id quia eum",
    "body": "quo et expedita modi cum officia vel magni\ndoloribus qui repudiandae\nvero nisi sit\nquos veniam quod sed accusamus veritatis error"
  }
]%

응답 헤더도 출력

curl은 기본적으로 응답 바디(response body)만을 HTTP 호출 결과로 콘솔에 출력해줍니다. 응답 헤더(response headers)까지 함께 확인하고 싶은 경우에는 -i 또는 --include 옵션을 사용하면 됩니다.

$ curl https://jsonplaceholder.typicode.com/posts/1 -i
HTTP/2 200
date: Tue, 20 Jul 2021 05:53:57 GMT
content-type: application/json; charset=utf-8
content-length: 292
x-powered-by: Express
x-ratelimit-limit: 1000
x-ratelimit-remaining: 999
x-ratelimit-reset: 1626031770
vary: Origin, Accept-Encoding
access-control-allow-credentials: true
cache-control: max-age=43200
pragma: no-cache
expires: -1
x-content-type-options: nosniff
etag: W/"124-yiKdLzqO5gfBrJFrcdJ8Yq0LGnU"
via: 1.1 vegur
cf-cache-status: HIT
age: 28467
accept-ranges: bytes
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=j9WfyPrbmhLFD%2FGJY8Y0QkYHaF6LFmIwPXe8rSvKX9R1C%2BDnhq3wk%2B8J%2FQA7YUXm90%2BHawRyM%2FSkGXqBMz9S3Py1VvqZFrk6fcLn9i2dPvD79UjyGHczGfNVt9EF1yKt28sGEepW61Lg6OXjCQOy"}],"group":"cf-nel","max_age":604800}
nel: {"report_to":"cf-nel","max_age":604800}
server: cloudflare
cf-ray: 6719ee9bafc7d326-LAX
alt-svc: h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}%

응답 헤더만 출력

간혹 가다가 응답 바디에는 관심이 없고 응답 헤더만 보고 싶은 때가 있는데요. 이럴 때는 -I, --head 옵션을 사용할 수 있습니다.

$ curl https://jsonplaceholder.typicode.com/posts/1 -I
HTTP/2 200
date: Tue, 20 Jul 2021 06:03:39 GMT
content-type: application/json; charset=utf-8
content-length: 292
x-powered-by: Express
x-ratelimit-limit: 1000
x-ratelimit-remaining: 999
x-ratelimit-reset: 1626031770
vary: Origin, Accept-Encoding
access-control-allow-credentials: true
cache-control: max-age=43200
pragma: no-cache
expires: -1
x-content-type-options: nosniff
etag: W/"124-yiKdLzqO5gfBrJFrcdJ8Yq0LGnU"
via: 1.1 vegur
cf-cache-status: HIT
age: 200
accept-ranges: bytes
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=OOghBjWfpaV%2FDvD6S5n9M3%2B5t5zSwqIGruGZCpbVmRXifdA8MCXHsrmUgdpmB9i80019QOIR%2BbaS5adnagtpk%2F4HXw8ZcHooWgIj6wC4hxStwgnRVWQ2tQtD0wSkMyAsjmgle10VrgSXvFyks2tB"}],"group":"cf-nel","max_age":604800}
nel: {"report_to":"cf-nel","max_age":604800}
server: cloudflare
cf-ray: 6719fcd5fe90e794-LAX
alt-svc: h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400

상세 디버깅

-v 또는 --verbose 옵션과 함께 curl 커맨드를 사용하면 상세한 디버깅 정보가 출력됩니다.

$ curl https://jsonplaceholder.typicode.com/posts/1 -v
*   Trying 104.21.10.8...
* TCP_NODELAY set
* Connected to jsonplaceholder.typicode.com (104.21.10.8) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; CN=sni.cloudflaressl.com
*  start date: Jun 28 00:00:00 2021 GMT
*  expire date: Jun 27 23:59:59 2022 GMT
*  subjectAltName: host "jsonplaceholder.typicode.com" matched cert's "*.typicode.com"
*  issuer: C=US; O=Cloudflare, Inc.; CN=Cloudflare Inc ECC CA-3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fafba809200)
> GET /posts/1 HTTP/2
> Host: jsonplaceholder.typicode.com
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 200
< date: Tue, 20 Jul 2021 05:57:17 GMT
< content-type: application/json; charset=utf-8
< content-length: 292
< x-powered-by: Express
< x-ratelimit-limit: 1000
< x-ratelimit-remaining: 999
< x-ratelimit-reset: 1626031770
< vary: Origin, Accept-Encoding
< access-control-allow-credentials: true
< cache-control: max-age=43200
< pragma: no-cache
< expires: -1
< x-content-type-options: nosniff
< etag: W/"124-yiKdLzqO5gfBrJFrcdJ8Yq0LGnU"
< via: 1.1 vegur
< cf-cache-status: HIT
< age: 28667
< accept-ranges: bytes
< expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=gUOmBzI7k0T5EKab5qTnEmQr8zlawq5NiGkpmOgunm3BZ1uTpwzNgED2daofOGP9Mu0w%2BmxlNWqaOm34jm%2FUPQxvp%2FzADfeegmPsz7RLWIzPIr2MWbg7Swdpf6%2BKjMtEhDU0NZBUZqY6ukl5v%2BAu"}],"group":"cf-nel","max_age":604800}
< nel: {"report_to":"cf-nel","max_age":604800}
< server: cloudflare
< cf-ray: 6719f37deb9c04df-LAX
< alt-svc: h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400
<
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
* Connection #0 to host jsonplaceholder.typicode.com left intact
}* Closing connection 0

리다이렉트

일시적이든 영구적이든 리소스가 다른 URL로 이동했을 경우, 서버에서는 Location 헤더에 변경된 URL을 응답해서 보냅니다. 마치 웹 브라우저처럼 이 URL을 따라가고 싶다면 -L 또는 --location 옵션을 사용하면 됩니다.

예를 들어, 제가 구글의 URL에서 www.을 생략하고 호출을 하면 301 상태 코드와 함께 Location 헤더에 www.가 붙어있는 온전한 URL이 수신됩니다.

$ curl https://google.com -i
HTTP/2 301
location: https://www.google.com/
content-type: text/html; charset=UTF-8
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-F_gJSkhfPu6rtqt9b_rmgw' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
date: Sat, 23 Mar 2024 22:18:08 GMT
expires: Mon, 22 Apr 2024 22:18:08 GMT
cache-control: public, max-age=2592000
server: gws
content-length: 220
x-xss-protection: 0
x-frame-options: SAMEORIGIN
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://www.google.com/">here</A>.
</BODY></HTML>

-L 옵션을 사용하면 Location 헤더로 넘어온 변경된 URL이 연달아 호출되는 것을 볼 수 있습니다.

$ curl https://google.com -I -L
HTTP/2 301
location: https://www.google.com/
content-type: text/html; charset=UTF-8
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-P-S47GROadrKDpyl8oFzag' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
date: Sat, 23 Mar 2024 22:11:38 GMT
expires: Mon, 22 Apr 2024 22:11:38 GMT
cache-control: public, max-age=2592000
server: gws
content-length: 220
x-xss-protection: 0
x-frame-options: SAMEORIGIN
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

HTTP/2 200
content-type: text/html; charset=ISO-8859-1
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce--Xmi580Mxdpli7POL7z94g' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
date: Sat, 23 Mar 2024 22:11:39 GMT
server: gws
x-xss-protection: 0
x-frame-options: SAMEORIGIN
expires: Sat, 23 Mar 2024 22:11:39 GMT
cache-control: private
set-cookie: 1P_JAR=2024-03-23-22; expires=Mon, 22-Apr-2024 22:11:39 GMT; path=/; domain=.google.com; Secure
set-cookie: AEC=Ae3NU9OiScZmJF_ahmrJOhqi9oldEGhUYJ6VD83O3wURK4tlmcAYU9Kp-w; expires=Thu, 19-Sep-2024 22:11:39 GMT; path=/; domain=.google.com; Secure; HttpOnly; SameSite=lax
set-cookie: NID=512=eiRsukK9HEbic_GZ_COs7mHq0HqyAXYsaaJKxLb51Z-IHwKkMrHgic8PxeWXUc6GK8_3cUTkDiHpfG3mIKccZG0x4vleiNO7_eBn14v1AmhkJcAeJgzCepOOcErZaWMnVGpP4XErKlCJbDspIvgcJkV6WxvpC02M6HmgByytb7M; expires=Sun, 22-Sep-2024 22:11:39 GMT; path=/; domain=.google.com; HttpOnly
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

유용한 옵션

위에서 작성한 예제를 유심해 보시면 curl 커맨드 실행 결과 맨 뒤에 % 기호가 붙어있는 것을 볼 수 있습니다. % 기호 대신에 다른 문자를 종료 기호로 사용하고 싶다면 -w 또는 --write-out 옵션을 사용하면 됩니다.

예를 들어, 줄바꿈을 하고 싶다면..

$ curl https://jsonplaceholder.typicode.com/posts/1 -w '\n'
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

마치면서

이상으로 curl 커맨드를 사용해서 터미널에서 HTTP 호출을 하는 기본적인 방법과 자주 사용되는 옵션에 대해서 알아보았습니다. curl 커맨드에 대한 좀 더 자세한 내용은 공식 사이트를 참고 바랍니다.