curl 명령을 읽고 코드로 옮기기
curl 명령을 필드별로 정확히 읽고 fetch나 파이썬 requests로 옮기는 법, 그리고 대부분의 번역이 걸려 넘어지는 암묵적 POST 함정을 정리합니다.
API 문서가 curl 줄을 건넵니다. 브라우저 네트워크 패널에 "Copy as cURL" 버튼이
있습니다. 누군가 버그 리포트에 재현을 curl ...로 붙여 넣습니다. 어느 경우든
curl 명령은 HTTP 요청의 정규 기술이고, 작업은 같습니다. 그것을 올바르게 읽어
fetch, 파이썬 requests, 또는 axios로 재현하는 것입니다. 번역의 대부분은
기계적입니다. 사람들이 틀리는 부분은 암묵적 동작, 즉 curl이 플래그에 명시되지
않은 채 하는 것들입니다.
요청의 해부
모든 HTTP 요청은 다섯 부분을 갖고, 모든 curl 플래그는 정확히 그중 하나에 매핑됩니다.
- 메서드 —
GET,POST,PUT등.-X로 설정하거나 추론됨. - URL — 쿼리 문자열을 포함한 맨 인자.
- 헤더 —
-H당 하나. - 본문 —
-d,--data-raw,-F, 또는--data-urlencode. - 인증 — HTTP Basic용
-u, 또는-H를 통한Authorization헤더.
curl 명령을 번역한다는 것은 각 플래그를 읽고, 그것이 다섯 부분 중 무엇을 설정하는지 정하고, 대상 언어로 동등물을 내놓는 것입니다. 메서드는 자주 암묵적인 유일한 필드이고, 대부분의 오역이 거기서 옵니다.
플래그 참조
| 플래그 | 의미 | 요청에 미치는 영향 |
|---|---|---|
-X, --request |
메서드 설정 | 추론된 메서드를 덮어씀(-X POST, -X PUT) |
-H, --header |
헤더 추가 | 플래그당 헤더 하나, 나중 것이 덮어쓸 수 있음 |
-d, --data |
요청 본문 | POST + application/x-www-form-urlencoded를 함의, 여러 -d는 &로 이어짐 |
--data-raw |
본문, 문자 그대로 | -d와 같지만 선두 @를 파일 참조로 다루지 않음 |
--data-urlencode |
본문, 퍼센트 인코딩 | 보내기 전에 값을 퍼센트 인코딩함 |
-G, --get |
데이터를 쿼리로 이동 | -d 데이터를 GET과 함께 URL 쿼리 문자열로 보냄 |
-u, --user |
HTTP Basic 인증 | Authorization: Basic <base64(user:pass)>를 보냄 |
-F, --form |
멀티파트 필드 | multipart/form-data 설정, -F file=@path로 파일 업로드 |
-b, --cookie |
쿠키 전송 | Cookie 헤더 추가 |
-L, --location |
리디렉션 따라가기 | 3xx 응답에서 요청을 재발행 |
이 중 몇 개는 분명히 짚을 날카로운 모서리가 있습니다. -d는 인자에서 줄바꿈을
벗기고 값이 @로 시작하면 파일에서 읽습니다(그래서 -d @body.json은 파일
내용을 보냅니다). --data-raw가 @를 문자 그대로 보내는 변형입니다.
--data-urlencode는 값을 퍼센트 인코딩하며, 이는 폼 필드가 &, =, 공백을
담을 때 중요합니다. 규칙은 URL 퍼센트 인코딩
에서 다룬 바로 그것입니다. 그리고 HTTPS 없는 -u는 자격 증명을 Base64
인코딩으로 전선 위에 보내며, 이는 인코딩이지 암호화가 아닙니다(아래에서 더).
암묵적 POST 함정
이것은 curl을 손으로 번역할 때 가장 흔한 실수입니다.
curl https://api.example.com/items -d 'name=widget&qty=3'
여기 -X POST가 없지만, 이것은 POST입니다. -d의 존재가 메서드를 기본
GET에서 POST로 바꾸고, -H로 덮어쓰지 않는 한
Content-Type: application/x-www-form-urlencoded를 설정합니다. 이 명령을
번역하는 사람들은 종종 쿼리 문자열을 가진 GET이나 JSON 본문을 가진 POST를
씁니다. 둘 다 틀렸습니다. 올바른 읽기는 POST, 폼 인코딩 본문,
name=widget&qty=3입니다.
같은 것이 -F(multipart/form-data로 POST를 함의)와
-T/--upload-file(PUT을 함의)에도 적용됩니다. 명시적 -X를 볼 때는 그것이
이깁니다. curl -X PUT -d '...'는 폼 본문을 가진 PUT입니다. 본문 플래그를
먼저 읽고, 메서드를 추론하고, 그다음 -X가 덮어쓰게 하세요.
작업 예
인증, 커스텀 헤더, JSON 본문을 가진 현실적 명령을 봅시다.
curl -X POST https://api.example.com/v1/orders \
-H 'Authorization: Bearer tok_abc123' \
-H 'Content-Type: application/json' \
-d '{"sku":"A-19","qty":2}'
-d가 JSON을 나르는 것은 오직 -H 'Content-Type: application/json'이 있기
때문임에 주목하세요. 그 헤더 없이는 curl이 같은 바이트를 폼 인코딩으로 라벨할
것입니다. 본문 콘텐츠 타입은 문자열의 모양이 아니라 헤더가 결정합니다.
자바스크립트 fetch에서는 이렇습니다.
const res = await fetch("https://api.example.com/v1/orders", {
method: "POST",
headers: {
"Authorization": "Bearer tok_abc123",
"Content-Type": "application/json",
},
body: JSON.stringify({ sku: "A-19", qty: 2 }),
});
파이썬 requests에서는 이렇습니다.
import requests
res = requests.post(
"https://api.example.com/v1/orders",
headers={"Authorization": "Bearer tok_abc123"},
json={"sku": "A-19", "qty": 2},
)
requests 버전은 json=을 쓰며, 이는 Content-Type: application/json 헤더를
대신 설정해 줍니다. 그래서 명시적으로 넘기면 중복이고, 예제는 그것을 뺐습니다.
그 편의는 또한 함정입니다. curl 명령의 콘텐츠 타입이 다른 것이었다면 json=은
틀린 도구이고 data=가 필요했을 것입니다. 파라미터를 고르기 전에 헤더를
읽으세요.
본문 콘텐츠 타입
본문 플래그는 서로 교환 가능하지 않습니다. 각각 다른 콘텐츠 타입과 클라이언트의 다른 파라미터에 매핑됩니다.
- 폼 인코딩 —
-d 'a=1&b=2'. 타입application/x-www-form-urlencoded.fetch에서는URLSearchParams를 짓고,requests에서는data={"a": 1, "b": 2}를 넘깁니다. - JSON —
-H 'Content-Type: application/json' -d '{...}'.fetch에서는 본문을JSON.stringify하고,requests에서는json={...}. - 멀티파트 —
-F field=value -F [email protected]. 경계가 있는 타입multipart/form-data.fetch에서는FormData객체를 쓰고(Content-Type을 수동으로 설정하지 마세요. 런타임이 경계를 더합니다),requests에서는files={...}를 넘깁니다.
실패 양상은 이들을 섞는 것입니다. 폼 인코딩 본문을 보내면서
application/json을 선언하거나, FormData를 JSON.stringify하는 것입니다.
서버는 그 불일치를 거부하며, 종종 헷갈리는 400으로 합니다.
Basic 인증은 Base64이지 암호화가 아니다
-u user:pass는 토큰이 base64("user:pass")인 Authorization: Basic <token>
헤더를 만듭니다. Base64는 키 없이 되돌릴 수 있습니다. 헤더를 보는 누구든 즉시
자격 증명을 복구합니다. HTTPS 위에서는 TLS가 전체 교환을 암호화하므로 받아들일
만하고, 평문 HTTP 위에서는 비밀번호를 평문으로 보내는 것과 같습니다. http://
URL에 대한 -u를 본다면, 그 자격 증명을 침해된 것으로 다루세요.
번역은 간단합니다. fetch에서는 이렇습니다.
headers: { "Authorization": "Basic " + btoa("user:pass") }
requests에서는 auth=("user", "pass")를 넘기면 라이브러리가 헤더를 대신
짓습니다.
번역이 일대일이 아닐 때
몇몇 curl 동작은 깔끔한 단일 줄 동등물이 없습니다. -L(리디렉션 따라가기)은
requests에서 기본이지만 fetch에서는 redirect: "follow"로 요청해야
합니다(이 또한 기본값). 그리고 둘 다 기본적으로 POST 본문을 리디렉션 대상으로
재전송하지 않는데, 이는 --post301/--post302가 설정되지 않은 한 curl 자신의
동작과 일치합니다. -b의 쿠키는 Cookie 헤더가 되지만, 원본이 요청 간 쿠키
유지에 -c/--cookie-jar에 의존했다면 세션 객체(requests.Session())가 그것을
재현하는 데 필요합니다. 그리고 --compressed는 클라이언트가 Accept-Encoding을
협상하게 하는 것에 매핑되며, fetch와 requests 둘 다 투명하게 합니다.
요청이 나가면, 응답 상태 코드가 번역이 맞았는지 알려줍니다. 415는 콘텐츠
타입이 틀렸다는 뜻이고, 401은 인증 헤더가 안 닿았다는 뜻입니다. 우리
HTTP 상태 참조는 각 코드를 그 유력한 원인에 매핑합니다.
흔한 경우라면, curl 명령을 우리 curl 변환기에 붙여 넣고
메서드와 콘텐츠 타입이 올바르게 추론된 fetch, requests, axios 동등물을
얻으세요. 손 번역이 그토록 자주 틀리는 암묵적 POST 경우를 포함해서요.