API 엔드포인트 테스트 방법: DNS·TLS·CORS 점검

핵심 요약

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

지금 확인하기

공개 API 엔드포인트를 테스트하는 6개 계층 도식: DNS 확인, 443 포트, TLS 인증서, HTTP 헤더, CORS, JSON 페이로드
공개 API 엔드포인트를 테스트하는 6개 계층 도식: DNS 확인, 443 포트, TLS 인증서, HTTP 헤더, CORS, JSON 페이로드
읽기 시작
  1. "API가 안 된다"가 하나의 문제가 아닌 이유
  2. 마음껏 호출할 수 있는 실제 엔드포인트 고르기
  3. 1계층: 호스트 이름이 확인되는가?
  4. 2계층: 443 포트가 열려 있는가?
  5. 3계층: TLS 인증서가 유효한가?
  6. 4계층: 응답 헤더 읽기
  7. 정상적인 JSON 응답의 모습
  8. 5계층: 브라우저 호출만 깨뜨리는 헤더
  9. 프리플라이트 함정: 헤더를 추가하기 전까지는 동작함
  10. 6계층: 페이로드가 기대한 형태인가?
  11. 증상 → 계층 → 도구
  12. 자주 묻는 질문

"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가 없는 페이지의 콘솔에서 동작합니다.

엔드포인트는 경로에 국가와 도시 슬러그를 그대로 받습니다.

bash
# 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가 쓰는 서브도메인이 애초에 만들어지지 않은 경우입니다.

무엇보다 먼저 호스트 이름을 확인하세요. 이 단계에서 아무것도 나오지 않으면 앱을 몇 번 재시작해도 소용없습니다. 클라이언트가 소켓을 여는 단계까지 가지도 못합니다.

bash
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초).
bash
# 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의 검증기를 사용해 통과할 수 있습니다). 이것이 "브라우저에서는 되는데 운영 환경에서는 안 된다"는 전형적인 버그 리포트의 원인입니다.

bash
# 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입니다.

bash
curl -s -D - -o /dev/null https://waqtazan.com/api/v1/timings/egypt/cairo

상태 줄을 먼저 읽고, 다음에 content-type, 그다음에 나머지를 읽으세요. 이 두 필드만 보면 요청이 성공했는지, 본문이 JSON인지 다른 것을 읽기 전에 알 수 있습니다.

정상적인 JSON 응답의 모습

위 명령을 2026년 10월 27일에 테스트 엔드포인트에 실행하자, 그 밖의 항목 중에서도 다음 헤더가 반환되었습니다.

헤더값알 수 있는 것
HTTP/2200요청이 성공했고 서버가 HTTP/2를 지원함
content-typeapplication/jsonHTML 에러 페이지가 아니라 JSON을 받게 됨
content-length567빈 본문이 아닌 실제 페이로드가 있음
access-control-allow-origin*브라우저가 이 응답을 읽을 수 있음
servercloudflare원본 서버 앞에 CDN이 있음
alt-svch3=":443"이후 요청에 HTTP/3을 안내함

5계층: 브라우저 호출만 깨뜨리는 헤더

이 계층에서 몇 시간씩 사라집니다. curl과 브라우저의 결과가 실제로 달라지는 곳이기 때문입니다. CORS는 서버가 아니라 브라우저가 강제합니다. Access-Control-Allow-Origin이 없어도 서버는 완전하고 올바른 200을 반환하지만, 브라우저가 JavaScript에 본문을 넘겨주기를 거부할 뿐입니다.

그래서 증상은 이렇습니다. curl은 완벽하게 동작하고 Postman도 완벽하게 동작하는데, 브라우저 콘솔에는 CORS 정책에 의해 요청이 차단되었다고 나옵니다. 서버에는 아무 문제가 없고, 응답에 헤더 하나가 빠졌을 뿐입니다.

테스트 엔드포인트는 access-control-allow-origin: *를 반환하므로 어느 오리진에서든 일반 fetch()가 동작합니다.

javascript
// 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).

프리플라이트는 다른 코드 경로에서 나오는 다른 응답이므로 따로 테스트하세요.

bash
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/cairo

2026년 10월 27일 기준으로 이 프리플라이트는 content-type: text/html인 200을 반환하고 access-control-allow-origin 헤더는 전혀 없습니다. 그래서 일반 GET은 잘 되는데도 프리플라이트가 필요한 브라우저 요청은 차단됩니다.

이것이 바로 그 함정입니다. 개발 중에는 잘 동작하던 연동이 Authorization이나 X-Request-Id 헤더를 추가하면 서버 쪽 변경이 전혀 없는데도 깨집니다. 공개 데이터만 필요하다면 요청을 단순하게 유지하면 계속 동작합니다.

6계층: 페이로드가 기대한 형태인가?

마지막 계층은 코드가 실제로 신경 쓰는 계층입니다. 필드 이름이 바뀌었거나, 타입이 숫자에서 문자열로 바뀌었거나, 파싱하지 못하는 형식의 타임스탬프가 오면 유효한 JSON의 200도 코드를 깨뜨릴 수 있습니다.

파서를 작성하기 전에 실제 응답을 먼저 보세요. 이 엔드포인트는 표시용으로 포맷된 시간과 ISO 타임스탬프를 모두 반환하는데, 따라 할 만한 패턴입니다. 현지화된 문자열은 사람용이고 ISO 값은 코드용입니다.

json
{
  "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, ENOTFOUND1 — DNSDNS 조회
ECONNREFUSED, instant failure2 — 포트포트 검사기
요청이 멈췄다가 타임아웃됨2 — 필터링된 포트포트 검사기
CERT_HAS_EXPIRED, UNABLE_TO_VERIFY_LEAF_SIGNATURE3 — TLSSSL 검사기
브라우저에서는 되고 curl이나 Node에서는 실패3 — 불완전한 체인SSL 검사기
401, 429, 5034 — HTTP 상태HTTP 헤더 검사기
curl에서는 되고 브라우저에서 차단됨5 — CORSHTTP 헤더 검사기
Unexpected token '<', "<!DOCTYPE "... is not valid JSON4 — HTML 에러 페이지HTTP 헤더 검사기
필드가 없거나 타입이 틀림6 — 페이로드curl … | jq

지연 시간은 이 단계에서 조금 벗어나 있습니다. 모든 계층이 통과하는데 응답이 느리다면 ping으로 호스트까지의 왕복 시간을 재고, DNS 속도 테스트로 이름 확인 자체가 병목인지 확인하세요.

엔드포인트의 실제 응답 헤더 확인

API가 반환하는 상태 코드, content-type, CORS 헤더를 그대로 확인하세요. 서버 측에서 요청하므로 브라우저 캐시나 확장 프로그램이 결과를 왜곡하지 않습니다. 무료, 가입 불필요.

출처

  1. Cross-Origin Resource Sharing (CORS), MDN
  2. Fetch Standard: CORS-safelisted request headers, WHATWG

자주 묻는 질문

Postman 없이 API 엔드포인트를 테스트하려면?

기본적인 테스트라면 curl이 Postman이 하는 일을 모두 할 수 있습니다. curl -s -D - -o /dev/null <url>은 응답 헤더만, curl -s <url> | jq는 JSON 본문 확인용, curl -s -o /dev/null -w '%{http_code}'는 상태 코드만 출력합니다. 터미널 없이 빠르게 확인하려면 온라인 HTTP 헤더 검사기가 브라우저에서 같은 정보를 보여 줍니다.

API가 curl에서는 되는데 브라우저에서는 실패하는 이유는?

거의 항상 CORS입니다. 서버는 어느 쪽에든 유효한 응답을 반환하지만, 브라우저는 응답에 내 오리진을 허용하는 Access-Control-Allow-Origin 헤더가 없으면 JavaScript에 본문을 노출하지 않습니다. CORS는 서버 정책이 아니라 브라우저 보안 정책이므로 curl은 완전히 무시합니다. 헤더가 없다면 서버가 보내야 하며 클라이언트 쪽 해결책은 없습니다.

curl이 상태 코드 000을 반환하면 무슨 뜻인가요?

HTTP 응답을 전혀 받지 못해 보고할 상태 코드가 없다는 뜻입니다. 보통 HTTP 이전 단계에서 실패한 것입니다. DNS가 확인되지 않았거나, TCP 연결이 거부 또는 필터링되었거나, TLS 핸드셰이크가 실패한 경우입니다. 연결을 받은 뒤 끊거나 응답하지 않는 서버도 000이 나오므로, -v를 붙여 다시 실행해 마지막으로 성공한 단계를 확인하세요.

API가 나만 안 되는지 모두에게 안 되는지 알려면?

내 네트워크 바깥에서 테스트하세요. 서버 측에서 실행되는 포트 검사기와 DNS 조회는 로컬 리졸버, VPN, 회사 방화벽, 브라우저 캐시를 변수에서 제거합니다. 외부 검사기에서는 응답하는데 내 컴퓨터에서는 응답하지 않는다면 문제는 로컬에 있으며, 보통 DNS 캐시나 방화벽 규칙입니다.

API가 200을 반환하는데 JSON 파서가 오류를 내는 이유는?

거의 확실하게 200 상태로 HTML을 받고 있는 것입니다. 캡티브 포털, 프록시, 일부 에러 페이지는 성공 코드와 함께 HTML 본문을 반환합니다. content-type 헤더를 확인하세요. application/json이 아니라 text/html이라면 본문은 웹 페이지이고, 파싱 오류는 원인이 아니라 증상입니다.

CORS 프리플라이트란 무엇이고 언제 발생하나요?

요청이 '단순'하지 않을 때 브라우저가 실제 요청 전에 자동으로 보내는 OPTIONS 요청입니다. GET, HEAD, POST 이외의 메서드를 쓰거나, Authorization 같은 커스텀 헤더를 설정하거나, 폼 데이터나 일반 텍스트가 아닌 Content-Type을 보낼 때(application/json도 해당) 발생합니다. 프리플라이트 응답에도 CORS 헤더가 있어야 합니다. 그래서 인증 헤더를 추가하기 전까지는 잘 동작하던 연동이 서버 변경 없이 깨질 수 있습니다.

API의 DNS는 내 리졸버와 공개 리졸버 중 어느 쪽으로 테스트해야 하나요?

둘 다 하고 비교하세요. ISP 리졸버는 권한 레코드가 바뀐 뒤에도 오래된 캐시 레코드를 계속 줄 수 있습니다. 기본 리졸버와 함께 1.1.1.1, 8.8.8.8에도 질의하면 전파 지연인지 정말 레코드가 없는 것인지 알 수 있습니다.

443 포트가 열려 있으면 HTTPS 엔드포인트가 동작하는 건가요?

아닙니다. 열린 포트는 무언가가 TCP 연결을 받았다는 사실만 확인해 줍니다. 인증서가 만료되었거나, 호스트 이름이 맞지 않거나, 중간 인증서 체인이 불완전하면 TLS 핸드셰이크는 여전히 실패할 수 있습니다. 포트가 열려 있음을 확인한 뒤 인증서를 따로 확인하세요.

작성자 소개

Shaik Vahid

DNS Robot 창립자. Vahid는 수년간 웹 호스팅 지원 업무를 하며 고객의 도메인, DNS 레코드, SSL 인증서, 이메일 전송 문제를 해결했습니다. DNS Robot의 도구를 만들고, 사람들이 실제로 겪는 오류를 바탕으로 이 가이드를 씁니다.

HTTP 헤더 검사

HTTP 응답 헤더 분석 및 보안 헤더 등급 평가