API 엔드포인트 테스트 방법: DNS·TLS·CORS 점검
핵심 요약
"API가 안 된다"는 거의 하나의 문제가 아닙니다. DNS, 443 포트, TLS, HTTP 상태, CORS, 페이로드의 6개 계층을 차례로 테스트하고 처음 실패하는 계층에서 멈추세요. 흔한 원인은 전파되지 않은 DNS 레코드, 하룻밤 사이 만료된 인증서, 그리고 curl은 잘 되는데 브라우저 호출만 깨뜨리는 Access-Control-Allow-Origin 헤더 누락입니다.
지금 확인하기

읽기 시작
"API가 안 된다"가 하나의 문제가 아닌 이유
API 호출이 실패하면 클라이언트가 보여 주는 에러는 실제로 고장 난 지점보다 여러 계층 위에서 만들어집니다. 브라우저의 Failed to fetch만으로는 호스트 이름이 확인되지 않았는지, TCP 연결이 거부되었는지, TLS 핸드셰이크가 거부되었는지, 아니면 서버가 정상적인 200을 반환했는데 CORS 헤더가 없어 브라우저가 버렸는지 알 수 없습니다. 네 가지 모두 같은 메시지가 나옵니다.
추측을 멈추고 스택을 순서대로 따라가세요. 각 계층은 바로 아래 계층이 성공해야 의미가 있으므로, 처음 실패하는 계층이 진짜 버그이고 그 뒤는 잡음입니다.
확인할 계층은 여섯 개이며, 모두 합쳐 2분 정도면 됩니다.
- DNS: 호스트 이름이 IP로 확인되는가?
- 443 포트: 수신 대기 중인 프로세스가 있는가, 아니면 연결이 거부되거나 필터링되는가?
- TLS: 인증서가 유효하고 만료되지 않았으며 체인이 완전한가?
- HTTP 상태: 서버가 실제로 무엇을 반환하는가: 200, 401, 429, 503?
- CORS: 브라우저가 본문을 넘겨주는 데 필요한 헤더가 응답에 있는가?
- 페이로드: JSON이 코드가 기대하는 형태인가?
엔드포인트의 상태와 헤더를 지금 확인
마음껏 호출할 수 있는 실제 엔드포인트 고르기
연습하려면 실제 데이터를 반환하고, API 키가 필요 없으며, 같은 요청을 스무 번 보내도 속도 제한이 걸리지 않는 라이브 엔드포인트가 필요합니다. 요즘 주요 공개 API는 대부분 가입 양식과 토큰을 요구하기 때문에, 요청 실패의 원인이 테스트하려는 계층이 아니라 인증 정보일 수 있어 후보가 많이 줄어듭니다.
아래 예시는 Waqt Azan (مواقيت الصلاة)의 무료 기도 시간 API를 사용합니다. 100개국 이상, 6만 곳이 넘는 지역을 다루는 아랍어 기도 시간 서비스입니다. 연습용 엔드포인트로 좋은 이유는 세 가지입니다. 키도 인증 헤더도 필요 없고, HTML이 아닌 구조화된 JSON을 반환하며, CORS 정책이 느슨합니다. 그래서 curl 명령은 그대로 실행되고, fetch() 예제도 엄격한 Content-Security-Policy가 없는 페이지의 콘솔에서 동작합니다.
엔드포인트는 경로에 국가와 도시 슬러그를 그대로 받습니다.
# Prayer times for a city — no API key, no headers required
curl -s https://waqtazan.com/api/v1/timings/saudi-arabia/riyadh
# The same data as a subscribable calendar feed
curl -s https://waqtazan.com/api/v1/ical/saudi-arabia/riyadh.ics실제로 디버깅 중인 호스트로 바꿔 쓰세요. 공개 API든, 자체 스테이징 서버든, 서드파티 웹훅 수신기든 여섯 계층은 똑같습니다.
1계층: 호스트 이름이 확인되는가?
DNS는 흔한 원인입니다. 레코드를 바꿨는데 전파되지 않았거나, 와일드카드를 제거했거나, API가 쓰는 서브도메인이 애초에 만들어지지 않은 경우입니다.
무엇보다 먼저 호스트 이름을 확인하세요. 이 단계에서 아무것도 나오지 않으면 앱을 몇 번 재시작해도 소용없습니다. 클라이언트가 소켓을 여는 단계까지 가지도 못합니다.
dig +short waqtazan.com A
nslookup waqtazan.com
# Compare what different resolvers see — catches mid-propagation records
dig +short waqtazan.com A @1.1.1.1
dig +short waqtazan.com A @8.8.8.8레코드를 바꾼 직후 두 공개 리졸버의 답이 다르면 아직 전파 중입니다. 클라이언트가 어느 리졸버에 닿느냐에 따라 API가 간헐적으로 고장 난 것처럼 보입니다(지오 라우팅 CDN은 리졸버마다 항상 다른 주소를 줄 수 있으므로, 설정한 레코드와 대조하세요). DNS 조회로 전체 레코드를 확인하거나, 레코드 변경 직후 DNS 전파 검사기로 전 세계 리졸버의 반영 상태를 확인하세요.
여기서 답이 비어 있으면 Chrome의 ERR_NAME_NOT_RESOLVED, Node의 getaddrinfo ENOTFOUND로 이어집니다. 해결 방법 전체는 ERR_NAME_NOT_RESOLVED를 참고하세요.
걱정하지 않아도 되는 점이 하나 있습니다. 많은 API가 CDN 뒤에 있어서 A 레코드가 원본 서버가 아닌 CDN 엣지를 가리킵니다. 정상입니다. 중요한 것은 어떤 IP인지가 아니라 답이 나온다는 사실입니다.
2계층: 443 포트가 열려 있는가?
호스트 이름이 확인되었으니 주소는 있습니다. 이제 그 주소에서 무언가 수신 대기 중인지 확인합니다. 클라이언트가 똑같이 보고해 버리는 서로 다른 세 가지 장애를 여기서 구분합니다.
- Open(열림): TCP 핸드셰이크가 완료됩니다. 다음 계층으로 넘어가세요.
- Refused(거부): 호스트가 즉시 RST로 응답했습니다. 호스트는 있지만 그 포트에 바인딩된 것이 없습니다. 보통 서비스가 죽었거나 localhost에만 바인딩된 경우입니다.
- Filtered(필터링): 연결이 타임아웃될 때까지 멈춥니다. 방화벽이나 보안 그룹이 패킷을 조용히 버리고 있어서, 요청이 1분 넘게 멈춰 있다가 실패합니다(macOS에서는 75초).
# Time-boxed TCP check: -w limits the connect on Linux; on macOS use -G 5 instead
nc -vz -w 5 waqtazan.com 443
# Or with curl: --connect-timeout caps the connect; 000 means no response came back
curl -s -o /dev/null -w '%{http_code}\n' --connect-timeout 5 https://waqtazan.com/거부와 필터링은 해결책이 완전히 다르므로 어느 쪽인지 아는 것이 좋습니다. 포트 검사기는 두 가지를 구분하고 네트워크 바깥에서 테스트합니다. 회사 VPN에서는 열려 있는 포트가 공개 인터넷에서는 방화벽에 막혀 있을 수 있어서 중요합니다.
curl이 상태 코드로 000을 출력하면 HTTP 응답이 돌아오지 않은 것입니다. 보통 1~3계층에서 실패했을 때(DNS 응답 없음, 포트 거부 또는 필터링, TLS 핸드셰이크 실패)지만, 연결을 받은 뒤 끊어 버리거나 아예 응답하지 않는 서버도 000이 나옵니다. -v를 붙이면 마지막으로 성공한 단계를 볼 수 있습니다. 거부된 경우는 ERR_CONNECTION_REFUSED에서 자세히 다룹니다.
3계층: TLS 인증서가 유효한가?
443 포트는 열려 있지만, 포트가 열려 있다고 HTTPS 엔드포인트가 동작하는 것은 아닙니다. 인증서는 일정에 따라 만료되고 갱신 자동화는 조용히 실패하기 때문에, 하룻밤 사이 고장 나는 전형적인 원인입니다.
인증서는 세 가지 조건을 동시에 만족해야 합니다. 만료되지 않았을 것, 이름이 요청한 호스트 이름과 일치할 것, 서버가 중간 인증서 체인을 완전히 보낼 것입니다. 세 번째가 까다롭습니다. 체인이 불완전해도 브라우저에서는 동작하는 경우가 많은데, Chrome은 빠진 중간 인증서를 직접 내려받고 Firefox는 중간 인증서를 미리 내장하고 있기 때문입니다. 반면 Node, Python, Java, OpenSSL 빌드의 curl은 실패합니다(macOS에 포함된 curl은 Apple의 검증기를 사용해 통과할 수 있습니다). 이것이 "브라우저에서는 되는데 운영 환경에서는 안 된다"는 전형적인 버그 리포트의 원인입니다.
# Expiry dates and subject
echo | openssl s_client -connect waqtazan.com:443 -servername waqtazan.com 2>/dev/null \
| openssl x509 -noout -subject -dates
# Full chain as presented by the server
openssl s_client -connect waqtazan.com:443 -servername waqtazan.com -showcerts < /dev/null-showcerts 출력에서 인증서 개수를 세어 보세요. 인증서가 한 장뿐이면 보통 중간 인증서가 빠진 것입니다. SSL 인증서 검사기는 만료일, 체인 완전성, 호스트 이름 일치를 한 번에 보고하며, 중간 인증서가 왜 중요한지는 SSL 인증서 체인이란에서 설명합니다.
-servername은 남겨 두세요. OpenSSL 1.1.1 이상은 -connect 호스트 이름에서 SNI를 채우지만, macOS에 포함된 LibreSSL openssl은 채우지 않고, IP 주소로 연결하면 어떤 버전도 채우지 않습니다. SNI가 없으면 공유 호스트는 기본 인증서를 주거나 Cloudflare처럼 핸드셰이크를 중단하므로, 존재하지 않는 이름 불일치를 쫓게 됩니다.
4계층: 응답 헤더 읽기
TLS가 정상이니 이제 비로소 실제 HTTP 통신이 됩니다. 클라이언트가 요약해 준 내용이 아니라 서버가 실제로 한 말을 읽으세요. 여기서는 헤더만 출력합니다. 본문은 내려받은 뒤 버려지므로 클라이언트가 보내는 것과 같은 GET입니다.
curl -s -D - -o /dev/null https://waqtazan.com/api/v1/timings/egypt/cairo상태 줄을 먼저 읽고, 다음에 content-type, 그다음에 나머지를 읽으세요. 이 두 필드만 보면 요청이 성공했는지, 본문이 JSON인지 다른 것을 읽기 전에 알 수 있습니다.
정상적인 JSON 응답의 모습
위 명령을 2026년 10월 27일에 테스트 엔드포인트에 실행하자, 그 밖의 항목 중에서도 다음 헤더가 반환되었습니다.
| 헤더 | 값 | 알 수 있는 것 |
|---|---|---|
HTTP/2 | 200 | 요청이 성공했고 서버가 HTTP/2를 지원함 |
content-type | application/json | HTML 에러 페이지가 아니라 JSON을 받게 됨 |
content-length | 567 | 빈 본문이 아닌 실제 페이로드가 있음 |
access-control-allow-origin | * | 브라우저가 이 응답을 읽을 수 있음 |
server | cloudflare | 원본 서버 앞에 CDN이 있음 |
alt-svc | h3=":443" | 이후 요청에 HTTP/3을 안내함 |
5계층: 브라우저 호출만 깨뜨리는 헤더
이 계층에서 몇 시간씩 사라집니다. curl과 브라우저의 결과가 실제로 달라지는 곳이기 때문입니다. CORS는 서버가 아니라 브라우저가 강제합니다. Access-Control-Allow-Origin이 없어도 서버는 완전하고 올바른 200을 반환하지만, 브라우저가 JavaScript에 본문을 넘겨주기를 거부할 뿐입니다.
그래서 증상은 이렇습니다. curl은 완벽하게 동작하고 Postman도 완벽하게 동작하는데, 브라우저 콘솔에는 CORS 정책에 의해 요청이 차단되었다고 나옵니다. 서버에는 아무 문제가 없고, 응답에 헤더 하나가 빠졌을 뿐입니다.
테스트 엔드포인트는 access-control-allow-origin: *를 반환하므로 어느 오리진에서든 일반 fetch()가 동작합니다.
// Run it in the console of a tab on a site without a strict CSP, such as example.com
// (this page's own CSP blocks it). It works there because ACAO is set to *
const r = await fetch('https://waqtazan.com/api/v1/timings/egypt/cairo')
console.log(await r.json())프리플라이트 함정: 헤더를 추가하기 전까지는 동작함
두 번째 함정이 있으며, 이 엔드포인트가 잘 보여 줍니다. 단순 요청(커스텀 헤더 없는 일반 GET)은 성공합니다. 하지만 커스텀 헤더를 추가하거나 Content-Type: application/json으로 POST하는 순간, 브라우저는 먼저 OPTIONS 프리플라이트를 보내고, 그 응답에도 자체 CORS 헤더가 있어야 합니다(MDN).
프리플라이트는 다른 코드 경로에서 나오는 다른 응답이므로 따로 테스트하세요.
curl -s -D - -o /dev/null -X OPTIONS \
-H 'Origin: https://example.com' \
-H 'Access-Control-Request-Method: GET' \
https://waqtazan.com/api/v1/timings/egypt/cairo2026년 10월 27일 기준으로 이 프리플라이트는 content-type: text/html인 200을 반환하고 access-control-allow-origin 헤더는 전혀 없습니다. 그래서 일반 GET은 잘 되는데도 프리플라이트가 필요한 브라우저 요청은 차단됩니다.
이것이 바로 그 함정입니다. 개발 중에는 잘 동작하던 연동이 Authorization이나 X-Request-Id 헤더를 추가하면 서버 쪽 변경이 전혀 없는데도 깨집니다. 공개 데이터만 필요하다면 요청을 단순하게 유지하면 계속 동작합니다.
6계층: 페이로드가 기대한 형태인가?
마지막 계층은 코드가 실제로 신경 쓰는 계층입니다. 필드 이름이 바뀌었거나, 타입이 숫자에서 문자열로 바뀌었거나, 파싱하지 못하는 형식의 타임스탬프가 오면 유효한 JSON의 200도 코드를 깨뜨릴 수 있습니다.
파서를 작성하기 전에 실제 응답을 먼저 보세요. 이 엔드포인트는 표시용으로 포맷된 시간과 ISO 타임스탬프를 모두 반환하는데, 따라 할 만한 패턴입니다. 현지화된 문자열은 사람용이고 ISO 값은 코드용입니다.
{
"city": { "slug": "riyadh", "name_ar": "الرياض", "name_en": "Riyadh" },
"country": "saudi-arabia",
"date": "2026-10-26",
"times": { "fajr": "4:38 ص", "dhuhr": "11:37 ص", "maghrib": "5:17 م" },
"timesISO": { "fajr": "2026-10-26T01:38:00.000Z", "dhuhr": "2026-10-26T08:37:00.000Z" },
"method": "umm_al_qura"
}항상 기계가 읽을 수 있는 필드를 파싱하세요. 여기서는 times가 아니라 timesISO입니다. 표시용 문자열은 현지화되어 있어 로캘이 바뀌면 형식도 바뀝니다.
응답을 jq로 넘기면 구조를 빠르게 확인할 수 있습니다. curl -s <url> | jq keys는 최상위 필드를 나열하고, jq '.times'는 원본 본문을 스크롤하지 않고 안쪽을 살펴봅니다.
증상 → 계층 → 도구
매번 여섯 계층을 모두 거치지 않고 문제가 되는 계층으로 바로 가려면 이 표를 사용하세요.
| 보이는 증상 | 실패한 계층 | 확인 도구 |
|---|---|---|
ERR_NAME_NOT_RESOLVED, ENOTFOUND | 1 — DNS | DNS 조회 |
ECONNREFUSED, instant failure | 2 — 포트 | 포트 검사기 |
| 요청이 멈췄다가 타임아웃됨 | 2 — 필터링된 포트 | 포트 검사기 |
CERT_HAS_EXPIRED, UNABLE_TO_VERIFY_LEAF_SIGNATURE | 3 — TLS | SSL 검사기 |
| 브라우저에서는 되고 curl이나 Node에서는 실패 | 3 — 불완전한 체인 | SSL 검사기 |
401, 429, 503 | 4 — HTTP 상태 | HTTP 헤더 검사기 |
| curl에서는 되고 브라우저에서 차단됨 | 5 — CORS | HTTP 헤더 검사기 |
Unexpected token '<', "<!DOCTYPE "... is not valid JSON | 4 — HTML 에러 페이지 | HTTP 헤더 검사기 |
| 필드가 없거나 타입이 틀림 | 6 — 페이로드 | curl … | jq |
지연 시간은 이 단계에서 조금 벗어나 있습니다. 모든 계층이 통과하는데 응답이 느리다면 ping으로 호스트까지의 왕복 시간을 재고, DNS 속도 테스트로 이름 확인 자체가 병목인지 확인하세요.
엔드포인트의 실제 응답 헤더 확인
API가 반환하는 상태 코드, content-type, CORS 헤더를 그대로 확인하세요. 서버 측에서 요청하므로 브라우저 캐시나 확장 프로그램이 결과를 왜곡하지 않습니다. 무료, 가입 불필요.
출처
자주 묻는 질문
Postman 없이 API 엔드포인트를 테스트하려면?
curl -s -D - -o /dev/null <url>은 응답 헤더만, curl -s <url> | jq는 JSON 본문 확인용, curl -s -o /dev/null -w '%{http_code}'는 상태 코드만 출력합니다. 터미널 없이 빠르게 확인하려면 온라인 HTTP 헤더 검사기가 브라우저에서 같은 정보를 보여 줍니다.API가 curl에서는 되는데 브라우저에서는 실패하는 이유는?
curl이 상태 코드 000을 반환하면 무슨 뜻인가요?
-v를 붙여 다시 실행해 마지막으로 성공한 단계를 확인하세요.


